Added Location and location ownership/rights to gamenight.

This commit is contained in:
2025-12-24 14:48:54 +01:00
parent 8a48119c80
commit ff88029a4b
57 changed files with 3034 additions and 995 deletions

860
backend-actix/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,8 @@
use std::{fs::{exists, read_dir, remove_dir_all, File}, io::Write, process::Command};
use std::{
fs::{exists, read_dir, remove_dir_all, File},
io::Write,
process::Command,
};
fn main() {
println!("cargo::rerun-if-changed=gamenight-api.yaml");
@@ -7,12 +10,19 @@ fn main() {
if exists("src/models").unwrap() {
remove_dir_all("src/models").unwrap();
}
let _ =
Command::new("openapi-generator")
.args(["generate", "-i", "gamenight-api.yaml", "-g", "rust", "--global-property", "models"])
.output()
.expect("Failed to generate models sources for the gamenight API");
let _ = Command::new("openapi-generator")
.args([
"generate",
"-i",
"gamenight-api.yaml",
"-g",
"rust",
"--global-property",
"models",
])
.output()
.expect("Failed to generate models sources for the gamenight API");
let mut file = File::create("src/models/mod.rs").unwrap();
let paths = read_dir("./src/models").unwrap();
@@ -21,10 +31,10 @@ fn main() {
let path = path.path();
let stem = path.file_stem().unwrap();
if stem == "mod" {
continue
continue;
}
let line = format!("pub mod {};\n", stem.to_str().unwrap());
let _ = file.write(line.as_bytes()).unwrap();
}
}
}

View File

@@ -38,6 +38,17 @@ paths:
parameters: []
security:
- JWT-Auth: []
/users:
get:
responses:
'200':
$ref: '#/components/responses/UsersResponse'
'400':
$ref: '#/components/responses/FailureResponse'
'401':
$ref: '#/components/responses/FailureResponse'
security:
- JWT-Auth: []
/user:
post:
summary: ''
@@ -242,9 +253,72 @@ paths:
$ref: '#/components/requestBodies/OwnedGamesRequest'
security:
- JWT-Auth: []
/location:
get:
responses:
'200':
$ref: '#/components/responses/LocationResponse'
'401':
$ref: '#/components/responses/FailureResponse'
'422':
$ref: '#/components/responses/FailureResponse'
requestBody:
$ref: '#/components/requestBodies/GetLocationRequest'
security:
- JWT-Auth: []
post:
responses:
'200':
description: 'Ok'
$ref: '#/components/responses/LocationIdResponse'
'401':
$ref: '#/components/responses/FailureResponse'
'422':
$ref: '#/components/responses/FailureResponse'
requestBody:
$ref: '#/components/requestBodies/AddLocationRequest'
security:
- JWT-Auth: []
/locations:
get:
responses:
'200':
$ref: '#/components/responses/LocationsResponse'
'401':
$ref: '#/components/responses/FailureResponse'
'422':
$ref: '#/components/responses/FailureResponse'
security:
- JWT-Auth: []
/location_authorize:
post:
responses:
'200':
description: 'Ok'
'401':
$ref: '#/components/responses/FailureResponse'
'422':
$ref: '#/components/responses/FailureResponse'
requestBody:
$ref: '#/components/requestBodies/AuthorizeLocationRequest'
security:
- JWT-Auth: []
/authorized_location_user_ids:
get:
responses:
'200':
$ref: "#/components/responses/UserIdsResponse"
'401':
$ref: '#/components/responses/FailureResponse'
'422':
$ref: '#/components/responses/FailureResponse'
requestBody:
$ref: '#/components/requestBodies/AuthorizedLocationUserIdsRequest'
security:
- JWT-Auth: []
components:
schemas:
Gamenight:
@@ -326,6 +400,14 @@ components:
type: string
required:
- user_id
LocationId:
title: LocationId
type: object
properties:
location_id:
type: string
required:
- location_id
AddGamenightRequestBody:
title: AddGamenightRequestBody
type: object
@@ -401,7 +483,49 @@ components:
type: array
items:
type: string
UserIdsResponse:
type: array
items:
$ref: "#/components/schemas/UserId"
AddLocationRequestBody:
type: object
properties:
name:
type: string
address:
type: string
note:
type: string
required:
- name
Location:
type: object
properties:
id:
type: string
name:
type: string
address:
type: string
note:
type: string
required:
- id
- name
AuthorizeLocationRequestBody:
type: object
properties:
location_id:
type: string
user_id:
type: string
op:
type: string
enum: [grant, revoke]
required:
- location_id
- user_id
- op
requestBodies:
LoginRequest:
content:
@@ -473,6 +597,27 @@ components:
application/json:
schema:
$ref: '#/components/schemas/UserId'
GetLocationRequest:
content:
application/json:
schema:
$ref: '#/components/schemas/LocationId'
AddLocationRequest:
content:
application/json:
schema:
$ref: '#/components/schemas/AddLocationRequestBody'
AuthorizeLocationRequest:
content:
application/json:
schema:
$ref: '#/components/schemas/AuthorizeLocationRequestBody'
AuthorizedLocationUserIdsRequest:
content:
application/json:
schema:
$ref: '#/components/schemas/LocationId'
responses:
TokenResponse:
description: Example response
@@ -500,6 +645,14 @@ components:
type: array
items:
$ref: '#/components/schemas/Gamenight'
UsersResponse:
description: List of all Users
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
GamenightResponse:
description: A gamenight being hosted
content:
@@ -532,6 +685,34 @@ components:
application/json:
schema:
$ref: '#/components/schemas/GameIdsResponse'
UserIdsResponse:
description: A list of user ids.
content:
application/json:
schema:
$ref: '#/components/schemas/UserIdsResponse'
LocationResponse:
description: A location
content:
application/json:
schema:
$ref: '#/components/schemas/Location'
LocationIdResponse:
description: A location Id
content:
application/json:
schema:
$ref: '#/components/schemas/LocationId'
LocationsResponse:
description: A list of all LocationsResponse
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Location'
securitySchemes:
JWT-Auth:
type: http

