chore: moved routes under dedicated mod

This commit is contained in:
əlemi 2023-01-22 16:06:42 +01:00
parent 2cf80d2eaf
commit 430342ac74
No known key found for this signature in database
GPG key ID: BBCBFE5D7244634E
4 changed files with 179 additions and 160 deletions

View file

@ -1,18 +1,19 @@
mod proto;
mod entities;
mod routes;
use std::{collections::HashMap, sync::Arc};
use chrono::{DateTime, Utc, Duration};
use chrono::{DateTime, Utc};
use clap::Parser;
use axum::{Router, routing::{get, post}, response::IntoResponse, Json, http::StatusCode, extract::{State, Query}};
use proto::{Error, AuthenticateRequest, AuthenticateResponse, JoinRequest, JoinResponse, ValidateRequest, RefreshRequest, RefreshResponse, RegisterRequest};
use sea_orm::{EntityTrait, QueryFilter, ColumnTrait, DatabaseConnection, Database, ActiveValue::NotSet, Set};
use axum::{Router, routing::{get, post}, response::IntoResponse, Json, http::StatusCode, extract::State};
use sea_orm::{EntityTrait, DatabaseConnection, Database, ActiveValue::NotSet, Set};
use tokio::sync::Mutex;
use uuid::Uuid;
use tracing::{info, warn};
use tracing::info;
use crate::proto::Property;
use crate::routes::auth::{authenticate, validate, refresh};
use crate::routes::session::{join, has_joined};
/// Reimplementation of legacy auth server for minecraft
#[derive(Parser, Debug, Clone)]
@ -31,7 +32,7 @@ struct ConfigArgs {
}
#[derive(Clone)]
struct JoinAttempt {
pub struct JoinAttempt {
server: String,
time: DateTime<Utc>,
}
@ -43,7 +44,7 @@ impl JoinAttempt {
}
#[derive(Clone)]
struct AppState {
pub struct AppState {
store: Arc<Mutex<HashMap<Uuid, JoinAttempt>>>,
db: DatabaseConnection,
cfg: ConfigArgs,
@ -82,7 +83,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
async fn fallback_route() -> impl IntoResponse {
let error = Error {
let error = proto::Error {
error: "No valid route specified".into(),
errorMessage: "No route requested or invalid route specified".into(),
cause: "Routing".into(),
@ -91,157 +92,7 @@ async fn fallback_route() -> impl IntoResponse {
(StatusCode::OK, Json(error))
}
async fn validate(State(state): State<AppState>, Json(payload): Json<ValidateRequest>) -> StatusCode {
let token = entities::token::Entity::find().filter(
entities::token::Column::AccessToken.eq(payload.accessToken)
).one(&state.db).await.unwrap();
if let Some(_t) = token {
StatusCode::NO_CONTENT
} else {
StatusCode::UNAUTHORIZED
}
}
async fn refresh(State(state): State<AppState>, Json(payload): Json<RefreshRequest>) -> Result<Json<RefreshResponse>, StatusCode> {
let token = entities::token::Entity::find().filter(
entities::token::Column::AccessToken.eq(payload.accessToken.clone())
).one(&state.db).await.unwrap();
if let Some(t) = token {
// TODO if user requests profile, fetch it and include it
let user = entities::user::Entity::find_by_id(t.user_id).one(&state.db).await.unwrap().unwrap();
entities::token::Entity::delete(
entities::token::ActiveModel{
id: NotSet,
access_token: Set(payload.accessToken.clone()),
created_at: NotSet,
user_id: NotSet,
}
).exec(&state.db).await.unwrap();
let new_access_token = Uuid::new_v4(); // TODO same as with authenticate
entities::token::Entity::insert(
entities::token::ActiveModel{
id: NotSet,
access_token: Set(payload.accessToken.clone()),
created_at: Set(Utc::now()),
user_id: Set(user.id),
}
).exec(&state.db).await.unwrap();
Ok(Json(
RefreshResponse {
accessToken: new_access_token.to_string(),
clientToken: "idc".into(),
selectedProfile: proto::Profile { id: user.uuid, name: user.name },
user: None,
}
))
} else {
Err(StatusCode::UNAUTHORIZED)
}
}
async fn authenticate(State(state): State<AppState>, Json(payload): Json<AuthenticateRequest>) -> Result<Json<AuthenticateResponse>, StatusCode> {
let user = entities::user::Entity::find().filter(
entities::user::Column::Name.eq(payload.username)
).one(&state.db).await.unwrap();
if let Some(u) = user {
if payload.password == u.password {
// make new token
let access_token = Uuid::new_v4().to_string(); // TODO maybe use a JWT?
entities::token::Entity::insert(entities::token::ActiveModel {
id: NotSet,
user_id: Set(u.id),
access_token: Set(access_token.clone()),
created_at: Set(Utc::now()),
}).exec(&state.db).await.unwrap();
let profile = proto::Profile {
name: u.name.clone(),
id: u.uuid,
};
Ok(Json(
AuthenticateResponse {
accessToken: access_token,
user: proto::User { id: u.uuid, username: u.name, properties: Some(vec![]) },
clientToken: Uuid::new_v4().to_string(),
availableProfiles: vec![profile.clone()],
selectedProfile: profile,
}
))
} else {
Err(StatusCode::UNAUTHORIZED)
}
} else {
Err(StatusCode::NOT_FOUND)
}
}
async fn join(State(state): State<AppState>, Json(payload): Json<JoinRequest>) -> StatusCode {
let user = entities::user::Entity::find().filter(
entities::user::Column::Uuid.eq(payload.selectedProfile.id)
).one(&state.db).await.unwrap().unwrap();
let tokens = entities::token::Entity::find().filter(
entities::token::Column::UserId.eq(user.id)
).all(&state.db).await.unwrap();
if tokens.iter().any(|x| x.access_token == payload.accessToken) {
state.store.lock().await.insert(payload.selectedProfile.id, JoinAttempt::new(payload.serverId.clone()));
info!(target: "JOIN", "user {} has joined server {}", payload.selectedProfile.name, payload.serverId);
StatusCode::OK
} else {
warn!(target: "JOIN", "user {} attempted to join server {} without a valid token", payload.selectedProfile.name, payload.serverId);
StatusCode::UNAUTHORIZED
}
}
async fn has_joined(State(state): State<AppState>, Query(query): Query<HashMap<String, String>>) -> Result<Json<JoinResponse>, StatusCode> {
let username = query.get("username").unwrap().clone();
let server_id = query.get("serverId").unwrap();
let user = entities::user::Entity::find().filter(
entities::user::Column::Name.eq(username.clone())
).one(&state.db).await.unwrap();
match user {
Some(user) => {
match state.store.lock().await.get(&user.uuid) {
Some(join) => {
if Utc::now() - join.time < Duration::seconds(state.cfg.time_window as i64)
&& join.server.to_lowercase() == server_id.to_lowercase() {
let response = JoinResponse {
id: user.uuid,
name: username.clone(),
properties: vec![
Property {
name: "textures".into(),
value: "".into(),
signature: Some("".into()),
}
],
};
info!(target: "hasJOINED", "server found user -> {:?}", response);
Ok(Json(response))
} else {
warn!(target: "hasJOINED", "server found user but join was late or for another server");
Err(StatusCode::NOT_FOUND)
}
},
None => {
warn!(target: "hasJOINED", "server didn't find user");
Err(StatusCode::NOT_FOUND)
},
}
},
None => {
warn!(target: "hasJOINED", "invalid UUID");
Err(StatusCode::NOT_FOUND)
},
}
}
async fn register_tmp(State(state): State<AppState>, Json(payload): Json<RegisterRequest>) -> StatusCode {
async fn register_tmp(State(state): State<AppState>, Json(payload): Json<proto::RegisterRequest>) -> StatusCode {
let user = entities::user::ActiveModel {
id: NotSet,
name: Set(payload.user),

93
src/routes/auth.rs Normal file
View file

@ -0,0 +1,93 @@
use axum::{extract::State, Json, http::StatusCode};
use chrono::Utc;
use sea_orm::{EntityTrait, QueryFilter, ColumnTrait, ActiveValue::NotSet, Set};
use uuid::Uuid;
use crate::{entities, AppState, proto};
pub async fn validate(State(state): State<AppState>, Json(payload): Json<proto::ValidateRequest>) -> StatusCode {
let token = entities::token::Entity::find().filter(
entities::token::Column::AccessToken.eq(payload.accessToken)
).one(&state.db).await.unwrap();
if let Some(_t) = token {
StatusCode::NO_CONTENT
} else {
StatusCode::UNAUTHORIZED
}
}
pub async fn refresh(State(state): State<AppState>, Json(payload): Json<proto::RefreshRequest>) -> Result<Json<proto::RefreshResponse>, StatusCode> {
let token = entities::token::Entity::find().filter(
entities::token::Column::AccessToken.eq(payload.accessToken.clone())
).one(&state.db).await.unwrap();
if let Some(t) = token {
// TODO if user requests profile, fetch it and include it
let user = entities::user::Entity::find_by_id(t.user_id).one(&state.db).await.unwrap().unwrap();
entities::token::Entity::delete(
entities::token::ActiveModel{
id: NotSet,
access_token: Set(payload.accessToken.clone()),
created_at: NotSet,
user_id: NotSet,
}
).exec(&state.db).await.unwrap();
let new_access_token = Uuid::new_v4(); // TODO same as with authenticate
entities::token::Entity::insert(
entities::token::ActiveModel{
id: NotSet,
access_token: Set(payload.accessToken.clone()),
created_at: Set(Utc::now()),
user_id: Set(user.id),
}
).exec(&state.db).await.unwrap();
Ok(Json(
proto::RefreshResponse {
accessToken: new_access_token.to_string(),
clientToken: "idc".into(),
selectedProfile: proto::Profile { id: user.uuid, name: user.name },
user: None,
}
))
} else {
Err(StatusCode::UNAUTHORIZED)
}
}
pub async fn authenticate(State(state): State<AppState>, Json(payload): Json<proto::AuthenticateRequest>) -> Result<Json<proto::AuthenticateResponse>, StatusCode> {
let user = entities::user::Entity::find().filter(
entities::user::Column::Name.eq(payload.username)
).one(&state.db).await.unwrap();
if let Some(u) = user {
if payload.password == u.password {
// make new token
let access_token = Uuid::new_v4().to_string(); // TODO maybe use a JWT?
entities::token::Entity::insert(entities::token::ActiveModel {
id: NotSet,
user_id: Set(u.id),
access_token: Set(access_token.clone()),
created_at: Set(Utc::now()),
}).exec(&state.db).await.unwrap();
let profile = proto::Profile {
name: u.name.clone(),
id: u.uuid,
};
Ok(Json(
proto::AuthenticateResponse {
accessToken: access_token,
user: proto::User { id: u.uuid, username: u.name, properties: Some(vec![]) },
clientToken: Uuid::new_v4().to_string(),
availableProfiles: vec![profile.clone()],
selectedProfile: profile,
}
))
} else {
Err(StatusCode::UNAUTHORIZED)
}
} else {
Err(StatusCode::NOT_FOUND)
}
}

2
src/routes/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod auth;
pub mod session;

73
src/routes/session.rs Normal file
View file

@ -0,0 +1,73 @@
use std::collections::HashMap;
use axum::{extract::{State, Query}, http::StatusCode, Json};
use chrono::{Duration, Utc};
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
use tracing::{info, warn};
use crate::{AppState, proto, JoinAttempt, entities};
pub async fn join(State(state): State<AppState>, Json(payload): Json<proto::JoinRequest>) -> StatusCode {
let user = entities::user::Entity::find().filter(
entities::user::Column::Uuid.eq(payload.selectedProfile.id)
).one(&state.db).await.unwrap().unwrap();
let tokens = entities::token::Entity::find().filter(
entities::token::Column::UserId.eq(user.id)
).all(&state.db).await.unwrap();
if tokens.iter().any(|x| x.access_token == payload.accessToken) {
state.store.lock().await.insert(payload.selectedProfile.id, JoinAttempt::new(payload.serverId.clone()));
info!(target: "JOIN", "user {} has joined server {}", payload.selectedProfile.name, payload.serverId);
StatusCode::OK
} else {
warn!(target: "JOIN", "user {} attempted to join server {} without a valid token", payload.selectedProfile.name, payload.serverId);
StatusCode::UNAUTHORIZED
}
}
pub async fn has_joined(State(state): State<AppState>, Query(query): Query<HashMap<String, String>>) -> Result<Json<proto::JoinResponse>, StatusCode> {
let username = query.get("username").unwrap().clone();
let server_id = query.get("serverId").unwrap();
let user = entities::user::Entity::find().filter(
entities::user::Column::Name.eq(username.clone())
).one(&state.db).await.unwrap();
match user {
Some(user) => {
match state.store.lock().await.get(&user.uuid) {
Some(join) => {
if Utc::now() - join.time < Duration::seconds(state.cfg.time_window as i64)
&& join.server.to_lowercase() == server_id.to_lowercase() {
let response = proto::JoinResponse {
id: user.uuid,
name: username.clone(),
properties: vec![
proto::Property {
name: "textures".into(),
value: "".into(),
signature: Some("".into()),
}
],
};
info!(target: "hasJOINED", "server found user -> {:?}", response);
Ok(Json(response))
} else {
warn!(target: "hasJOINED", "server found user but join was late or for another server");
Err(StatusCode::NOT_FOUND)
}
},
None => {
warn!(target: "hasJOINED", "server didn't find user");
Err(StatusCode::NOT_FOUND)
},
}
},
None => {
warn!(target: "hasJOINED", "invalid UUID");
Err(StatusCode::NOT_FOUND)
},
}
}