// rocie - An enterprise grocery management system // // Copyright (C) 2026 Benedikt Peetz // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of Rocie. // // You should have received a copy of the License along with this program. // If not, see . use actix_identity::Identity; use actix_web::{HttpMessage, HttpRequest, HttpResponse, Responder, Result, post, web}; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use crate::{ api::set::auth::user::UserStub, app::App, storage::{ migrate::migrate_db, sql::{ config::Config, insert::Operations, user::{PasswordHash, User, UserId}, }, }, }; #[derive(ToSchema, Deserialize, Serialize)] struct LoginInfo { /// The user name of the user. user_name: String, /// The password of the user. password: String, } /// Log in as a specific user #[utoipa::path( responses( ( status = OK, description = "User logged in", ), ( status = NOT_FOUND, description = "User name not found" ), ( status = FORBIDDEN, description = "Password did not match" ), ( status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String ) ), request_body = LoginInfo, )] #[post("/login")] async fn login( request: HttpRequest, app: web::Data, info: web::Json, ) -> Result { let info = info.into_inner(); if let Some(user) = User::from_name(&app, info.user_name).await? { if user.password_hash.verify(&info.password) { Identity::login(&request.extensions(), user.id.to_string())?; Ok(HttpResponse::Ok().finish()) } else { Ok(HttpResponse::Forbidden().finish()) } } else { Ok(HttpResponse::NotFound().finish()) } } /// Log the current user out #[utoipa::path( responses( ( status = OK, description = "User logged out", ), ( status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String ) ), )] #[post("/logout")] async fn logout(user: Identity) -> impl Responder { user.logout(); HttpResponse::Ok() } #[derive(ToSchema, Deserialize)] struct ProvisionInfo { user: UserStub, /// Whether we should apply the default configuration. use_defaults: bool, } /// Provision this instance. /// /// This only works, if no users exist yet. #[utoipa::path( responses( ( status = OK, description = "User created and logged in", body = UserId, ), ( status = FORBIDDEN, description = "Instance already provisioned", ), ( status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String ) ), request_body = ProvisionInfo, )] #[post("/provision")] async fn provision( request: HttpRequest, app: web::Data, info: web::Json, ) -> Result { if User::get_all(&app).await?.is_empty() { let info = info.into_inner(); let mut ops = Operations::new("register user (during provisioning)"); let password_hash = PasswordHash::from_password(&info.user.password); let user = User::register( info.user.name, password_hash, info.user.description, &mut ops, ); ops.apply(&app).await?; if info.use_defaults { let mut ops = Operations::new("Set should use defaults on config (during provisioning)"); let mut config = Config::get(&app).await?; config.set_use_default(true, &mut ops); ops.apply(&app).await?; migrate_db(&app).await?; } Identity::login(&request.extensions(), user.id.to_string())?; Ok(HttpResponse::Ok().json(user.id)) } else { Ok(HttpResponse::Forbidden().finish()) } }