View File

@@ -3,19 +3,19 @@ pub mod models;
pub mod request;
use actix_cors::Cors;
use actix_web::middleware::Logger;
use actix_web::HttpServer;
use actix_web::App;
use actix_web::http;
use actix_web::middleware::Logger;
use actix_web::web;
use request::{*, login, register, gamenights};
use tracing_actix_web::TracingLogger;
use actix_web::App;
use actix_web::HttpServer;
use gamenight_database::*;
use request::{gamenights, login, register, *};
use tracing_actix_web::TracingLogger;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let url = "postgres://root:root@127.0.0.1/gamenight";
let pool = get_connection_pool(url);
let mut conn = pool.get_conn();
@@ -26,11 +26,11 @@ async fn main() -> std::io::Result<()> {
HttpServer::new(move || {
let cors = Cors::default()
.allowed_origin("0.0.0.0")
.allowed_origin_fn(|_origin, _req_head| { true })
.allowed_methods(vec!["GET", "POST"])
.allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT])
.allowed_header(http::header::CONTENT_TYPE)
.max_age(3600);
.allowed_origin_fn(|_origin, _req_head| true)
.allowed_methods(vec!["GET", "POST"])
.allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT])
.allowed_header(http::header::CONTENT_TYPE)
.max_age(3600);
App::new()
.wrap(cors)
@@ -44,6 +44,7 @@ async fn main() -> std::io::Result<()> {
.service(gamenight_post)
.service(gamenight_get)
.service(get_user)
.service(get_users)
.service(get_user_unauthenticated)
.service(post_join_gamenight)
.service(post_leave_gamenight)
@@ -55,8 +56,12 @@ async fn main() -> std::io::Result<()> {
.service(post_own_game)
.service(post_disown_game)
.service(get_owned_games)
.service(get_locations)
.service(post_location)
.service(post_location_authorize)
.service(get_authorized_location_user_ids)
})
.bind(("::1", 8080))?
.run()
.await
}
}

View File

