login
This commit is contained in:
1
backend/Cargo.lock
generated
1
backend/Cargo.lock
generated
@@ -2521,6 +2521,7 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
|
"hex",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ axum_typed_multipart = "0.16.3"
|
|||||||
chrono = "0.4.41"
|
chrono = "0.4.41"
|
||||||
derive_more = { version = "2.0.1", features = ["display"] }
|
derive_more = { version = "2.0.1", features = ["display"] }
|
||||||
dotenv = "0.15.0"
|
dotenv = "0.15.0"
|
||||||
|
hex = "0.4.3"
|
||||||
mime_guess = "2.0.5"
|
mime_guess = "2.0.5"
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
CREATE TABLE auth_sessions (
|
||||||
|
session_id VARCHAR NOT NULL PRIMARY KEY,
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
expires_at TIMESTAMP NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
@@ -39,7 +39,8 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
fs_service.clone(),
|
fs_service.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let auth_service = domain::warren::service::auth::Service::new(postgres, metrics, notifier);
|
let auth_service =
|
||||||
|
domain::warren::service::auth::Service::new(postgres, metrics, notifier, config.auth);
|
||||||
|
|
||||||
let server_config = HttpServerConfig::new(
|
let server_config = HttpServerConfig::new(
|
||||||
&config.server_address,
|
&config.server_address,
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ use std::{env, str::FromStr};
|
|||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
use tracing::level_filters::LevelFilter;
|
use tracing::level_filters::LevelFilter;
|
||||||
|
|
||||||
|
use crate::domain::warren::service::auth::AuthConfig;
|
||||||
|
|
||||||
const DATABASE_URL_KEY: &str = "DATABASE_URL";
|
const DATABASE_URL_KEY: &str = "DATABASE_URL";
|
||||||
const DATABASE_NAME_KEY: &str = "DATABASE_NAME";
|
const DATABASE_NAME_KEY: &str = "DATABASE_NAME";
|
||||||
|
|
||||||
@@ -29,24 +31,28 @@ pub struct Config {
|
|||||||
pub database_name: String,
|
pub database_name: String,
|
||||||
|
|
||||||
pub log_level: LevelFilter,
|
pub log_level: LevelFilter,
|
||||||
|
|
||||||
|
pub auth: AuthConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn from_env() -> anyhow::Result<Self> {
|
pub fn from_env() -> anyhow::Result<Self> {
|
||||||
let server_address = load_env(SERVER_ADDRESS_KEY)?;
|
let server_address = Self::load_env(SERVER_ADDRESS_KEY)?;
|
||||||
let server_port = load_env(SERVER_PORT_KEY)?.parse()?;
|
let server_port = Self::load_env(SERVER_PORT_KEY)?.parse()?;
|
||||||
let cors_allow_origin = load_env(CORS_ALLOW_ORIGIN_KEY)?;
|
let cors_allow_origin = Self::load_env(CORS_ALLOW_ORIGIN_KEY)?;
|
||||||
|
|
||||||
let serve_dir = load_env(SERVE_DIRECTORY_KEY)?;
|
let serve_dir = Self::load_env(SERVE_DIRECTORY_KEY)?;
|
||||||
let static_frontend_dir = load_env(STATIC_FRONTEND_DIRECTORY).ok();
|
let static_frontend_dir = Self::load_env(STATIC_FRONTEND_DIRECTORY).ok();
|
||||||
|
|
||||||
let database_url = load_env(DATABASE_URL_KEY)?;
|
let database_url = Self::load_env(DATABASE_URL_KEY)?;
|
||||||
let database_name = load_env(DATABASE_NAME_KEY)?;
|
let database_name = Self::load_env(DATABASE_NAME_KEY)?;
|
||||||
|
|
||||||
let log_level =
|
let log_level =
|
||||||
LevelFilter::from_str(&load_env(LOG_LEVEL_KEY).unwrap_or("INFO".to_string()))
|
LevelFilter::from_str(&Self::load_env(LOG_LEVEL_KEY).unwrap_or("INFO".to_string()))
|
||||||
.context("Failed to convert the value of LOG_LEVEL to a LevelFilter")?;
|
.context("Failed to convert the value of LOG_LEVEL to a LevelFilter")?;
|
||||||
|
|
||||||
|
let auth_config = AuthConfig::from_env()?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
server_address,
|
server_address,
|
||||||
server_port,
|
server_port,
|
||||||
@@ -59,10 +65,12 @@ impl Config {
|
|||||||
database_name,
|
database_name,
|
||||||
|
|
||||||
log_level,
|
log_level,
|
||||||
|
|
||||||
|
auth: auth_config,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn load_env(key: &str) -> anyhow::Result<String> {
|
pub fn load_env(key: &str) -> anyhow::Result<String> {
|
||||||
env::var(key).context(format!("Failed to get environment variable: {key}"))
|
env::var(key).context(format!("Failed to get environment variable: {key}"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
65
backend/src/lib/domain/warren/models/auth_session/mod.rs
Normal file
65
backend/src/lib/domain/warren/models/auth_session/mod.rs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use derive_more::Display;
|
||||||
|
use thiserror::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub mod requests;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::FromRow)]
|
||||||
|
pub struct AuthSession {
|
||||||
|
session_id: AuthSessionId,
|
||||||
|
user_id: Uuid,
|
||||||
|
expires_at: NaiveDateTime,
|
||||||
|
created_at: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthSession {
|
||||||
|
pub fn new(
|
||||||
|
session_id: AuthSessionId,
|
||||||
|
user_id: Uuid,
|
||||||
|
expires_at: NaiveDateTime,
|
||||||
|
created_at: NaiveDateTime,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
session_id,
|
||||||
|
user_id,
|
||||||
|
expires_at,
|
||||||
|
created_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn session_id(&self) -> &AuthSessionId {
|
||||||
|
&self.session_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn user_id(&self) -> &Uuid {
|
||||||
|
&self.user_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A valid auth session id
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Display, sqlx::Type)]
|
||||||
|
#[sqlx(transparent)]
|
||||||
|
pub struct AuthSessionId(String);
|
||||||
|
|
||||||
|
impl AuthSessionId {
|
||||||
|
pub fn new(raw: &str) -> Result<Self, AuthSessionIdError> {
|
||||||
|
let trimmed = raw.trim();
|
||||||
|
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
return Err(AuthSessionIdError::Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self(trimmed.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_string(self) -> String {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Error)]
|
||||||
|
pub enum AuthSessionIdError {
|
||||||
|
#[error("An auth session id must not be empty")]
|
||||||
|
Empty,
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::domain::warren::models::user::User;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct CreateAuthSessionRequest {
|
||||||
|
user: User,
|
||||||
|
expiration: SessionExpirationTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CreateAuthSessionRequest {
|
||||||
|
pub fn new(user: User, expiration_time: SessionExpirationTime) -> Self {
|
||||||
|
Self {
|
||||||
|
user,
|
||||||
|
expiration: expiration_time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn user(&self) -> &User {
|
||||||
|
&self.user
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expiration(&self) -> &SessionExpirationTime {
|
||||||
|
&self.expiration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum CreateAuthSessionError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct SessionExpirationTime(u64);
|
||||||
|
|
||||||
|
impl SessionExpirationTime {
|
||||||
|
pub fn new(value: u64) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn millis(&self) -> u64 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod auth_session;
|
||||||
pub mod file;
|
pub mod file;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
pub mod warren;
|
pub mod warren;
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ impl User {
|
|||||||
&self.email
|
&self.email
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn password_hash(&self) -> &str {
|
||||||
|
&self.hash
|
||||||
|
}
|
||||||
|
|
||||||
pub fn admin(&self) -> bool {
|
pub fn admin(&self) -> bool {
|
||||||
self.admin
|
self.admin
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::domain::warren::models::auth_session::requests::CreateAuthSessionError;
|
||||||
|
|
||||||
use super::{UserEmail, UserName};
|
use super::{UserEmail, UserName};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
@@ -87,3 +89,72 @@ impl RegisterUserRequest {
|
|||||||
&self.password
|
&self.password
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct VerifyUserPasswordRequest {
|
||||||
|
email: UserEmail,
|
||||||
|
password: UserPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VerifyUserPasswordRequest {
|
||||||
|
pub fn new(email: UserEmail, password: UserPassword) -> Self {
|
||||||
|
Self { email, password }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn email(&self) -> &UserEmail {
|
||||||
|
&self.email
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn password(&self) -> &UserPassword {
|
||||||
|
&self.password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LoginUserRequest> for VerifyUserPasswordRequest {
|
||||||
|
fn from(value: LoginUserRequest) -> Self {
|
||||||
|
Self {
|
||||||
|
email: value.email,
|
||||||
|
password: value.password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct LoginUserRequest {
|
||||||
|
email: UserEmail,
|
||||||
|
password: UserPassword,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoginUserRequest {
|
||||||
|
pub fn new(email: UserEmail, password: UserPassword) -> Self {
|
||||||
|
Self { email, password }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn email(&self) -> &UserEmail {
|
||||||
|
&self.email
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn password(&self) -> &UserPassword {
|
||||||
|
&self.password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum VerifyUserPasswordError {
|
||||||
|
#[error("There is no user with this email: {0}")]
|
||||||
|
NotFound(UserEmail),
|
||||||
|
#[error("The password is incorrect")]
|
||||||
|
IncorrectPassword,
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum LoginUserError {
|
||||||
|
#[error(transparent)]
|
||||||
|
VerifyUser(#[from] VerifyUserPasswordError),
|
||||||
|
#[error(transparent)]
|
||||||
|
CreateAuthToken(#[from] CreateAuthSessionError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|||||||
@@ -52,4 +52,10 @@ pub trait FileSystemMetrics: Clone + Send + Sync + 'static {
|
|||||||
pub trait AuthMetrics: Clone + Send + Sync + 'static {
|
pub trait AuthMetrics: Clone + Send + Sync + 'static {
|
||||||
fn record_user_registration_success(&self) -> impl Future<Output = ()> + Send;
|
fn record_user_registration_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
fn record_user_registration_failure(&self) -> impl Future<Output = ()> + Send;
|
fn record_user_registration_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
|
fn record_user_login_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
fn record_user_login_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
|
fn record_auth_session_creation_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
fn record_auth_session_creation_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,16 @@ pub use notifier::*;
|
|||||||
pub use repository::*;
|
pub use repository::*;
|
||||||
|
|
||||||
use super::models::{
|
use super::models::{
|
||||||
|
auth_session::{
|
||||||
|
AuthSession,
|
||||||
|
requests::{CreateAuthSessionError, CreateAuthSessionRequest},
|
||||||
|
},
|
||||||
file::{
|
file::{
|
||||||
CreateDirectoryError, CreateDirectoryRequest, CreateFileError, CreateFileRequest,
|
CreateDirectoryError, CreateDirectoryRequest, CreateFileError, CreateFileRequest,
|
||||||
DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError, DeleteFileRequest, File,
|
DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError, DeleteFileRequest, File,
|
||||||
FilePath, ListFilesError, ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
FilePath, ListFilesError, ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
||||||
},
|
},
|
||||||
user::{RegisterUserError, RegisterUserRequest, User},
|
user::{LoginUserError, LoginUserRequest, RegisterUserError, RegisterUserRequest, User},
|
||||||
warren::{
|
warren::{
|
||||||
CreateWarrenDirectoryError, CreateWarrenDirectoryRequest, DeleteWarrenDirectoryError,
|
CreateWarrenDirectoryError, CreateWarrenDirectoryRequest, DeleteWarrenDirectoryError,
|
||||||
DeleteWarrenDirectoryRequest, DeleteWarrenFileError, DeleteWarrenFileRequest,
|
DeleteWarrenDirectoryRequest, DeleteWarrenFileError, DeleteWarrenFileRequest,
|
||||||
@@ -98,4 +102,12 @@ pub trait AuthService: Clone + Send + Sync + 'static {
|
|||||||
&self,
|
&self,
|
||||||
request: RegisterUserRequest,
|
request: RegisterUserRequest,
|
||||||
) -> impl Future<Output = Result<User, RegisterUserError>> + Send;
|
) -> impl Future<Output = Result<User, RegisterUserError>> + Send;
|
||||||
|
fn login_user(
|
||||||
|
&self,
|
||||||
|
request: LoginUserRequest,
|
||||||
|
) -> impl Future<Output = Result<AuthSession, LoginUserError>> + Send;
|
||||||
|
fn create_auth_session(
|
||||||
|
&self,
|
||||||
|
request: CreateAuthSessionRequest,
|
||||||
|
) -> impl Future<Output = Result<AuthSession, CreateAuthSessionError>> + Send;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::domain::warren::models::{
|
use crate::domain::warren::models::{
|
||||||
file::{AbsoluteFilePath, File, FilePath},
|
file::{AbsoluteFilePath, File, FilePath},
|
||||||
user::User,
|
user::User,
|
||||||
@@ -72,4 +74,6 @@ pub trait FileSystemNotifier: Clone + Send + Sync + 'static {
|
|||||||
|
|
||||||
pub trait AuthNotifier: Clone + Send + Sync + 'static {
|
pub trait AuthNotifier: Clone + Send + Sync + 'static {
|
||||||
fn user_registered(&self, user: &User) -> impl Future<Output = ()> + Send;
|
fn user_registered(&self, user: &User) -> impl Future<Output = ()> + Send;
|
||||||
|
fn user_logged_in(&self, user: &User) -> impl Future<Output = ()> + Send;
|
||||||
|
fn auth_session_created(&self, user_id: &Uuid) -> impl Future<Output = ()> + Send;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
use crate::domain::warren::models::{
|
use crate::domain::warren::models::{
|
||||||
|
auth_session::{
|
||||||
|
AuthSession,
|
||||||
|
requests::{CreateAuthSessionError, CreateAuthSessionRequest},
|
||||||
|
},
|
||||||
file::{
|
file::{
|
||||||
CreateDirectoryError, CreateDirectoryRequest, CreateFileError, CreateFileRequest,
|
CreateDirectoryError, CreateDirectoryRequest, CreateFileError, CreateFileRequest,
|
||||||
DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError, DeleteFileRequest, File,
|
DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError, DeleteFileRequest, File,
|
||||||
FilePath, ListFilesError, ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
FilePath, ListFilesError, ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
||||||
},
|
},
|
||||||
user::{RegisterUserError, RegisterUserRequest, User},
|
user::{
|
||||||
|
RegisterUserError, RegisterUserRequest, User, VerifyUserPasswordError,
|
||||||
|
VerifyUserPasswordRequest,
|
||||||
|
},
|
||||||
warren::{FetchWarrenError, FetchWarrenRequest, ListWarrensError, ListWarrensRequest, Warren},
|
warren::{FetchWarrenError, FetchWarrenRequest, ListWarrensError, ListWarrensRequest, Warren},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -55,4 +62,12 @@ pub trait AuthRepository: Clone + Send + Sync + 'static {
|
|||||||
&self,
|
&self,
|
||||||
request: RegisterUserRequest,
|
request: RegisterUserRequest,
|
||||||
) -> impl Future<Output = Result<User, RegisterUserError>> + Send;
|
) -> impl Future<Output = Result<User, RegisterUserError>> + Send;
|
||||||
|
fn verify_user_password(
|
||||||
|
&self,
|
||||||
|
request: VerifyUserPasswordRequest,
|
||||||
|
) -> impl Future<Output = Result<User, VerifyUserPasswordError>> + Send;
|
||||||
|
fn create_auth_session(
|
||||||
|
&self,
|
||||||
|
request: CreateAuthSessionRequest,
|
||||||
|
) -> impl Future<Output = Result<AuthSession, CreateAuthSessionError>> + Send;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,49 @@
|
|||||||
use crate::domain::warren::{
|
use crate::{
|
||||||
models::user::{RegisterUserError, RegisterUserRequest, User},
|
config::Config,
|
||||||
ports::{AuthMetrics, AuthNotifier, AuthRepository, AuthService},
|
domain::warren::{
|
||||||
|
models::{
|
||||||
|
auth_session::{
|
||||||
|
AuthSession,
|
||||||
|
requests::{
|
||||||
|
CreateAuthSessionError, CreateAuthSessionRequest, SessionExpirationTime,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user::{
|
||||||
|
LoginUserError, LoginUserRequest, RegisterUserError, RegisterUserRequest, User,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ports::{AuthMetrics, AuthNotifier, AuthRepository, AuthService},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const AUTH_SESSION_EXPIRATION_KEY: &str = "AUTH_SESSION_EXPIRATION";
|
||||||
|
|
||||||
|
/// The authentication service configuration
|
||||||
|
///
|
||||||
|
/// * `session_lifetime`: The amount of milliseconds a client session is valid
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct AuthConfig {
|
||||||
|
session_lifetime: SessionExpirationTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthConfig {
|
||||||
|
pub fn from_env() -> anyhow::Result<Self> {
|
||||||
|
let session_lifetime = {
|
||||||
|
match Config::load_env(AUTH_SESSION_EXPIRATION_KEY).ok() {
|
||||||
|
Some(raw) => SessionExpirationTime::new(raw.parse()?),
|
||||||
|
// 86.400.000 milliseconds = 1 day
|
||||||
|
None => SessionExpirationTime::new(86_400_000),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self { session_lifetime })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn session_lifetime(&self) -> SessionExpirationTime {
|
||||||
|
self.session_lifetime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Service<R, M, N>
|
pub struct Service<R, M, N>
|
||||||
where
|
where
|
||||||
@@ -13,6 +54,7 @@ where
|
|||||||
repository: R,
|
repository: R,
|
||||||
metrics: M,
|
metrics: M,
|
||||||
notifier: N,
|
notifier: N,
|
||||||
|
config: AuthConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R, M, N> Service<R, M, N>
|
impl<R, M, N> Service<R, M, N>
|
||||||
@@ -21,11 +63,12 @@ where
|
|||||||
M: AuthMetrics,
|
M: AuthMetrics,
|
||||||
N: AuthNotifier,
|
N: AuthNotifier,
|
||||||
{
|
{
|
||||||
pub fn new(repository: R, metrics: M, notifier: N) -> Self {
|
pub fn new(repository: R, metrics: M, notifier: N, config: AuthConfig) -> Self {
|
||||||
Self {
|
Self {
|
||||||
repository,
|
repository,
|
||||||
metrics,
|
metrics,
|
||||||
notifier,
|
notifier,
|
||||||
|
config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,4 +91,44 @@ where
|
|||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn login_user(&self, request: LoginUserRequest) -> Result<AuthSession, LoginUserError> {
|
||||||
|
let user = self
|
||||||
|
.repository
|
||||||
|
.verify_user_password(request.into())
|
||||||
|
.await
|
||||||
|
.map_err(|e| LoginUserError::VerifyUser(e))?;
|
||||||
|
|
||||||
|
let result = self
|
||||||
|
.create_auth_session(CreateAuthSessionRequest::new(
|
||||||
|
user.clone(),
|
||||||
|
self.config.session_lifetime(),
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if result.as_ref().is_ok() {
|
||||||
|
self.metrics.record_user_login_success().await;
|
||||||
|
self.notifier.user_logged_in(&user).await;
|
||||||
|
} else {
|
||||||
|
self.metrics.record_user_login_failure().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_auth_session(
|
||||||
|
&self,
|
||||||
|
request: CreateAuthSessionRequest,
|
||||||
|
) -> Result<AuthSession, CreateAuthSessionError> {
|
||||||
|
let result = self.repository.create_auth_session(request).await;
|
||||||
|
|
||||||
|
if let Ok(session) = result.as_ref() {
|
||||||
|
self.metrics.record_auth_session_creation_success().await;
|
||||||
|
self.notifier.auth_session_created(session.user_id()).await;
|
||||||
|
} else {
|
||||||
|
self.metrics.record_auth_session_creation_failure().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
domain::warren::models::{
|
domain::warren::models::{
|
||||||
file::{CreateDirectoryError, DeleteDirectoryError, DeleteFileError, ListFilesError},
|
file::{CreateDirectoryError, DeleteDirectoryError, DeleteFileError, ListFilesError},
|
||||||
user::RegisterUserError,
|
user::{LoginUserError, RegisterUserError, VerifyUserPasswordError},
|
||||||
warren::{
|
warren::{
|
||||||
CreateWarrenDirectoryError, DeleteWarrenDirectoryError, DeleteWarrenFileError,
|
CreateWarrenDirectoryError, DeleteWarrenDirectoryError, DeleteWarrenFileError,
|
||||||
FetchWarrenError, ListWarrenFilesError, ListWarrensError, RenameWarrenEntryError,
|
FetchWarrenError, ListWarrenFilesError, ListWarrensError, RenameWarrenEntryError,
|
||||||
@@ -134,3 +134,21 @@ impl From<RegisterUserError> for ApiError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<LoginUserError> for ApiError {
|
||||||
|
fn from(value: LoginUserError) -> Self {
|
||||||
|
match value {
|
||||||
|
LoginUserError::VerifyUser(e) => match e {
|
||||||
|
VerifyUserPasswordError::NotFound(_) => {
|
||||||
|
Self::NotFound("Could not find a user with that email".to_string())
|
||||||
|
}
|
||||||
|
VerifyUserPasswordError::IncorrectPassword => {
|
||||||
|
Self::BadRequest("The email and password do not match".to_string())
|
||||||
|
}
|
||||||
|
VerifyUserPasswordError::Unknown(e) => Self::InternalServerError(e.to_string()),
|
||||||
|
},
|
||||||
|
LoginUserError::CreateAuthToken(e) => Self::InternalServerError(e.to_string()),
|
||||||
|
LoginUserError::Unknown(e) => Self::InternalServerError(e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
94
backend/src/lib/inbound/http/handlers/auth/login.rs
Normal file
94
backend/src/lib/inbound/http/handlers/auth/login.rs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
use axum::{Json, extract::State, http::StatusCode};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
domain::warren::{
|
||||||
|
models::{
|
||||||
|
auth_session::AuthSession,
|
||||||
|
user::{LoginUserRequest, UserEmail, UserEmailError, UserPassword, UserPasswordError},
|
||||||
|
},
|
||||||
|
ports::{AuthService, WarrenService},
|
||||||
|
},
|
||||||
|
inbound::http::{
|
||||||
|
AppState,
|
||||||
|
responses::{ApiError, ApiSuccess},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct LoginUserHttpRequestBody {
|
||||||
|
email: String,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Error)]
|
||||||
|
pub enum ParseLoginUserHttpRequestError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Email(#[from] UserEmailError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Password(#[from] UserPasswordError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ParseLoginUserHttpRequestError> for ApiError {
|
||||||
|
fn from(value: ParseLoginUserHttpRequestError) -> Self {
|
||||||
|
match value {
|
||||||
|
ParseLoginUserHttpRequestError::Email(err) => Self::BadRequest(
|
||||||
|
match err {
|
||||||
|
UserEmailError::Empty => "The provided email is empty",
|
||||||
|
UserEmailError::Invalid => "The provided email is invalid",
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
ParseLoginUserHttpRequestError::Password(err) => Self::BadRequest(
|
||||||
|
match err {
|
||||||
|
UserPasswordError::Empty => "The provided password is empty",
|
||||||
|
// Best not give a potential bad actor any hints since this is the login and
|
||||||
|
// not the registration
|
||||||
|
UserPasswordError::LeadingWhitespace
|
||||||
|
| UserPasswordError::TrailingWhitespace
|
||||||
|
| UserPasswordError::TooShort
|
||||||
|
| UserPasswordError::TooLong => "",
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoginUserHttpRequestBody {
|
||||||
|
fn try_into_domain(self) -> Result<LoginUserRequest, ParseLoginUserHttpRequestError> {
|
||||||
|
let email = UserEmail::new(&self.email)?;
|
||||||
|
let password = UserPassword::new(&self.password)?;
|
||||||
|
|
||||||
|
Ok(LoginUserRequest::new(email, password))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||||
|
pub struct LoginResponseBody {
|
||||||
|
token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AuthSession> for LoginResponseBody {
|
||||||
|
fn from(value: AuthSession) -> Self {
|
||||||
|
Self {
|
||||||
|
token: value.session_id().to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login<WS: WarrenService, AS: AuthService>(
|
||||||
|
State(state): State<AppState<WS, AS>>,
|
||||||
|
Json(request): Json<LoginUserHttpRequestBody>,
|
||||||
|
) -> Result<ApiSuccess<LoginResponseBody>, ApiError> {
|
||||||
|
let domain_request = request.try_into_domain()?;
|
||||||
|
|
||||||
|
state
|
||||||
|
.auth_service
|
||||||
|
.login_user(domain_request)
|
||||||
|
.await
|
||||||
|
.map(|token| ApiSuccess::new(StatusCode::OK, token.into()))
|
||||||
|
.map_err(ApiError::from)
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
|
mod login;
|
||||||
mod register;
|
mod register;
|
||||||
|
use login::login;
|
||||||
use register::register;
|
use register::register;
|
||||||
|
|
||||||
use axum::{Router, routing::post};
|
use axum::{Router, routing::post};
|
||||||
@@ -9,5 +11,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub fn routes<WS: WarrenService, AS: AuthService>() -> Router<AppState<WS, AS>> {
|
pub fn routes<WS: WarrenService, AS: AuthService>() -> Router<AppState<WS, AS>> {
|
||||||
Router::new().route("/register", post(register))
|
Router::new()
|
||||||
|
.route("/register", post(register))
|
||||||
|
.route("/login", post(login))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,10 +115,10 @@ impl FileSystemMetrics for MetricsDebugLogger {
|
|||||||
tracing::debug!("[Metrics] File deletion failed");
|
tracing::debug!("[Metrics] File deletion failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn record_entry_rename_success(&self) -> () {
|
async fn record_entry_rename_success(&self) {
|
||||||
tracing::debug!("[Metrics] Entry rename succeeded");
|
tracing::debug!("[Metrics] Entry rename succeeded");
|
||||||
}
|
}
|
||||||
async fn record_entry_rename_failure(&self) -> () {
|
async fn record_entry_rename_failure(&self) {
|
||||||
tracing::debug!("[Metrics] Entry rename failed");
|
tracing::debug!("[Metrics] Entry rename failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,4 +130,18 @@ impl AuthMetrics for MetricsDebugLogger {
|
|||||||
async fn record_user_registration_failure(&self) {
|
async fn record_user_registration_failure(&self) {
|
||||||
tracing::debug!("[Metrics] User registration failed");
|
tracing::debug!("[Metrics] User registration failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn record_user_login_success(&self) {
|
||||||
|
tracing::debug!("[Metrics] User login succeeded");
|
||||||
|
}
|
||||||
|
async fn record_user_login_failure(&self) {
|
||||||
|
tracing::debug!("[Metrics] User login failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn record_auth_session_creation_success(&self) {
|
||||||
|
tracing::debug!("[Metrics] Auth session creation succeeded");
|
||||||
|
}
|
||||||
|
async fn record_auth_session_creation_failure(&self) {
|
||||||
|
tracing::debug!("[Metrics] Auth session creation failed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::domain::warren::{
|
use crate::domain::warren::{
|
||||||
models::{
|
models::{
|
||||||
file::{File, FilePath},
|
file::{File, FilePath},
|
||||||
@@ -118,4 +120,12 @@ impl AuthNotifier for NotifierDebugLogger {
|
|||||||
async fn user_registered(&self, user: &User) {
|
async fn user_registered(&self, user: &User) {
|
||||||
tracing::debug!("[Notifier] Registered user {}", user.name());
|
tracing::debug!("[Notifier] Registered user {}", user.name());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn user_logged_in(&self, user: &User) {
|
||||||
|
tracing::debug!("[Notifier] Logged in user {}", user.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn auth_session_created(&self, user_id: &Uuid) {
|
||||||
|
tracing::debug!("[Notifier] Created auth session for user {}", user_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,13 @@ use std::str::FromStr;
|
|||||||
|
|
||||||
use anyhow::{Context, anyhow};
|
use anyhow::{Context, anyhow};
|
||||||
use argon2::{
|
use argon2::{
|
||||||
Argon2,
|
Argon2, PasswordHash, PasswordVerifier,
|
||||||
password_hash::{PasswordHasher, SaltString, rand_core::OsRng},
|
password_hash::{
|
||||||
|
PasswordHasher, SaltString,
|
||||||
|
rand_core::{OsRng, RngCore as _},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
use chrono::Utc;
|
||||||
use sqlx::{
|
use sqlx::{
|
||||||
ConnectOptions as _, Connection as _, PgConnection, PgPool,
|
ConnectOptions as _, Connection as _, PgConnection, PgPool,
|
||||||
postgres::{PgConnectOptions, PgPoolOptions},
|
postgres::{PgConnectOptions, PgPoolOptions},
|
||||||
@@ -13,7 +17,14 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use crate::domain::warren::{
|
use crate::domain::warren::{
|
||||||
models::{
|
models::{
|
||||||
user::{RegisterUserError, RegisterUserRequest, User, UserEmail, UserName, UserPassword},
|
auth_session::{
|
||||||
|
AuthSession,
|
||||||
|
requests::{CreateAuthSessionError, CreateAuthSessionRequest, SessionExpirationTime},
|
||||||
|
},
|
||||||
|
user::{
|
||||||
|
RegisterUserError, RegisterUserRequest, User, UserEmail, UserName, UserPassword,
|
||||||
|
VerifyUserPasswordError, VerifyUserPasswordRequest,
|
||||||
|
},
|
||||||
warren::{
|
warren::{
|
||||||
FetchWarrenError, FetchWarrenRequest, ListWarrensError, ListWarrensRequest, Warren,
|
FetchWarrenError, FetchWarrenRequest, ListWarrensError, ListWarrensRequest, Warren,
|
||||||
},
|
},
|
||||||
@@ -151,6 +162,89 @@ impl Postgres {
|
|||||||
|
|
||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_user_from_email(
|
||||||
|
&self,
|
||||||
|
connection: &mut PgConnection,
|
||||||
|
email: &UserEmail,
|
||||||
|
) -> Result<User, sqlx::Error> {
|
||||||
|
let user: User = sqlx::query_as(
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
users
|
||||||
|
WHERE
|
||||||
|
email = $1
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.bind(email)
|
||||||
|
.fetch_one(connection)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_user_password_against_hash(
|
||||||
|
&self,
|
||||||
|
password: &UserPassword,
|
||||||
|
hash: &str,
|
||||||
|
) -> Result<(), VerifyUserPasswordError> {
|
||||||
|
let argon = Argon2::default();
|
||||||
|
let hash =
|
||||||
|
PasswordHash::new(hash).map_err(|e| VerifyUserPasswordError::Unknown(anyhow!(e)))?;
|
||||||
|
|
||||||
|
argon
|
||||||
|
.verify_password(password.as_str().as_bytes(), &hash)
|
||||||
|
.map_err(|e| match e {
|
||||||
|
argon2::password_hash::Error::Password => {
|
||||||
|
VerifyUserPasswordError::IncorrectPassword
|
||||||
|
}
|
||||||
|
_ => VerifyUserPasswordError::Unknown(anyhow!(e)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_session(
|
||||||
|
&self,
|
||||||
|
connection: &mut PgConnection,
|
||||||
|
user: &User,
|
||||||
|
expiration: &SessionExpirationTime,
|
||||||
|
) -> anyhow::Result<AuthSession> {
|
||||||
|
let mut rng = OsRng;
|
||||||
|
let mut bytes = vec![0_u8; 32];
|
||||||
|
rng.fill_bytes(&mut bytes);
|
||||||
|
|
||||||
|
let session_id = hex::encode(bytes);
|
||||||
|
|
||||||
|
let expiration_time = Utc::now().timestamp_millis() + i64::try_from(expiration.millis())?;
|
||||||
|
|
||||||
|
let mut tx = connection.begin().await?;
|
||||||
|
|
||||||
|
let session: AuthSession = sqlx::query_as(
|
||||||
|
"
|
||||||
|
INSERT INTO auth_sessions (
|
||||||
|
session_id,
|
||||||
|
user_id,
|
||||||
|
expires_at
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
TO_TIMESTAMP($3::double precision / 1000)
|
||||||
|
)
|
||||||
|
RETURNING
|
||||||
|
*
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.bind(session_id)
|
||||||
|
.bind(user.id())
|
||||||
|
.bind(expiration_time)
|
||||||
|
.fetch_one(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
Ok(session)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WarrenRepository for Postgres {
|
impl WarrenRepository for Postgres {
|
||||||
@@ -217,6 +311,50 @@ impl AuthRepository for Postgres {
|
|||||||
|
|
||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn verify_user_password(
|
||||||
|
&self,
|
||||||
|
request: VerifyUserPasswordRequest,
|
||||||
|
) -> Result<User, VerifyUserPasswordError> {
|
||||||
|
let mut connection = self
|
||||||
|
.pool
|
||||||
|
.acquire()
|
||||||
|
.await
|
||||||
|
.context("Failed to get a PostgreSQL connection")?;
|
||||||
|
|
||||||
|
let user = self
|
||||||
|
.get_user_from_email(&mut connection, request.email())
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
if is_not_found_error(&e) {
|
||||||
|
VerifyUserPasswordError::NotFound(request.email().clone())
|
||||||
|
} else {
|
||||||
|
VerifyUserPasswordError::Unknown(anyhow!(e))
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
self.check_user_password_against_hash(request.password(), user.password_hash())?;
|
||||||
|
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_auth_session(
|
||||||
|
&self,
|
||||||
|
request: CreateAuthSessionRequest,
|
||||||
|
) -> Result<AuthSession, CreateAuthSessionError> {
|
||||||
|
let mut connection = self
|
||||||
|
.pool
|
||||||
|
.acquire()
|
||||||
|
.await
|
||||||
|
.context("Failed to get a PostgreSQL connection")?;
|
||||||
|
|
||||||
|
let session = self
|
||||||
|
.create_session(&mut connection, request.user(), request.expiration())
|
||||||
|
.await
|
||||||
|
.context("Failed to create session")?;
|
||||||
|
|
||||||
|
Ok(session)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_not_found_error(err: &sqlx::Error) -> bool {
|
fn is_not_found_error(err: &sqlx::Error) -> bool {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
# this file is ignored when the app is built since we're using SSG
|
# this file is ignored when the app is built since we're using SSG
|
||||||
|
|
||||||
NUXT_PUBLIC_API_BASE="http://127.0.0.1:8080/api"
|
NUXT_PUBLIC_API_BASE="http://127.0.0.1:8080/api"
|
||||||
|
NUXT_COOKIES_SECURE="false"
|
||||||
|
NUXT_COOKIES_SAME_SITE="strict"
|
||||||
|
|||||||
20
frontend/composables/useAuthSession.ts
Normal file
20
frontend/composables/useAuthSession.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import type { AuthSession } from '~/types/auth';
|
||||||
|
|
||||||
|
const SAME_SITE_SETTINGS = ['strict', 'lax', 'none'] as const;
|
||||||
|
|
||||||
|
export function useAuthSession() {
|
||||||
|
const config = useRuntimeConfig().public;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const sameSite = SAME_SITE_SETTINGS.includes(config.cookiesSameSite as any)
|
||||||
|
? (config.cookiesSameSite as (typeof SAME_SITE_SETTINGS)[number])
|
||||||
|
: 'strict';
|
||||||
|
|
||||||
|
return useCookie('auth_session', {
|
||||||
|
default: () => null as AuthSession | null,
|
||||||
|
secure: config.cookiesSecure.toLowerCase() === 'true',
|
||||||
|
sameSite,
|
||||||
|
path: '/',
|
||||||
|
httpOnly: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
42
frontend/lib/api/auth/login.ts
Normal file
42
frontend/lib/api/auth/login.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { toast } from 'vue-sonner';
|
||||||
|
import type { ApiResponse } from '~/types/api';
|
||||||
|
|
||||||
|
export async function loginUser(
|
||||||
|
email: string,
|
||||||
|
password: string
|
||||||
|
): Promise<{ success: boolean }> {
|
||||||
|
const { data, error } = await useFetch<ApiResponse<{ token: string }>>(
|
||||||
|
getApiUrl('auth/login'),
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
}),
|
||||||
|
responseType: 'json',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (data.value == null) {
|
||||||
|
toast.error('Login', {
|
||||||
|
description: error.value?.data ?? 'Failed to login',
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
useAuthSession().value = { type: 'WarrenAuth', id: data.value.data.token };
|
||||||
|
|
||||||
|
toast.success('Login', {
|
||||||
|
description: `Successfully logged in`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
15
frontend/lib/api/index.ts
Normal file
15
frontend/lib/api/index.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export function getApiHeaders(
|
||||||
|
includeAuth: boolean = true
|
||||||
|
): Record<string, string> {
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (includeAuth) {
|
||||||
|
const authSession = useAuthSession().value;
|
||||||
|
|
||||||
|
if (authSession != null) {
|
||||||
|
headers['authorization'] = `${authSession.type} ${authSession.id}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
@@ -2,12 +2,14 @@ import { toast } from 'vue-sonner';
|
|||||||
import type { DirectoryEntry } from '~/types';
|
import type { DirectoryEntry } from '~/types';
|
||||||
import type { ApiResponse } from '~/types/api';
|
import type { ApiResponse } from '~/types/api';
|
||||||
import type { Warren } from '~/types/warrens';
|
import type { Warren } from '~/types/warrens';
|
||||||
|
import { getApiHeaders } from '.';
|
||||||
|
|
||||||
export async function getWarrens(): Promise<Record<string, Warren>> {
|
export async function getWarrens(): Promise<Record<string, Warren>> {
|
||||||
const { data, error } = await useFetch<ApiResponse<{ warrens: Warren[] }>>(
|
const { data, error } = await useFetch<ApiResponse<{ warrens: Warren[] }>>(
|
||||||
getApiUrl('warrens'),
|
getApiUrl('warrens'),
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
headers: getApiHeaders(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ export default defineNuxtConfig({
|
|||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
public: {
|
public: {
|
||||||
apiBase: '/api',
|
apiBase: '/api',
|
||||||
|
cookiesSecure: 'false',
|
||||||
|
cookiesSameSite: 'strict',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
CardContent,
|
CardContent,
|
||||||
CardFooter,
|
CardFooter,
|
||||||
} from '@/components/ui/card';
|
} from '@/components/ui/card';
|
||||||
|
import { loginUser } from '~/lib/api/auth/login';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'auth',
|
layout: 'auth',
|
||||||
@@ -14,6 +15,35 @@ definePageMeta({
|
|||||||
|
|
||||||
// TODO: Get this from the backend
|
// TODO: Get this from the backend
|
||||||
const OPEN_ID = false;
|
const OPEN_ID = false;
|
||||||
|
const loggingIn = ref(false);
|
||||||
|
const email = ref('');
|
||||||
|
const password = ref('');
|
||||||
|
|
||||||
|
const inputValid = computed(
|
||||||
|
() => email.value.trim().length > 0 && password.value.trim().length > 0
|
||||||
|
);
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
if (!inputValid.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loggingIn.value = true;
|
||||||
|
|
||||||
|
const { success } = await loginUser(email.value, password.value);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
await navigateTo({ path: '/' });
|
||||||
|
}
|
||||||
|
|
||||||
|
loggingIn.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyDown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
submit();
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -29,24 +59,30 @@ const OPEN_ID = false;
|
|||||||
<Label for="email">Email</Label>
|
<Label for="email">Email</Label>
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
|
v-model="email"
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="your@email.com"
|
placeholder="your@email.com"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
required
|
required
|
||||||
|
@keydown="onKeyDown"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="password">Password</Label>
|
<Label for="password">Password</Label>
|
||||||
<Input
|
<Input
|
||||||
id="password"
|
id="password"
|
||||||
|
v-model="password"
|
||||||
type="password"
|
type="password"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
required
|
required
|
||||||
|
@keydown="onKeyDown"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter class="flex-col gap-2">
|
<CardFooter class="flex-col gap-2">
|
||||||
<Button class="w-full">Sign in</Button>
|
<Button class="w-full" :disabled="!inputValid" @click="submit"
|
||||||
|
>Sign in</Button
|
||||||
|
>
|
||||||
<Button class="w-full" variant="outline" :disabled="!OPEN_ID"
|
<Button class="w-full" variant="outline" :disabled="!OPEN_ID"
|
||||||
>OpenID Connect</Button
|
>OpenID Connect</Button
|
||||||
>
|
>
|
||||||
|
|||||||
5
frontend/types/auth.ts
Normal file
5
frontend/types/auth.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export type AuthSessionType = 'WarrenAuth';
|
||||||
|
export interface AuthSession {
|
||||||
|
type: AuthSessionType;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user