Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
518 changes: 284 additions & 234 deletions Cargo.lock

Large diffs are not rendered by default.

21 changes: 16 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ description = "Universal Auth System for Grass Development Team."
repository = "https://github.com/Grass-Development-Team/auth"
license = "Apache-2.0"
edition = "2024"
rust-version = "1.88"

[workspace.dependencies]
anyhow = "1"
Expand Down Expand Up @@ -37,14 +38,17 @@ regex = "1"
minijinja = "2"

sea-orm = { version = "1.1", features = [
"sqlx-all",
"runtime-tokio-native-tls",
"sqlx-postgres",
"runtime-tokio-rustls",
"macros",
"with-chrono",
"with-uuid",
] }
sea-orm-migration = "1.1"
redis = { version = "0.27.5", features = ["tokio-native-tls-comp"] }
sea-orm-migration = { version = "1.1", default-features = false, features = [
"runtime-tokio-rustls",
"sqlx-postgres",
] }
redis = { version = "0.27.5", features = ["tokio-rustls-comp"] }

sha2 = "0.10.9"
argon2 = "0.5"
Expand All @@ -56,7 +60,14 @@ subtle = "2.6"
jsonwebtoken = "9.3.1"
uuid = { version = "1.16.0", features = ["v4"] }

lettre = "0.11"
lettre = { version = "0.11", default-features = false, features = [
"builder",
"hostname",
"pool",
"smtp-transport",
"rustls-tls",
"rustls-platform-verifier",
] }

auth = { path = "auth" }
assets = { path = "crates/assets" }
Expand Down
2 changes: 1 addition & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ fmt-check:
cargo outdated -R

@msrv:
cargo msrv find
cargo msrv find --min 1.85