@@ -1,19 +1,22 @@
use std::future::{Ready, ready};
use std::future::{ready, Ready};
use actix_web::{FromRequest, http, HttpRequest, dev::Payload, web::Data};
use actix_web::{dev::Payload, http, web::Data, FromRequest, HttpRequest};
use chrono::Utc;
use jsonwebtoken::{encode, Header, EncodingKey, decode, DecodingKey, Validation};
use serde::{Serialize, Deserialize};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use gamenight_database::{user::{get_user, User}, DbPool};
use gamenight_database::{
user::{get_user, User},
DbPool,
};
use super::error::ApiError;
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
exp: i64,
uid: Uuid
uid: Uuid,
}
pub struct AuthUser(pub User);
@@ -25,24 +28,31 @@ pub struct AuthUser(pub User);
// pub role: Role,
// }
impl From<User> for AuthUser {
impl From<User> for AuthUser {
fn from(value: User) -> Self {
Self(value)
}
}
fn get_claims(req: &HttpRequest) -> Result<Claims, ApiError> {
let token = req.headers()
fn get_claims(req: &HttpRequest) -> Result<Claims, ApiError> {
let token = req
.headers()
.get(http::header::AUTHORIZATION)
.map(|h| h.to_str().unwrap().split_at(7).1.to_string());
let token = token.ok_or(ApiError{
let token = token.ok_or(ApiError {
status: 400,
message: "JWT-token was not specified in the Authorization header as Bearer: token".to_string()
message: "JWT-token was not specified in the Authorization header as Bearer: token"
.to_string(),
})?;
let secret = "secret";
Ok(decode::<Claims>(token.as_str(), &DecodingKey::from_secret(secret.as_bytes()), &Validation::default())?.claims)
Ok(decode::<Claims>(
token.as_str(),
&DecodingKey::from_secret(secret.as_bytes()),
&Validation::default(),
)?
.claims)
}
pub fn get_token(user: &User) -> Result<String, ApiError> {
@@ -55,7 +65,8 @@ pub fn get_token(user: &User) -> Result<String, ApiError> {
Ok(encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(secret.as_bytes()))?)
&EncodingKey::from_secret(secret.as_bytes()),
)?)
}
impl FromRequest for AuthUser {
@@ -63,15 +74,15 @@ impl FromRequest for AuthUser {
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
ready(
(|| -> Result<AuthUser, ApiError>{
let pool = req.app_data::<Data<DbPool>>().expect("No database configured");
let mut conn = pool.get().expect("couldn't get db connection from pool");
let uid = get_claims(req)?.uid;
let user = get_user(&mut conn, uid)?;
Ok(user.into())
})()
)
ready((|| -> Result<AuthUser, ApiError> {
let pool = req
.app_data::<Data<DbPool>>()
.expect("No database configured");
let mut conn = pool.get().expect("couldn't get db connection from pool");
let uid = get_claims(req)?.uid;
let user = get_user(&mut conn, uid)?;
Ok(user.into())
})())
}
}
}

View File

@@ -1,6 +1,10 @@
use actix_web::{
error::BlockingError,
http::{header::ContentType, StatusCode},
HttpResponse, ResponseError,
};
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter, Result};
use actix_web::{ResponseError, error::BlockingError, HttpResponse, http::{header::ContentType, StatusCode}};
use serde::{Serialize, Deserialize};
use validator::ValidationErrors;
use gamenight_database::error::DatabaseError;
@@ -9,7 +13,7 @@ use gamenight_database::error::DatabaseError;
pub struct ApiError {
#[serde(skip_serializing)]
pub status: u16,
pub message: String
pub message: String,
}
impl Display for ApiError {
@@ -21,9 +25,9 @@ impl Display for ApiError {
impl ResponseError for ApiError {
fn error_response(&self) -> HttpResponse {
HttpResponse::build(StatusCode::from_u16(self.status).unwrap())
.content_type(ContentType::json())
.body(serde_json::to_string(&self).unwrap())
}
.content_type(ContentType::json())
.body(serde_json::to_string(&self).unwrap())
}
}
impl From<DatabaseError> for ApiError {
@@ -31,7 +35,7 @@ impl From<DatabaseError> for ApiError {
ApiError {
//Todo, split this in unrecoverable and schema error
status: 500,
message: value.0
message: value.0,
}
}
}
@@ -40,7 +44,7 @@ impl From<BlockingError> for ApiError {
fn from(value: BlockingError) -> Self {
ApiError {
status: 500,
message: value.to_string()
message: value.to_string(),
}
}
}
@@ -49,7 +53,7 @@ impl From<serde_json::Error> for ApiError {
fn from(value: serde_json::Error) -> Self {
ApiError {
status: 500,
message: value.to_string()
message: value.to_string(),
}
}
}
@@ -58,7 +62,7 @@ impl From<jsonwebtoken::errors::Error> for ApiError {
fn from(value: jsonwebtoken::errors::Error) -> Self {
ApiError {
status: 500,
message: value.to_string()
message: value.to_string(),
}
}
}
@@ -67,7 +71,7 @@ impl From<ValidationErrors> for ApiError {
fn from(value: ValidationErrors) -> Self {
ApiError {
status: 422,
message: value.to_string()
message: value.to_string(),
}
}
}
@@ -76,7 +80,7 @@ impl From<chrono::ParseError> for ApiError {
fn from(value: chrono::ParseError) -> Self {
ApiError {
status: 422,
message: value.to_string()
message: value.to_string(),
}
}
}
@@ -85,7 +89,7 @@ impl From<uuid::Error> for ApiError {
fn from(value: uuid::Error) -> Self {
ApiError {
status: 422,
message: value.to_string()
message: value.to_string(),
}
}
}

View File

@@ -1,53 +1,73 @@
use actix_web::{get, http::header::ContentType, post, web, HttpResponse, Responder};
use gamenight_database::{game::{insert_game, load_game, rename_game}, owned_game::{disown_game, own_game, owned_games, OwnedGame}, DbPool, GetConnection};
use gamenight_database::{
game::{insert_game, load_game, rename_game},
owned_game::{disown_game, own_game, owned_games, OwnedGame},
DbPool, GetConnection,
};
use uuid::Uuid;
use crate::{models::{add_game_request_body::AddGameRequestBody, game::Game, game_id::GameId, rename_game_request_body::RenameGameRequestBody}, request::{authorization::AuthUser, error::ApiError}};
use crate::{
models::{
add_game_request_body::AddGameRequestBody, game::Game, game_id::GameId,
rename_game_request_body::RenameGameRequestBody,
},
request::{authorization::AuthUser, error::ApiError},
};
#[get("/games")]
pub async fn get_games(pool: web::Data<DbPool>, _user: AuthUser) -> Result<impl Responder, ApiError> {
pub async fn get_games(
pool: web::Data<DbPool>,
_user: AuthUser,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
let games: Vec<gamenight_database::game::Game> = gamenight_database::games(&mut conn)?;
let model: Vec<Game> = games.iter().map(|x| {
Game {
let model: Vec<Game> = games
.iter()
.map(|x| Game {
id: x.id.to_string(),
name: x.name.clone(),
}}
).collect();
})
.collect();
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&model)?)
)
.body(serde_json::to_string(&model)?))
}
impl From<AddGameRequestBody> for gamenight_database::game::Game {
fn from(value: AddGameRequestBody) -> Self {
Self {
id: Uuid::new_v4(),
name: value.name
name: value.name,
}
}
}
#[get("/game")]
pub async fn get_game(pool: web::Data<DbPool>, _user: AuthUser, game_id: web::Json<GameId>) -> Result<impl Responder, ApiError> {
pub async fn get_game(
pool: web::Data<DbPool>,
_user: AuthUser,
game_id: web::Json<GameId>,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
let db_game = load_game(&mut conn, Uuid::parse_str(&game_id.0.game_id)?)?;
let model = Game {
let model = Game {
id: db_game.id.to_string(),
name: db_game.name
name: db_game.name,
};
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&model)?)
)
.body(serde_json::to_string(&model)?))
}
#[post("/game")]
pub async fn post_game(pool: web::Data<DbPool>, _user: AuthUser, game_data: web::Json<AddGameRequestBody>) -> Result<impl Responder, ApiError> {
pub async fn post_game(
pool: web::Data<DbPool>,
_user: AuthUser,
game_data: web::Json<AddGameRequestBody>,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
insert_game(&mut conn, game_data.0.into())?;
@@ -55,40 +75,68 @@ pub async fn post_game(pool: web::Data<DbPool>, _user: AuthUser, game_data: web:
}
#[post("/rename_game")]
pub async fn post_rename_game(pool: web::Data<DbPool>, _user: AuthUser, game_data: web::Json<RenameGameRequestBody>) -> Result <impl Responder, ApiError> {
pub async fn post_rename_game(
pool: web::Data<DbPool>,
_user: AuthUser,
game_data: web::Json<RenameGameRequestBody>,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
rename_game(&mut conn, Uuid::parse_str(&game_data.0.id)?, game_data.0.name)?;
rename_game(
&mut conn,
Uuid::parse_str(&game_data.0.id)?,
game_data.0.name,
)?;
Ok(HttpResponse::Ok())
}
#[post("/own")]
pub async fn post_own_game(pool: web::Data<DbPool>, user: AuthUser, game_id: web::Json<GameId>) -> Result <impl Responder, ApiError> {
pub async fn post_own_game(
pool: web::Data<DbPool>,
user: AuthUser,
game_id: web::Json<GameId>,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
own_game(&mut conn, OwnedGame { user_id: user.0.id, game_id: Uuid::parse_str(&game_id.0.game_id)? })?;
own_game(
&mut conn,
OwnedGame {
user_id: user.0.id,
game_id: Uuid::parse_str(&game_id.0.game_id)?,
},
)?;
Ok(HttpResponse::Ok())
}
#[post("/disown")]
pub async fn post_disown_game(pool: web::Data<DbPool>, user: AuthUser, game_id: web::Json<GameId>) -> Result <impl Responder, ApiError> {
pub async fn post_disown_game(
pool: web::Data<DbPool>,
user: AuthUser,
game_id: web::Json<GameId>,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
disown_game(&mut conn, OwnedGame { user_id: user.0.id, game_id: Uuid::parse_str(&game_id.0.game_id)? })?;
disown_game(
&mut conn,
OwnedGame {
user_id: user.0.id,
game_id: Uuid::parse_str(&game_id.0.game_id)?,
},
)?;
Ok(HttpResponse::Ok())
}
#[get("/owned_games")]
pub async fn get_owned_games(pool: web::Data<DbPool>, user: AuthUser) -> Result <impl Responder, ApiError> {
pub async fn get_owned_games(
pool: web::Data<DbPool>,
user: AuthUser,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
let game_ids = owned_games(&mut conn, user.0.id)?;
let model : Vec<String> = game_ids.iter().map(|x| {
x.to_string()
}).collect();
let model: Vec<String> = game_ids.iter().map(|x| x.to_string()).collect();
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&model)?)
)
.body(serde_json::to_string(&model)?))
}

View File

@@ -1,12 +1,17 @@
use actix_web::{get, web, Responder, http::header::ContentType, HttpResponse, post};
use actix_web::{get, http::header::ContentType, post, web, HttpResponse, Responder};
use chrono::{DateTime, ParseError};
use uuid::Uuid;
use gamenight_database::{gamenight, DbPool, GetConnection};
use crate::{models::{add_gamenight_request_body::AddGamenightRequestBody, gamenight::Gamenight, get_gamenight_request_body::GetGamenightRequestBody}, request::authorization::AuthUser};
use crate::request::error::ApiError;
use crate::{
models::{
add_gamenight_request_body::AddGamenightRequestBody, gamenight::Gamenight,
get_gamenight_request_body::GetGamenightRequestBody,
},
request::authorization::AuthUser,
};
impl AddGamenightRequestBody {
pub fn into_with_user(&self, user: AuthUser) -> Result<gamenight::Gamenight, ParseError> {
@@ -14,7 +19,8 @@ impl AddGamenightRequestBody {
datetime: DateTime::parse_from_rfc3339(&self.datetime)?.with_timezone(&chrono::Utc),
id: Uuid::new_v4(),
name: self.name.clone(),
owner_id: user.0.id
owner_id: user.0.id,
location_id: None,
})
}
}
@@ -26,38 +32,49 @@ impl From<GetGamenightRequestBody> for Uuid {
}
#[get("/gamenights")]
pub async fn gamenights(pool: web::Data<DbPool>, _user: AuthUser) -> Result<impl Responder, ApiError> {
pub async fn gamenights(
pool: web::Data<DbPool>,
_user: AuthUser,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
let gamenights: Vec<gamenight::Gamenight> = gamenight_database::gamenights(&mut conn)?;
let model: Vec<Gamenight> = gamenights.iter().map(|x| {
Gamenight {
let model: Vec<Gamenight> = gamenights
.iter()
.map(|x| Gamenight {
id: x.id.to_string(),
name: x.name.clone(),
datetime: x.datetime.to_rfc3339(),
owner_id: x.owner_id.to_string()
}}
).collect();
owner_id: x.owner_id.to_string(),
})
.collect();
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&model)?)
)
.body(serde_json::to_string(&model)?))
}
#[post("/gamenight")]
pub async fn gamenight_post(pool: web::Data<DbPool>, user: AuthUser, gamenight_data: web::Json<AddGamenightRequestBody>) -> Result<impl Responder, ApiError> {
pub async fn gamenight_post(
pool: web::Data<DbPool>,
user: AuthUser,
gamenight_data: web::Json<AddGamenightRequestBody>,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
gamenight::add_gamenight(&mut conn, gamenight_data.into_with_user(user)?)?;
Ok(HttpResponse::Ok())
}
#[get("/gamenight")]
pub async fn gamenight_get(pool: web::Data<DbPool>, _user: AuthUser, gamenight_data: web::Json<GetGamenightRequestBody>) -> Result<impl Responder, ApiError> {
pub async fn gamenight_get(
pool: web::Data<DbPool>,
_user: AuthUser,
gamenight_data: web::Json<GetGamenightRequestBody>,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
let gamenight = gamenight::get_gamenight(&mut conn, gamenight_data.into_inner().into())?;
let model = Gamenight{
let model = Gamenight {
id: gamenight.id.to_string(),
datetime: gamenight.datetime.to_rfc3339(),
name: gamenight.name,
@@ -67,4 +84,4 @@ pub async fn gamenight_get(pool: web::Data<DbPool>, _user: AuthUser, gamenight_d
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&model)?))
}
}

View File

@@ -1,34 +1,55 @@
use actix_web::{post, web, HttpResponse, Responder};
use gamenight_database::{gamenight_participants::{delete_gamenight_participant, insert_gamenight_participant, GamenightParticipant}, DbPool, GetConnection};
use gamenight_database::{
gamenight_participants::{
delete_gamenight_participant, insert_gamenight_participant, GamenightParticipant,
},
DbPool, GetConnection,
};
use uuid::Uuid;
use crate::{models::gamenight_id::GamenightId, request::{authorization::AuthUser, error::ApiError}};
use crate::{
models::gamenight_id::GamenightId,
request::{authorization::AuthUser, error::ApiError},
};
#[post("/join")]
pub async fn post_join_gamenight(pool: web::Data<DbPool>, user: AuthUser, gamenight_id: web::Json<GamenightId>) -> Result<impl Responder, ApiError> {
pub async fn post_join_gamenight(
pool: web::Data<DbPool>,
user: AuthUser,
gamenight_id: web::Json<GamenightId>,
) -> Result<impl Responder, ApiError> {
web::block(move || -> Result<usize, ApiError> {
let mut conn = pool.get_conn();
Ok(insert_gamenight_participant(&mut conn, GamenightParticipant {
gamenight_id: Uuid::parse_str(&gamenight_id.gamenight_id)?,
user_id: user.0.id
})?)
}).await??;
Ok(insert_gamenight_participant(
&mut conn,
GamenightParticipant {
gamenight_id: Uuid::parse_str(&gamenight_id.gamenight_id)?,
user_id: user.0.id,
},
)?)
})
.await??;
Ok(HttpResponse::Ok())
}
#[post("/leave")]
pub async fn post_leave_gamenight(pool: web::Data<DbPool>, user: AuthUser, gamenight_id: web::Json<GamenightId>) -> Result<impl Responder, ApiError> {
pub async fn post_leave_gamenight(
pool: web::Data<DbPool>,
user: AuthUser,
gamenight_id: web::Json<GamenightId>,
) -> Result<impl Responder, ApiError> {
web::block(move || -> Result<usize, ApiError> {
let mut conn = pool.get_conn();
let participant = GamenightParticipant {
gamenight_id: Uuid::parse_str(&gamenight_id.gamenight_id)?,
user_id: user.0.id
user_id: user.0.id,
};
let x = delete_gamenight_participant(&mut conn, participant)?;
Ok(x)
}).await??;
})
.await??;
Ok(HttpResponse::Ok())
}

