aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorrtkay123 <dev@kanjala.com>2026-02-22 15:18:34 +0200
committerrtkay123 <dev@kanjala.com>2026-02-22 15:18:34 +0200
commit3d4b23c53f203249b1d0c8e51d668f7b86dbaa6c (patch)
treee3ec9dbe9216e0633338c1ba8960721d10b8bc69
parent3fef60a3daf7d17dff22d815400e03f36e4128c9 (diff)
downloadsellershut-master.tar.bz2
sellershut-master.zip
feat: prefill welcome stepHEADmaster
-rw-r--r--lib/auth-service/src/client/mod.rs2
-rw-r--r--lib/auth-service/src/service/mod.rs9
-rw-r--r--sellershut/sellershut.toml1
-rw-r--r--sellershut/src/config/cli/mod.rs9
-rw-r--r--sellershut/src/config/mod.rs17
-rw-r--r--sellershut/src/server/mod.rs3
-rw-r--r--sellershut/src/server/routes/auth/mod.rs63
-rw-r--r--sellershut/src/server/routes/me/extractor.rs59
-rw-r--r--sellershut/src/server/routes/me/mod.rs31
-rw-r--r--sellershut/src/server/routes/mod.rs1
-rw-r--r--sellershut/src/server/shutdown.rs2
-rw-r--r--sellershut/src/state/mod.rs2
-rw-r--r--website/src/app.css4
-rw-r--r--website/src/lib/components/user-profile.svelte36
-rw-r--r--website/src/routes/+page.svelte4
-rw-r--r--website/src/routes/login/+page.svelte4
-rw-r--r--website/src/routes/welcome/+page.server.ts81
-rw-r--r--website/src/routes/welcome/+page.svelte9
18 files changed, 248 insertions, 89 deletions
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<Option<String>> {
- 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<LogLevel>,
+ /// Frontend URL
+ #[arg(
+ short,
+ long,
+ value_name = "FRONTEND_URL",
+ default_value = "http://localhost:5173"
+ )]
+ pub frontend_url: Option<Url>,
}
#[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<CliEnvironment> 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<AppState>) -> 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<Arc<AppState>>,
) -> Result<impl IntoResponse, AppError> {
let provider = csrf_token_validation_workflow(&params, &cookies, &data).await?;
-
- // Get an auth token
-
let user = match provider {
OauthProvider::Discord => {
let token = get_token(&data.discord_client, &data.http_client, &params.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<S> FromRequestParts<S> for AuthUser
+where
+ Arc<AppState>: FromRef<S>,
+ S: Send + Sync,
+{
+ type Rejection = AppError;
+
+ fn from_request_parts(
+ parts: &mut Parts,
+ state: &S,
+ ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
+ async {
+ let state = Arc::from_ref(state);
+
+ let cookie_header = parts.extract::<TypedHeader<Cookie>>().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<Arc<AppState>>, 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<AppState>) {
#[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 @@
-<svg
- class={$$props.class}
- viewBox="0 0 512 512"
- version="1.1"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:xlink="http://www.w3.org/1999/xlink"
->
- <title>user-profile-filled</title>
- <g id="Page-1" stroke="none" stroke-width="1" fill="currentColor" fill-rule="evenodd">
- <g id="drop" fill="currentColor" transform="translate(42.666667, 42.666667)">
- <path
- d="M213.333333,3.55271368e-14 C269.912851,3.55271368e-14 324.175019,22.4761259 364.18278,62.4838867 C404.190541,102.491647 426.666667,156.753816 426.666667,213.333333 C426.666667,331.15408 331.15408,426.666667 213.333333,426.666667 C95.5125867,426.666667 2.84217094e-14,331.15408 2.84217094e-14,213.333333 C2.84217094e-14,95.5125867 95.5125867,3.55271368e-14 213.333333,3.55271368e-14 Z M234.666667,234.666667 L192,234.666667 C139.18529,234.666667 93.8415802,266.653822 74.285337,312.314895 C105.229171,355.70638 155.977088,384 213.333333,384 C270.689579,384 321.437496,355.70638 352.381644,312.31198 C332.825087,266.653822 287.481377,234.666667 234.666667,234.666667 L234.666667,234.666667 Z M213.333333,64 C177.987109,64 149.333333,92.653776 149.333333,128 C149.333333,163.346224 177.987109,192 213.333333,192 C248.679557,192 277.333333,163.346224 277.333333,128 C277.333333,92.653776 248.679557,64 213.333333,64 Z"
- id="Combined-Shape"
- >
- </path>
- </g>
- </g>
-</svg>
+<svg
+ class={$$props.class}
+ viewBox="0 0 512 512"
+ version="1.1"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+>
+ <title>user-profile-filled</title>
+ <g id="Page-1" stroke="none" stroke-width="1" fill="currentColor" fill-rule="evenodd">
+ <g id="drop" fill="currentColor" transform="translate(42.666667, 42.666667)">
+ <path
+ d="M213.333333,3.55271368e-14 C269.912851,3.55271368e-14 324.175019,22.4761259 364.18278,62.4838867 C404.190541,102.491647 426.666667,156.753816 426.666667,213.333333 C426.666667,331.15408 331.15408,426.666667 213.333333,426.666667 C95.5125867,426.666667 2.84217094e-14,331.15408 2.84217094e-14,213.333333 C2.84217094e-14,95.5125867 95.5125867,3.55271368e-14 213.333333,3.55271368e-14 Z M234.666667,234.666667 L192,234.666667 C139.18529,234.666667 93.8415802,266.653822 74.285337,312.314895 C105.229171,355.70638 155.977088,384 213.333333,384 C270.689579,384 321.437496,355.70638 352.381644,312.31198 C332.825087,266.653822 287.481377,234.666667 234.666667,234.666667 L234.666667,234.666667 Z M213.333333,64 C177.987109,64 149.333333,92.653776 149.333333,128 C149.333333,163.346224 177.987109,192 213.333333,192 C248.679557,192 277.333333,163.346224 277.333333,128 C277.333333,92.653776 248.679557,64 213.333333,64 Z"
+ id="Combined-Shape"
+ >
+ </path>
+ </g>
+ </g>
+</svg>
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 @@
<script lang="ts">
- import { profileSchema } from "$lib/schemas/profile";
-
+ import { profileSchema } from '$lib/schemas/profile';
</script>
+
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
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();
};
-
</script>
<div
@@ -24,7 +23,8 @@
<div class="mt-8 space-y-4">
<a
href={url('discord')}
- target="_blank" rel="noopener noreferrer"
+ target="_blank"
+ rel="noopener noreferrer"
class="group relative flex w-full transform-gpu items-center justify-center rounded-xl border border-gray-900 bg-white px-4 py-3 text-sm font-medium text-gray-900 transition-all duration-200 hover:bg-gray-50 focus:ring-2 focus:ring-rose-500 focus:ring-offset-2 focus:outline-none active:scale-[0.98]"
>
<span class="absolute inset-y-0 left-0 flex items-center pl-4">
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<string, string>
- });
- }
-
- // 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<string, string>
- });
- }
-
- // 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<string, string>,
+ });
+ }
+
+ 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<string, string>,
+ });
+ }
+
+ // 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 @@
<script lang="ts">
import { enhance } from '$app/forms';
- import type { ActionData } from './$types';
+ import type { ActionData, PageData } from './$types';
type FormFailure = {
errors: { username?: string[]; bio?: string[] };
@@ -8,7 +8,7 @@
};
const domain = 'sellershut.com';
- let { form }: { form: ActionData } = $props();
+ let { form, data }: { form: ActionData; data: PageData } = $props();
const formError = $derived(form && 'errors' in form ? (form as FormFailure) : null);
const errors = $derived(formError?.errors);
@@ -30,6 +30,9 @@
$effect(() => {
if (formError?.data?.username) username = formError.data.username;
if (formError?.data?.bio) bio = formError.data.bio;
+ if (data) {
+ username = data.user.username;
+ }
});
</script>
@@ -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"