# Combined quality check
@quality: audit outdated msrv fmt-check clippy test
Expand Down
22 changes: 17 additions & 5 deletions auth/src/internal/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::{error::Error as StdError, fmt::Display};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AppErrorKind {
Undefined,
BadRequest,
Unauthorized,
Forbidden,
Expand All @@ -20,6 +21,7 @@ pub enum AppErrorKind {
UserDeleted,
DuplicatePassword,
VerificationEmailSendFailed,
TokenInvalid,
}

#[derive(Debug)]
Expand All @@ -31,21 +33,26 @@ pub struct AppError {
}

impl AppError {
pub fn new(kind: AppErrorKind, op: &'static str) -> Self {
pub fn new() -> Self {
Self {
kind,
op,
kind: AppErrorKind::Undefined,
op: "",
detail: None,
source: None,
}
}

pub fn biz(kind: AppErrorKind, op: &'static str) -> Self {
Self::new(kind, op)
Self::new().with_kind(kind).with_op(op)
}

pub fn infra(kind: AppErrorKind, op: &'static str, source: impl Into<anyhow::Error>) -> Self {
Self::new(kind, op).with_source(source)
Self::new().with_kind(kind).with_op(op).with_source(source)
}

pub fn with_kind(mut self, kind: AppErrorKind) -> Self {
self.kind = kind;
self
}

pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
Expand All @@ -58,6 +65,11 @@ impl AppError {
self
}

pub fn with_op(mut self, op: &'static str) -> Self {
self.op = op;
self
}

pub fn source_ref(&self) -> Option<&(dyn StdError + 'static)> {
self.source.as_ref().map(|err| err.as_ref())
}
Expand Down
20 changes: 20 additions & 0 deletions auth/src/models/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use crypto::password::PasswordError;
use sea_orm::DbErr;
use thiserror::Error;

use crate::internal::error::{AppError, AppErrorKind};

#[derive(Debug, Error)]
pub enum ModelError {
#[error("Database error: {0}")]
Expand All @@ -15,3 +17,21 @@ pub enum ModelError {
#[error("Model error: {0}")]
Custom(String),
}

impl From<ModelError> for AppError {
fn from(value: ModelError) -> Self {
match value {
ModelError::DBError(err) => AppError::new()
.with_kind(AppErrorKind::InternalError)
.with_source(err),
ModelError::PasswordError(err) => AppError::new()
.with_kind(AppErrorKind::ParamError)
.with_detail(err.to_string()),
ModelError::ParamsError => AppError::new().with_kind(AppErrorKind::ParamError),
ModelError::Empty => AppError::new().with_kind(AppErrorKind::NotFound),
ModelError::Custom(msg) => AppError::new()
.with_kind(AppErrorKind::InternalError)
.with_detail(msg),
}
}
}
12 changes: 12 additions & 0 deletions auth/src/models/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,18 @@ impl Model {

user.update(conn).await.map_err(ModelError::DBError)
}

pub async fn update_status(
&self,
conn: &impl ConnectionTrait,
status: AccountStatus,
) -> Result<Model, ModelError> {
let mut user = self.clone().into_active_model();

user.status = Set(status);

user.update(conn).await.map_err(ModelError::DBError)
}
}

impl AccountStatus {
Expand Down
18 changes: 16 additions & 2 deletions auth/src/routers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use tower_http::{cors, cors::CorsLayer};
use crate::{
internal::config::Config,
routers::{
controllers::{auth, common, users},
controllers::{actions, auth, common, users},
middleware::permission::PermissionAccess,
utils::content_type,
},
Expand Down Expand Up @@ -58,13 +58,24 @@ pub fn get_router(app: Router<AppState>, config: &Config) -> Router<AppState> {
Router::new().nest("/oauth", oauth)
};

let action = {
let route = Router::new().route("/verify-email", get(actions::verify_email));
let route = Router::new().nest("/actions", route);
if config.dev_mode {
route.layer(public_cors.clone())
} else {
route.layer(internal_cors.clone())
}
};

let api_v1 = {
// Auth
let auth = {
let route = Router::new()
.route("/login", post(auth::login))
.route("/logout", any(auth::logout))
.route("/register", post(auth::register))
.route("/verify-email", post(auth::verify_email))
.route("/forget-password", post(auth::forget_password))
.route("/reset-password", get(auth::reset_password))
.route(
Expand Down Expand Up @@ -142,7 +153,10 @@ pub fn get_router(app: Router<AppState>, config: &Config) -> Router<AppState> {
Router::new().nest("/api", route)
};

app.merge(api).merge(oauth).fallback(static_asset_fallback)
app.merge(api)
.merge(oauth)
.merge(action)
.fallback(static_asset_fallback)
}

async fn static_asset_fallback(request: Request) -> impl IntoResponse {
Expand Down
1 change: 1 addition & 0 deletions auth/src/routers/controllers.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod actions;
pub mod auth;
pub mod common;
pub mod users;
35 changes: 35 additions & 0 deletions auth/src/routers/controllers/actions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use axum::{
extract::{Query, State},
http::StatusCode,
response::{Html, IntoResponse, Response as AxumResponse},
};

use crate::{services::actions::ActionsVerifyEmailService, state::AppState};

pub async fn verify_email(
State(state): State<AppState>,
Query(req): Query<ActionsVerifyEmailService>,
) -> AxumResponse {
match req.render_verify_email_page(&state.config) {
Ok(html) => Html(html).into_response(),
Err(err) => {
let source = err.source_ref().map(ToString::to_string);
tracing::error!(
op = err.op,
kind = ?err.kind,
detail = ?err.detail,
source = ?source,
"failed to render verify-email action page"
);
(
StatusCode::INTERNAL_SERVER_ERROR,
Html(
"<!doctype html><html><head><meta charset=\"UTF-8\" /><title>Verification \
Error</title></head><body><p>Unable to load verification page. Please try \
again later.</p></body></html>",
),
)
.into_response()
},
}
}
50 changes: 49 additions & 1 deletion auth/src/routers/controllers/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use axum::{
http::StatusCode,
};
use axum_extra::extract::CookieJar;
use redis::aio::MultiplexedConnection;
use token::services::SessionService;

use crate::{
Expand All @@ -23,8 +24,31 @@ pub async fn register(
State(state): State<AppState>,
Json(req): Json<auth::RegisterService>,
) -> Response<String> {
let mut redis: Option<MultiplexedConnection> = if state.mail.is_some() {
match state.redis.get_multiplexed_tokio_connection().await {
Ok(redis) => Some(redis),
Err(err) => {
return app_error_to_response(
AppError::infra(
AppErrorKind::InternalError,
"auth.controller.register.redis",
err,
)
.with_detail("Unable to connect to redis"),
);
},
}
} else {
None
};

match req
.register(&state.db, &state.config, state.mail.as_deref())
.register(
&state.db,
&state.config,
state.mail.as_deref(),
redis.as_mut(),
)
.await
{
Ok(message) => Response::new(
Expand Down Expand Up @@ -225,3 +249,27 @@ pub async fn forget_password(
Err(err) => app_error_to_response(err),
}
}

pub async fn verify_email(
State(state): State<AppState>,
Json(req): Json<auth::VerifyEmailService>,
) -> Response {
let mut redis = match state.redis.get_multiplexed_tokio_connection().await {
Ok(redis) => redis,
Err(err) => {
return app_error_to_response(
AppError::infra(
AppErrorKind::InternalError,
"auth.controller.verify_email.redis",
err,
)
.with_detail("Unable to connect to redis"),
);
},
};

match req.verify_email(&state.db, &mut redis).await {
Ok(_) => Response::new(ResponseCode::OK.into(), ResponseCode::OK.into(), None),
Err(err) => app_error_to_response(err),
}
}
2 changes: 2 additions & 0 deletions auth/src/routers/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::{
impl From<AppErrorKind> for ResponseCode {
fn from(value: AppErrorKind) -> Self {
match value {
AppErrorKind::Undefined => ResponseCode::InternalError,
AppErrorKind::BadRequest => ResponseCode::BadRequest,
AppErrorKind::Unauthorized => ResponseCode::Unauthorized,
AppErrorKind::Forbidden => ResponseCode::Forbidden,
Expand All @@ -24,6 +25,7 @@ impl From<AppErrorKind> for ResponseCode {
AppErrorKind::UserDeleted => ResponseCode::UserDeleted,
AppErrorKind::DuplicatePassword => ResponseCode::DuplicatePassword,
AppErrorKind::VerificationEmailSendFailed => ResponseCode::VerificationEmailSendFailed,
AppErrorKind::TokenInvalid => ResponseCode::TokenInvalid,
}
}
}
Expand Down
Loading
Loading