View File

@@ -0,0 +1,65 @@
use actix_web::{get, http::header::ContentType, post, web, HttpResponse, Responder};
use gamenight_database::{
location::{insert_location, locations},
DbPool, GetConnection,
};
use uuid::Uuid;
use crate::{
models::{
add_location_request_body::AddLocationRequestBody, location::Location,
location_id::LocationId,
},
request::{authorization::AuthUser, error::ApiError},
};
impl From<AddLocationRequestBody> for gamenight_database::location::Location {
fn from(value: AddLocationRequestBody) -> Self {
Self {
id: Uuid::new_v4(),
name: value.name,
address: value.address,
note: value.note,
}
}
}
#[get("/locations")]
pub async fn get_locations(
pool: web::Data<DbPool>,
_user: AuthUser,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
let games: Vec<gamenight_database::location::Location> = locations(&mut conn)?;
let model: Vec<Location> = games
.iter()
.map(|x| Location {
id: x.id.to_string(),
name: x.name.clone(),
address: x.address.clone(),
note: x.note.clone(),
})
.collect();
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&model)?))
}
#[post("/location")]
pub async fn post_location(
pool: web::Data<DbPool>,
_user: AuthUser,
game_data: web::Json<AddLocationRequestBody>,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
let uuid = insert_location(&mut conn, game_data.0.into())?;
let model = LocationId {
location_id: uuid.to_string(),
};
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&model)?))
}

