From 3d4b23c53f203249b1d0c8e51d668f7b86dbaa6c Mon Sep 17 00:00:00 2001 From: rtkay123 Date: Sun, 22 Feb 2026 15:18:34 +0200 Subject: feat: prefill welcome step --- lib/auth-service/src/client/mod.rs | 2 +- lib/auth-service/src/service/mod.rs | 9 ++- sellershut/sellershut.toml | 1 + sellershut/src/config/cli/mod.rs | 9 +++ sellershut/src/config/mod.rs | 17 ++++-- sellershut/src/server/mod.rs | 3 +- sellershut/src/server/routes/auth/mod.rs | 63 +++++++++++++------- sellershut/src/server/routes/me/extractor.rs | 59 +++++++++++++++++++ sellershut/src/server/routes/me/mod.rs | 31 ++++++++++ sellershut/src/server/routes/mod.rs | 1 + sellershut/src/server/shutdown.rs | 2 - sellershut/src/state/mod.rs | 2 + website/src/app.css | 4 +- website/src/lib/components/user-profile.svelte | 36 ++++++------ website/src/routes/+page.svelte | 4 +- website/src/routes/login/+page.svelte | 4 +- website/src/routes/welcome/+page.server.ts | 81 ++++++++++++++++---------- website/src/routes/welcome/+page.svelte | 9 ++- 18 files changed, 248 insertions(+), 89 deletions(-) create mode 100644 sellershut/src/server/routes/me/extractor.rs create mode 100644 sellershut/src/server/routes/me/mod.rs diff --git a/lib/auth-service/src/client/mod.rs b/lib/auth-service/src/client/mod.rs index e02672b..774a863 100644 --- a/lib/auth-service/src/client/mod.rs +++ b/lib/auth-service/src/client/mod.rs @@ -30,7 +30,7 @@ impl Deref for OauthClient { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ClientConfig { client_id: String, client_secret: SecretString, diff --git a/lib/auth-service/src/service/mod.rs b/lib/auth-service/src/service/mod.rs index 31c9019..b699c51 100644 --- a/lib/auth-service/src/service/mod.rs +++ b/lib/auth-service/src/service/mod.rs @@ -30,7 +30,14 @@ pub struct AuthService { impl AccountMgr for AuthService { #[instrument(skip(self))] async fn get_apid_by_email(&self, email: &str) -> Result> { - todo!() + let result = sqlx::query_scalar!( + "select user_id from account where email = $1 limit 1", + email + ) + .fetch_optional(&self.database) + .await?; + debug!(user = ?result, "find my email"); + Ok(result) } #[instrument(skip(transaction))] diff --git a/sellershut/sellershut.toml b/sellershut/sellershut.toml index 01b9481..8a69b95 100644 --- a/sellershut/sellershut.toml +++ b/sellershut/sellershut.toml @@ -2,6 +2,7 @@ port = 2210 environment = "dev" log-level = "trace" +frontend-url = "http://localhost:5173" [database] url = "postgres://postgres:password@localhost:5432/sellershut" diff --git a/sellershut/src/config/cli/mod.rs b/sellershut/src/config/cli/mod.rs index 00d51a1..6afd615 100644 --- a/sellershut/src/config/cli/mod.rs +++ b/sellershut/src/config/cli/mod.rs @@ -6,6 +6,7 @@ use std::path::PathBuf; use clap::Parser; use clap::ValueEnum; use serde::Deserialize; +use url::Url; use crate::config::cli::cache::Cache; use crate::config::cli::{database::Database, oauth::Oauth}; @@ -43,6 +44,14 @@ pub struct Server { /// Log Level #[arg(short, long, value_name = "LOG_LEVEL")] pub log_level: Option, + /// Frontend URL + #[arg( + short, + long, + value_name = "FRONTEND_URL", + default_value = "http://localhost:5173" + )] + pub frontend_url: Option, } #[derive(Deserialize, ValueEnum, Clone, Copy, Default, Debug)] diff --git a/sellershut/src/config/mod.rs b/sellershut/src/config/mod.rs index 3646b89..03f2675 100644 --- a/sellershut/src/config/mod.rs +++ b/sellershut/src/config/mod.rs @@ -29,7 +29,7 @@ impl From for Environment { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Configuration { pub server: Server, pub oauth: Oauth, @@ -37,20 +37,21 @@ pub struct Configuration { pub cache: CacheConfig, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Oauth { pub redirect_url: Url, pub discord: ClientConfig, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Server { pub port: u16, pub environment: Environment, pub log_level: Level, + pub frontend_url: Url, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Database { pub url: Url, pub pool_size: u32, @@ -71,6 +72,13 @@ impl Configuration { .or(file.server.log_level) .unwrap_or_default(); + let frontend_url = cli + .server + .frontend_url + .as_ref() + .or(file.server.frontend_url.as_ref()) + .unwrap(); + let environment = cli .server .environment @@ -225,6 +233,7 @@ impl Configuration { Ok(Self { server: Server { + frontend_url: frontend_url.to_owned(), port: port.unwrap(), environment, log_level: log_level.into(), diff --git a/sellershut/src/server/mod.rs b/sellershut/src/server/mod.rs index 9818ce5..b02618e 100644 --- a/sellershut/src/server/mod.rs +++ b/sellershut/src/server/mod.rs @@ -37,7 +37,8 @@ pub async fn router(state: Arc) -> Router<()> { let stubs = OpenApiRouter::with_openapi(doc) .routes(utoipa_axum::routes!(routes::health)) .routes(utoipa_axum::routes!(routes::auth::auth)) - .routes(utoipa_axum::routes!(routes::auth::authorised)); + .routes(utoipa_axum::routes!(routes::auth::authorised)) + .routes(utoipa_axum::routes!(routes::me::get_me)); let (router, _api) = stubs.split_for_parts(); diff --git a/sellershut/src/server/routes/auth/mod.rs b/sellershut/src/server/routes/auth/mod.rs index 7bcfe0b..1eef57d 100644 --- a/sellershut/src/server/routes/auth/mod.rs +++ b/sellershut/src/server/routes/auth/mod.rs @@ -134,9 +134,6 @@ pub async fn authorised( State(data): State>, ) -> Result { let provider = csrf_token_validation_workflow(¶ms, &cookies, &data).await?; - - // Get an auth token - let user = match provider { OauthProvider::Discord => { let token = get_token(&data.discord_client, &data.http_client, ¶ms.code).await?; @@ -144,30 +141,54 @@ pub async fn authorised( } }; - if let Some(ap_id) = data.auth_service.get_apid_by_email(&user.email).await? { + let url = if let Some(ap_id) = data.auth_service.get_apid_by_email(&user.email).await? { debug!("user exists"); - data.auth_service - .create_account(provider.into(), &user.id, &ap_id, &user.email, None::<&sqlx::PgPool>) - .await?; - } else { - debug!("user does not exist, creating"); - // create account and user in a transaction - let mut transaction = data.database.begin().await?; - data.auth_service .create_account( provider.into(), &user.id, - "", + &ap_id, &user.email, - Some(&mut *transaction), + None::<&sqlx::PgPool>, ) .await?; + data.config.server.frontend_url.clone() + } else { + debug!("user does not exist, creating"); + // create account and user in a transaction + // let mut transaction = data.database.begin().await?; + // + // data.auth_service + // .create_account( + // provider.into(), + // &user.id, + // "", + // &user.email, + // Some(&mut *transaction), + // ) + // .await?; + // + // transaction.commit().await?; - transaction.commit().await?; - } + let mut endpoint = data.config.server.frontend_url.clone(); + endpoint.set_path("welcome"); + endpoint + }; + + let session_id = cookies.get(COOKIE_NAME).context("Session unavailable")?; + + let mut session = data + .auth_service + .load_session(session_id.to_owned()) + .await? + .context("Session expired")?; + + session.insert("email", user.email)?; + session.insert("username", user.username)?; - Ok(String::default()) + data.auth_service.store_session(session).await?; + + Ok(Redirect::to(url.as_str())) } async fn csrf_token_validation_workflow( @@ -195,10 +216,10 @@ async fn csrf_token_validation_workflow( .get(OAUTH_PROVIDER) .context("provider not found in session")?; - data.auth_service - .destroy_session(session) - .await - .context("Failed to destroy old session")?; + // data.auth_service + // .destroy_session(session) + // .await + // .context("Failed to destroy old session")?; // Validate CSRF token is the same as the one in the auth request if *stored_csrf_token.secret() != auth_request.state { diff --git a/sellershut/src/server/routes/me/extractor.rs b/sellershut/src/server/routes/me/extractor.rs new file mode 100644 index 0000000..60b2099 --- /dev/null +++ b/sellershut/src/server/routes/me/extractor.rs @@ -0,0 +1,59 @@ +use anyhow::Context; +use async_session::SessionStore; +use axum::{ + RequestPartsExt, + extract::{FromRef, FromRequestParts}, + http::request::Parts, +}; +use axum_extra::{TypedHeader, headers::Cookie}; +use std::sync::Arc; + +use crate::{ + server::{error::AppError, routes::auth::COOKIE_NAME}, + state::AppState, +}; + +#[derive(Debug)] +pub struct AuthUser { + pub email: String, + pub username: String, +} + +impl FromRequestParts for AuthUser +where + Arc: FromRef, + S: Send + Sync, +{ + type Rejection = AppError; + + fn from_request_parts( + parts: &mut Parts, + state: &S, + ) -> impl Future> + Send { + async { + let state = Arc::from_ref(state); + + let cookie_header = parts.extract::>().await?; + dbg!(&cookie_header); + + let session_id = cookie_header + .get(COOKIE_NAME) + .context("Session cookie missing")?; + + let session = state + .auth_service + .load_session(session_id.to_owned()) + .await? + .context("Invalid or expired session")?; + + let email: String = session + .get("email") + .context("Session corrupted: no email")?; + let username: String = session + .get("username") + .context("Session corrupted: no username")?; + + Ok(AuthUser { email, username }) + } + } +} diff --git a/sellershut/src/server/routes/me/mod.rs b/sellershut/src/server/routes/me/mod.rs new file mode 100644 index 0000000..c35bbca --- /dev/null +++ b/sellershut/src/server/routes/me/mod.rs @@ -0,0 +1,31 @@ +mod extractor; +use std::sync::Arc; + +use axum::{Json, extract::State, response::IntoResponse}; +use serde::Serialize; + +use crate::{server::routes::me::extractor::AuthUser, state::AppState}; + +#[derive(Serialize)] +pub struct UserResponse { + pub email: String, + pub username: String, +} + +/// Get health of the API. +#[utoipa::path( + method(get), + path = "/me", + responses( + (status = OK, description = "Success", body = str, content_type = "application/json") + ) +)] +pub async fn get_me(State(data): State>, user: AuthUser) -> impl IntoResponse { + // Fetch from DB + // let profile = data.auth_service.get_profile_by_id(&user.user_id).await?; + + Json(UserResponse { + email: user.email, + username: user.username, + }) +} diff --git a/sellershut/src/server/routes/mod.rs b/sellershut/src/server/routes/mod.rs index 66eb4e6..589745b 100644 --- a/sellershut/src/server/routes/mod.rs +++ b/sellershut/src/server/routes/mod.rs @@ -1,4 +1,5 @@ pub(super) mod auth; +pub(super) mod me; use axum::response::IntoResponse; use utoipa::OpenApi; diff --git a/sellershut/src/server/shutdown.rs b/sellershut/src/server/shutdown.rs index 075b3d0..08153fe 100644 --- a/sellershut/src/server/shutdown.rs +++ b/sellershut/src/server/shutdown.rs @@ -21,8 +21,6 @@ pub async fn shutdown_signal(state: Arc) { #[cfg(not(unix))] let terminate = std::future::pending::<()>(); - state.database.close().await; - tokio::select! { _ = ctrl_c => {}, _ = terminate => {}, diff --git a/sellershut/src/state/mod.rs b/sellershut/src/state/mod.rs index ab0262e..748bc0b 100644 --- a/sellershut/src/state/mod.rs +++ b/sellershut/src/state/mod.rs @@ -12,6 +12,7 @@ pub struct AppState { pub auth_service: AuthService, pub database: PgPool, pub http_client: HttpAuthClient, + pub config: Configuration, } impl AppState { @@ -45,6 +46,7 @@ impl AppState { auth_service, database, http_client: reqwest::Client::new().into(), + config: config.clone(), }) } } diff --git a/website/src/app.css b/website/src/app.css index 798629d..cd67023 100644 --- a/website/src/app.css +++ b/website/src/app.css @@ -1,3 +1,3 @@ @import 'tailwindcss'; -@plugin "@tailwindcss/forms"; -@plugin "@tailwindcss/typography"; +@plugin '@tailwindcss/forms'; +@plugin '@tailwindcss/typography'; diff --git a/website/src/lib/components/user-profile.svelte b/website/src/lib/components/user-profile.svelte index 7ad7ba2..4e41e61 100644 --- a/website/src/lib/components/user-profile.svelte +++ b/website/src/lib/components/user-profile.svelte @@ -1,18 +1,18 @@ - - user-profile-filled - - - - - - - + + user-profile-filled + + + + + + + diff --git a/website/src/routes/+page.svelte b/website/src/routes/+page.svelte index a9c8dd0..a69efc4 100644 --- a/website/src/routes/+page.svelte +++ b/website/src/routes/+page.svelte @@ -1,6 +1,6 @@ +

Welcome to SvelteKit

Visit svelte.dev/docs/kit to read the documentation

diff --git a/website/src/routes/login/+page.svelte b/website/src/routes/login/+page.svelte index 1226799..6e6325d 100644 --- a/website/src/routes/login/+page.svelte +++ b/website/src/routes/login/+page.svelte @@ -7,7 +7,6 @@ url.searchParams.set('provider', provider); return url.toString(); }; -
diff --git a/website/src/routes/welcome/+page.server.ts b/website/src/routes/welcome/+page.server.ts index 503f361..5a6e024 100644 --- a/website/src/routes/welcome/+page.server.ts +++ b/website/src/routes/welcome/+page.server.ts @@ -3,36 +3,53 @@ import type { Actions, PageServerLoad } from './$types'; import { profileSchema } from '$lib/schemas/profile'; export const actions: Actions = { - default: async ({ request, fetch }) => { - console.log("hello"); - const formData = await request.formData(); - const data = Object.fromEntries(formData); - - // 1. Zod Validation - const result = profileSchema.safeParse(data); - - if (!result.success) { - return fail(400, { - errors: result.error.flatten().fieldErrors, - data: data as Record - }); - } - - // 2. Example: Check availability against your backend - // Replace this with your actual backend URL - const response = await fetch(`/api/check-username?u=${result.data.username}`); - const { available } = await response.json(); - - if (!available) { - return fail(400, { - errors: { username: ["This username is already taken"] }, - data: data as Record - }); - } - - // 3. Success: Send to backend to create profile - // await fetch('...', { method: 'POST', body: JSON.stringify(result.data) }); - - throw redirect(303, '/dashboard'); - } + default: async ({ request, fetch }) => { + console.log('hello'); + const formData = await request.formData(); + const data = Object.fromEntries(formData); + + const result = profileSchema.safeParse(data); + + if (!result.success) { + return fail(400, { + errors: result.error.flatten().fieldErrors, + data: data as Record, + }); + } + + const response = await fetch(`/api/check-username?u=${result.data.username}`); + const { available } = await response.json(); + + if (!available) { + return fail(400, { + errors: { username: ['This username is already taken'] }, + data: data as Record, + }); + } + + // 3. Success: Send to backend to create profile + // await fetch('...', { method: 'POST', body: JSON.stringify(result.data) }); + + throw redirect(303, '/dashboard'); + }, +}; + +export const load = async ({ fetch, request }) => { + const res = await fetch('http://localhost:2210/me', { + headers: { + cookie: request.headers.get('cookie') || '', + }, + }); + + if (res.status === 401) throw redirect(302, '/login'); + + const userData = await res.json(); + + // if (userData.is_onboarded) { + // throw redirect(302, '/dashboard'); + // } + // + return { + user: userData, + }; }; diff --git a/website/src/routes/welcome/+page.svelte b/website/src/routes/welcome/+page.svelte index 863b69f..ade0837 100644 --- a/website/src/routes/welcome/+page.svelte +++ b/website/src/routes/welcome/+page.svelte @@ -1,6 +1,6 @@ @@ -128,7 +131,7 @@ id="email" name="email" type="email" - value="email@domain.com" + value={data.user.email} readonly tabindex="-1" class="flex-1 cursor-not-allowed border-none bg-transparent py-2.5 pr-4 pl-3 text-sm text-gray-500 outline-none focus:ring-0 md:text-base" -- cgit v1.2.3