View File

@@ -0,0 +1,79 @@
use actix_web::{get, http::header::ContentType, post, web, HttpResponse, Responder};
use gamenight_database::{
location_owner::{grant_permission, location_permissions, revoke_permission, LocationOwner},
user::Role,
DbPool, GetConnection,
};
use uuid::Uuid;
use crate::{
models::{
authorize_location_request_body::{
AuthorizeLocationRequestBody,
Op::{Grant, Revoke},
},
location_id::LocationId,
user_id::UserId,
},
request::{authorization::AuthUser, error::ApiError},
};
impl<'a> TryFrom<&'a AuthorizeLocationRequestBody> for LocationOwner {
type Error = ApiError;
fn try_from(value: &'a AuthorizeLocationRequestBody) -> Result<Self, Self::Error> {
Ok(LocationOwner {
location_id: Uuid::parse_str(&value.location_id)?,
user_id: Uuid::parse_str(&value.user_id)?,
})
}
}
#[post("/location_authorize")]
pub async fn post_location_authorize(
pool: web::Data<DbPool>,
user: AuthUser,
auth_data: web::Json<AuthorizeLocationRequestBody>,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
let inner_auth_data = &auth_data.into_inner();
let location_owner: LocationOwner = inner_auth_data.try_into()?;
let authorized = location_permissions(&mut conn, location_owner.location_id)?;
if user.0.role != Role::Admin && !authorized.contains(&user.0.id) {
Ok(HttpResponse::Unauthorized())
} else {
match inner_auth_data.op {
Grant => grant_permission(&mut conn, location_owner)?,
Revoke => revoke_permission(&mut conn, location_owner)?,
};
Ok(HttpResponse::Ok())
}
}
impl From<Uuid> for UserId {
fn from(value: Uuid) -> Self {
Self {
user_id: value.to_string(),
}
}
}
#[get("/authorized_location_user_ids")]
pub async fn get_authorized_location_user_ids(
pool: web::Data<DbPool>,
_user: AuthUser,
location_id: web::Json<LocationId>,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
let permissions =
location_permissions(&mut conn, Uuid::parse_str(&location_id.0.location_id)?)?;
let model: Vec<UserId> = permissions.into_iter().map(Into::into).collect();
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&model)?))
}

View File

@@ -1,27 +1,33 @@
mod user_handlers;
mod gamenight_handlers;
mod error;
mod authorization;
mod join_gamenight;
mod participant_handlers;
mod error;
mod game;
mod gamenight_handlers;
mod join_gamenight;
mod location;
mod location_owner;
mod participant_handlers;
mod user_handlers;
pub use game::get_game;
pub use game::get_games;
pub use game::get_owned_games;
pub use game::post_disown_game;
pub use game::post_game;
pub use game::post_own_game;
pub use game::post_rename_game;
pub use gamenight_handlers::gamenight_get;
pub use gamenight_handlers::gamenight_post;
pub use gamenight_handlers::gamenights;
pub use join_gamenight::post_join_gamenight;
pub use join_gamenight::post_leave_gamenight;
pub use location::get_locations;
pub use location::post_location;
pub use location_owner::get_authorized_location_user_ids;
pub use location_owner::post_location_authorize;
pub use participant_handlers::get_get_participants;
pub use user_handlers::get_user;
pub use user_handlers::get_user_unauthenticated;
pub use user_handlers::get_users;
pub use user_handlers::login;
pub use user_handlers::refresh;
pub use user_handlers::register;
pub use gamenight_handlers::gamenights;
pub use gamenight_handlers::gamenight_post;
pub use gamenight_handlers::gamenight_get;
pub use user_handlers::get_user;
pub use user_handlers::get_user_unauthenticated;
pub use join_gamenight::post_join_gamenight;
pub use join_gamenight::post_leave_gamenight;
pub use participant_handlers::get_get_participants;
pub use game::get_games;
pub use game::get_game;
pub use game::post_game;
pub use game::post_rename_game;
pub use game::post_own_game;
pub use game::post_disown_game;
pub use game::get_owned_games;

View File

@@ -2,16 +2,30 @@ use actix_web::{get, http::header::ContentType, web, HttpResponse, Responder};
use gamenight_database::{DbPool, GetConnection};
use uuid::Uuid;
use crate::{models::{gamenight_id::GamenightId, participants::Participants}, request::{authorization::AuthUser, error::ApiError}};
use crate::{
models::{gamenight_id::GamenightId, participants::Participants},
request::{authorization::AuthUser, error::ApiError},
};
#[get("/participants")]
pub async fn get_get_participants(pool: web::Data<DbPool>, _user: AuthUser, gamenight_info: web::Json<GamenightId>) -> Result<impl Responder, ApiError> {
pub async fn get_get_participants(
pool: web::Data<DbPool>,
_user: AuthUser,
gamenight_info: web::Json<GamenightId>,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
let users = gamenight_database::get_participants(&mut conn, &Uuid::parse_str(&gamenight_info.into_inner().gamenight_id)?)?
.iter().map(|x| x.to_string()).collect();
let users = gamenight_database::get_participants(
&mut conn,
&Uuid::parse_str(&gamenight_info.into_inner().gamenight_id)?,
)?
.iter()
.map(|x| x.to_string())
.collect();
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&Participants{participants: users})?))
}
.body(serde_json::to_string(&Participants {
participants: users,
})?))
}

View File

@@ -1,19 +1,14 @@
use actix_web::http::header::ContentType;
use actix_web::{get, post, web, HttpResponse, Responder};
use crate::models::{
login::Login, registration::Registration, token::Token, user::User, user_id::UserId,
};
use crate::request::{authorization::get_token, error::ApiError};
use actix_web::{get, http::header::ContentType, post, web, HttpResponse, Responder};
use gamenight_database::user::{count_users_with_email, count_users_with_username};
use gamenight_database::{DbPool, GetConnection};
use serde::{Deserialize, Serialize};
use serde_json;
use uuid::Uuid;
use validator::{Validate, ValidateArgs, ValidationError};
use crate::models::login::Login;
use crate::models::registration::Registration;
use crate::models::token::Token;
use crate::models::user::User;
use crate::models::user_id::UserId;
use crate::request::error::ApiError;
use crate::request::authorization::get_token;
use serde_json;
use gamenight_database::{DbPool, GetConnection};
use super::authorization::AuthUser;
@@ -21,7 +16,7 @@ impl From<Login> for gamenight_database::user::LoginUser {
fn from(val: Login) -> Self {
gamenight_database::user::LoginUser {
username: val.username,
password: val.password
password: val.password,
}
}
}
@@ -31,20 +26,22 @@ impl From<Registration> for gamenight_database::user::Register {
gamenight_database::user::Register {
email: val.email,
username: val.username,
password: val.password
password: val.password,
}
}
}
pub struct RegisterContext<'v_a> {
pub pool: &'v_a DbPool
pub pool: &'v_a DbPool,
}
pub fn unique_username(username: &String, context: &RegisterContext) -> Result<(), ValidationError> {
pub fn unique_username(
username: &String,
context: &RegisterContext,
) -> Result<(), ValidationError> {
let mut conn = context.pool.get_conn();
match count_users_with_username(&mut conn, username)
{
match count_users_with_username(&mut conn, username) {
Ok(0) => Ok(()),
Ok(_) => Err(ValidationError::new("User already exists")),
Err(_) => Err(ValidationError::new("Database error while validating user")),
@@ -54,28 +51,23 @@ pub fn unique_username(username: &String, context: &RegisterContext) -> Result<(
pub fn unique_email(email: &String, context: &RegisterContext) -> Result<(), ValidationError> {
let mut conn = context.pool.get_conn();
match count_users_with_email(&mut conn, email)
{
match count_users_with_email(&mut conn, email) {
Ok(0) => Ok(()),
Ok(_) => Err(ValidationError::new("email already exists")),
Err(_) => Err(ValidationError::new("Database error while validating email"))
Err(_) => Err(ValidationError::new(
"Database error while validating email",
)),
}
}
#[derive(Serialize, Deserialize, Clone, Validate)]
#[validate(context = RegisterContext::<'v_a>)]
pub struct ValidatableRegistration {
#[validate(
length(min = 1),
custom(function = "unique_username", use_context)
)]
#[validate(length(min = 1), custom(function = "unique_username", use_context))]
pub username: String,
#[validate(
email,
custom(function = "unique_email", use_context)
)]
#[validate(email, custom(function = "unique_email", use_context))]
pub email: String,
#[validate(length(min = 10), must_match(other = "password_repeat", ))]
#[validate(length(min = 10), must_match(other = "password_repeat",))]
pub password: String,
pub password_repeat: String,
}
@@ -86,13 +78,16 @@ impl From<Registration> for ValidatableRegistration {
username: value.username,
email: value.email,
password: value.password,
password_repeat: value.password_repeat
password_repeat: value.password_repeat,
}
}
}
#[get("/token")]
pub async fn login(pool: web::Data<DbPool>, login_data: web::Json<Login>) -> Result<impl Responder, ApiError> {
pub async fn login(
pool: web::Data<DbPool>,
login_data: web::Json<Login>,
) -> Result<impl Responder, ApiError> {
let data = login_data.into_inner();
if let Ok(Some(user)) = web::block(move || {
@@ -102,35 +97,45 @@ pub async fn login(pool: web::Data<DbPool>, login_data: web::Json<Login>) -> Res
.await?
{
let token = get_token(&user)?;
let response = Token{ jwt_token: Some(token) };
let response = Token {
jwt_token: Some(token),
};
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&response)?))
}
else {
Err(ApiError{status: 401, message: "User doesn't exist or password doesn't match".to_string()})
} else {
Err(ApiError {
status: 401,
message: "User doesn't exist or password doesn't match".to_string(),
})
}
}
#[post("/token")]
pub async fn refresh(user: AuthUser) -> Result<impl Responder, ApiError> {
let new_token = get_token(&user.0)?;
let response = Token{ jwt_token: Some(new_token) };
let response = Token {
jwt_token: Some(new_token),
};
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&response)?))
}
#[post("/user")]
pub async fn register(pool: web::Data<DbPool>, register_data: web::Json<Registration>) -> Result<impl Responder, ApiError> {
pub async fn register(
pool: web::Data<DbPool>,
register_data: web::Json<Registration>,
) -> Result<impl Responder, ApiError> {
web::block(move || -> Result<(), ApiError> {
let validatable_registration: ValidatableRegistration = register_data.clone().into();
validatable_registration.validate_with_args(&RegisterContext{pool: &pool})?;
validatable_registration.validate_with_args(&RegisterContext { pool: &pool })?;
let register_request = register_data.into_inner().into();
let mut conn = pool.get_conn();
gamenight_database::register(&mut conn, register_request)?;
Ok(())
}).await??;
})
.await??;
Ok(HttpResponse::Ok())
}
@@ -146,20 +151,42 @@ impl From<gamenight_database::user::User> for User {
}
#[get("/user")]
pub async fn get_user(pool: web::Data<DbPool>, _user: AuthUser, user_info: web::Json<UserId>) -> Result<impl Responder, ApiError> {
pub async fn get_user(
pool: web::Data<DbPool>,
_user: AuthUser,
user_info: web::Json<UserId>,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
let user = gamenight_database::user::get_user(&mut conn, Uuid::parse_str(&user_info.into_inner().user_id)?)?;
let user = gamenight_database::user::get_user(
&mut conn,
Uuid::parse_str(&user_info.into_inner().user_id)?,
)?;
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&user)?))
}
#[get("/user")]
pub async fn get_user_unauthenticated(_path: web::Path<UserId>) -> Result<impl Responder, ApiError> {
pub async fn get_user_unauthenticated(
_path: web::Path<UserId>,
) -> Result<impl Responder, ApiError> {
Ok(HttpResponse::Forbidden())
}
#[get("/users")]
pub async fn get_users(
pool: web::Data<DbPool>,
_user: AuthUser,
) -> Result<impl Responder, ApiError> {
let mut conn = pool.get_conn();
let users = gamenight_database::user::get_users(&mut conn)?;
let model: Vec<User> = users.into_iter().map(Into::into).collect();
Ok(HttpResponse::Ok()
.content_type(ContentType::json())
.body(serde_json::to_string(&model)?))
}