From 7afd726188560dee567a30b11ca43a2fa46facd0 Mon Sep 17 00:00:00 2001 From: SirBlobby Date: Thu, 14 May 2026 18:40:27 -0400 Subject: [PATCH] Update Version: 1.4.0 --- Dockerfile | 10 +- README.md | 20 +- docker-compose.yml | 2 + package.json | 2 +- server/Cargo.toml | 2 +- server/src/admin.rs | 140 +++++++ server/src/auth.rs | 16 +- server/src/db/postgres.rs | 21 +- server/src/db/sqlite.rs | 13 +- server/src/main.rs | 11 + server/src/models.rs | 53 +++ server/src/setup.rs | 73 ++++ src/lib/components/Toolbar.svelte | 59 ++- src/lib/ts/auth.ts | 1 + src/lib/ts/store.ts | 1 + src/routes/+layout.svelte | 13 + src/routes/doc/[id]/+page.svelte | 17 +- src/routes/login/+page.svelte | 14 +- src/routes/register/+page.svelte | 102 +++-- src/routes/settings/+page.svelte | 622 +++++++++++++++++++++--------- src/routes/setup/+page.svelte | 137 +++++++ 21 files changed, 1051 insertions(+), 278 deletions(-) create mode 100644 server/src/admin.rs create mode 100644 server/src/setup.rs create mode 100644 src/routes/setup/+page.svelte diff --git a/Dockerfile b/Dockerfile index b346358..ad51f3e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,10 +33,12 @@ COPY --from=backend-builder /app/server/target/release/server /app/server # Postgres: postgres://user:pass@host:5432/typstdrive # DB_TYPE Database backend: "sqlite" or "postgres" # Auto-detected from DATABASE_URL if not set. -# COOKIE_SECRET 64+ byte secret for signing session cookies. -# If unset, a random key is generated on each start -# and all sessions are invalidated on restart. -# RUST_LOG Log filter (default: server=debug,tower_http=debug) +# COOKIE_SECRET 64+ byte secret for signing session cookies. +# If unset, a random key is generated on each start +# and all sessions are invalidated on restart. +# ALLOW_REGISTRATION Set to "false" to disable public registration. +# Admins can still create accounts via the admin panel. +# RUST_LOG Log filter (default: server=debug,tower_http=debug) ENV PORT=3000 ENV STATIC_DIR=/app/build ENV DATABASE_URL=sqlite:///data/typstdrive.db?mode=rwc diff --git a/README.md b/README.md index f280126..985ebef 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # TypstDrive -[![Version](https://img.shields.io/badge/version-1.3.0-blue.svg)](https://github.com/your-username/typstdrive) +[![Version](https://img.shields.io/badge/version-1.4.0-blue.svg)](https://github.com/your-username/typstdrive) [![Typst Version](https://img.shields.io/badge/Typst-0.14.2-239dad?logo=typst&logoColor=white)](https://typst.app/) [![Rust](https://img.shields.io/badge/Rust-1.82+-orange?logo=rust&logoColor=white)](https://www.rust-lang.org/) [![SvelteKit](https://img.shields.io/badge/SvelteKit-5-ff3e00?logo=svelte)](https://kit.svelte.dev/) @@ -15,10 +15,11 @@ TypstDrive is a collaborative web editor for Typst. With built-in dark mode, mul ## Features - **Real-Time Collaboration**: Powered by Yjs and CodeMirror 6, see changes and cursors from other users instantly. -- **Instant Preview**: Compile Typst to SVG on the fly with sub-second latency, featuring interactive document zoom controls. +- **Instant Preview**: Compile Typst to SVG on the fly with sub-second latency, featuring interactive document zoom controls and a collapsible preview pane. - **Customizable Themes**: Choose from multiple editor themes (Catppuccin, Arch Linux, Cerberus) and toggle global dark mode. - **Export Options**: Export your compiled documents directly to PDF, PNG, SVG, HTML, Markdown, Word, or LaTeX formats using internal conversion and Pandoc integrations. - **User Authentication & Document Access**: Secure accounts, workspaces, and sharing features via email-based collaborator invitations (Editor or Viewer roles) for all your documents. +- **Admin System**: First-run setup wizard creates an admin account. Admins can manage all users, create new accounts with temporary passwords, toggle admin privileges, and delete accounts from the Settings panel. - **Presentation Mode**: Turn your documents into instant slideshows with built-in slide controls and a live drawing/annotation tool overlay. - **Asset Management**: Upload and seamlessly use custom fonts and images directly within your documents. @@ -85,6 +86,8 @@ TypstDrive is completely self-hostable. A Docker image packages both the Rust ba 3. Open your browser and navigate to `http://localhost:3000`. +On first launch with no users in the database, you will be redirected to the **Setup** page to create the initial admin account. + ### Data Storage By default, TypstDrive uses **SQLite** — no separate database container required. All data is stored in a single file persisted via the `appdata` Docker volume. @@ -102,6 +105,7 @@ All variables can be set in the `environment:` section of `docker-compose.yml` o | `PORT` | `3000` | Port the HTTP server listens on. | | `STATIC_DIR` | `/app/build` | Path to compiled frontend assets. | | `COOKIE_SECRET` | *(random)* | 64+ byte secret used to sign session cookies. **If not set, a random key is generated on startup and all sessions are invalidated on every container restart.** Generate a stable value with: `openssl rand -hex 64` | +| `ALLOW_REGISTRATION` | `true` | Set to `false` to disable public self-registration. When disabled, the register link is hidden on the login page and the registration endpoint returns 403. Admins can still create accounts from the Settings panel. | | `RUST_LOG` | `server=debug,tower_http=debug` | Log filter. Set to `info` for quieter production logs. | #### Example: production-ready `docker-compose.yml` snippet @@ -112,6 +116,7 @@ environment: - DB_TYPE=sqlite - PORT=3000 - COOKIE_SECRET=your-64-plus-byte-secret-here + - ALLOW_REGISTRATION=false - RUST_LOG=info ``` @@ -120,6 +125,15 @@ Generate a `COOKIE_SECRET`: openssl rand -hex 64 ``` +### Admin Panel + +The first account created via the setup wizard is automatically an administrator. Admins have access to an **Admin** section in Settings (`/settings`) which provides: + +- A list of all users with creation dates +- **Create User** — set a username, email, and temporary password; optionally grant admin privileges immediately +- **Toggle Admin** — promote or demote any other user +- **Delete User** — permanently remove any account other than your own + ## Contributing & Local Development Clone the official Typst compiler into the `typst/` folder before building the backend: @@ -156,4 +170,4 @@ The frontend dev server proxies API calls to `localhost:3000` automatically.

Dashboard view Authentication view -

\ No newline at end of file +

diff --git a/docker-compose.yml b/docker-compose.yml index 74412a8..8d66e7b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,6 +21,8 @@ services: # Without this, sessions are cleared on every container restart. # Generate one with: openssl rand -hex 64 # - COOKIE_SECRET=your-64-plus-byte-secret-here + # Set to "false" to disable public registration (admin panel can still create users) + - ALLOW_REGISTRATION=false # Log verbosity (default: server=debug,tower_http=debug) # - RUST_LOG=info volumes: diff --git a/package.json b/package.json index 3f853b2..0a2bd1b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "typstdrive", "private": true, - "version": "1.3.0", + "version": "1.4.0", "type": "module", "scripts": { "dev": "vite dev --host", diff --git a/server/Cargo.toml b/server/Cargo.toml index f7fcbd5..2dbc9be 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "server" -version = "1.3.0" +version = "1.4.0" edition = "2021" [dependencies] diff --git a/server/src/admin.rs b/server/src/admin.rs new file mode 100644 index 0000000..6526006 --- /dev/null +++ b/server/src/admin.rs @@ -0,0 +1,140 @@ +use axum::{ + extract::{Path, State}, + http::StatusCode, + Json, +}; +use axum_extra::extract::cookie::SignedCookieJar; +use uuid::Uuid; +use argon2::{ + password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, + Argon2, +}; + +use crate::{ + models::{AdminCreateUserRequest, AdminUserView, UpdateUserRequest}, + AppState, +}; + +async fn require_admin(state: &AppState, jar: &SignedCookieJar) -> Result { + let user_id = jar.get("session_user_id").map(|c| c.value().to_string()) + .ok_or((StatusCode::UNAUTHORIZED, "Not logged in".to_string()))?; + + let is_admin: Option<(i64,)> = sqlx::query_as("SELECT is_admin FROM users WHERE id = ?") + .bind(&user_id) + .fetch_optional(&state.db) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + + match is_admin { + Some((v,)) if v != 0 => Ok(user_id), + _ => Err((StatusCode::FORBIDDEN, "Admin access required".to_string())), + } +} + +pub async fn create_user( + State(state): State, + jar: SignedCookieJar, + Json(payload): Json, +) -> Result<(StatusCode, Json), (StatusCode, String)> { + require_admin(&state, &jar).await?; + + if payload.username.is_empty() || payload.email.is_empty() || payload.password.is_empty() { + return Err((StatusCode::BAD_REQUEST, "Username, email, and password are required".to_string())); + } + + let salt = SaltString::generate(&mut OsRng); + let password_hash = Argon2::default() + .hash_password(payload.password.as_bytes(), &salt) + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? + .to_string(); + + let user_id = Uuid::new_v4().to_string(); + let is_admin = payload.is_admin.unwrap_or(false); + + let result = sqlx::query_as::<_, AdminUserView>( + "INSERT INTO users (id, username, email, password_hash, is_admin) VALUES (?, ?, ?, ?, ?) RETURNING id, username, email, is_admin, created_at" + ) + .bind(&user_id) + .bind(&payload.username) + .bind(&payload.email) + .bind(&password_hash) + .bind(if is_admin { 1i64 } else { 0i64 }) + .fetch_one(&state.db) + .await; + + match result { + Ok(user) => Ok((StatusCode::CREATED, Json(user))), + Err(sqlx::Error::Database(err)) if err.is_unique_violation() => { + Err((StatusCode::CONFLICT, "Username or email already exists".to_string())) + } + Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())), + } +} + +pub async fn list_users( + State(state): State, + jar: SignedCookieJar, +) -> Result>, (StatusCode, String)> { + require_admin(&state, &jar).await?; + + let users = sqlx::query_as::<_, AdminUserView>( + "SELECT id, username, email, is_admin, created_at FROM users ORDER BY created_at ASC" + ) + .fetch_all(&state.db) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + + Ok(Json(users)) +} + +pub async fn update_user( + State(state): State, + jar: SignedCookieJar, + Path(user_id): Path, + Json(payload): Json, +) -> Result, (StatusCode, String)> { + let requester_id = require_admin(&state, &jar).await?; + + if let Some(is_admin) = payload.is_admin { + if !is_admin && requester_id == user_id { + return Err((StatusCode::BAD_REQUEST, "Cannot remove your own admin privileges".to_string())); + } + sqlx::query("UPDATE users SET is_admin = ? WHERE id = ?") + .bind(if is_admin { 1i64 } else { 0i64 }) + .bind(&user_id) + .execute(&state.db) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + } + + let user = sqlx::query_as::<_, AdminUserView>( + "SELECT id, username, email, is_admin, created_at FROM users WHERE id = ?" + ) + .bind(&user_id) + .fetch_optional(&state.db) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? + .ok_or((StatusCode::NOT_FOUND, "User not found".to_string()))?; + + Ok(Json(user)) +} + +pub async fn delete_user( + State(state): State, + jar: SignedCookieJar, + Path(user_id): Path, +) -> Result { + let requester_id = require_admin(&state, &jar).await?; + + if requester_id == user_id { + return Err((StatusCode::BAD_REQUEST, "Cannot delete your own account via admin panel".to_string())); + } + + sqlx::query("DELETE FROM users WHERE id = ?") + .bind(&user_id) + .execute(&state.db) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + + Ok(StatusCode::NO_CONTENT) +} diff --git a/server/src/auth.rs b/server/src/auth.rs index 9e9857e..644c7dc 100644 --- a/server/src/auth.rs +++ b/server/src/auth.rs @@ -11,6 +11,8 @@ use crate::{ AppState, }; +const USER_FIELDS: &str = "id, username, email, password_hash, is_admin"; + use argon2::{ password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, Argon2, @@ -20,6 +22,10 @@ pub async fn register( State(state): State, Json(payload): Json, ) -> Result, (StatusCode, String)> { + if !state.registration_enabled { + return Err((StatusCode::FORBIDDEN, "Registration is disabled on this instance".to_string())); + } + if payload.username.is_empty() || payload.password.is_empty() || payload.email.is_empty() { return Err((StatusCode::BAD_REQUEST, "Username, email, and password cannot be empty".to_string())); } @@ -34,7 +40,7 @@ pub async fn register( let user_id = Uuid::new_v4().to_string(); let result = sqlx::query_as::<_, User>( - "INSERT INTO users (id, username, email, password_hash) VALUES (?, ?, ?, ?) RETURNING id, username, email, password_hash" + "INSERT INTO users (id, username, email, password_hash) VALUES (?, ?, ?, ?) RETURNING id, username, email, password_hash, is_admin" ) .bind(&user_id) .bind(&payload.username) @@ -57,7 +63,7 @@ pub async fn login( jar: SignedCookieJar, Json(payload): Json, ) -> Result<(SignedCookieJar, Json), (StatusCode, String)> { - let user = sqlx::query_as::<_, User>("SELECT id, username, email, password_hash FROM users WHERE email = ?") + let user = sqlx::query_as::<_, User>("SELECT id, username, email, password_hash, is_admin FROM users WHERE email = ?") .bind(&payload.email) .fetch_optional(&state.db) .await @@ -106,7 +112,7 @@ pub async fn update_profile( match result { Ok(_) => { - let user = sqlx::query_as::<_, User>("SELECT id, username, email, password_hash FROM users WHERE id = ?") + let user = sqlx::query_as::<_, User>("SELECT id, username, email, password_hash, is_admin FROM users WHERE id = ?") .bind(&user_id) .fetch_optional(&state.db) .await @@ -135,7 +141,7 @@ pub async fn me( None => return Err((StatusCode::UNAUTHORIZED, "Not logged in".to_string())), }; - let user = sqlx::query_as::<_, User>("SELECT id, username, email, password_hash FROM users WHERE id = ?") + let user = sqlx::query_as::<_, User>("SELECT id, username, email, password_hash, is_admin FROM users WHERE id = ?") .bind(&user_id) .fetch_optional(&state.db) .await @@ -159,7 +165,7 @@ pub async fn change_password( return Err((StatusCode::BAD_REQUEST, "Passwords cannot be empty".to_string())); } - let user = sqlx::query_as::<_, User>("SELECT id, username, email, password_hash FROM users WHERE id = ?") + let user = sqlx::query_as::<_, User>("SELECT id, username, email, password_hash, is_admin FROM users WHERE id = ?") .bind(&user_id) .fetch_optional(&state.db) .await diff --git a/server/src/db/postgres.rs b/server/src/db/postgres.rs index a84da5d..58fd871 100644 --- a/server/src/db/postgres.rs +++ b/server/src/db/postgres.rs @@ -6,7 +6,9 @@ pub async fn init_schema(pool: &AnyPool) { id TEXT PRIMARY KEY, username TEXT NOT NULL UNIQUE, email TEXT UNIQUE, - password_hash TEXT NOT NULL + password_hash TEXT NOT NULL, + is_admin BOOLEAN NOT NULL DEFAULT FALSE, + created_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS') )", "CREATE TABLE IF NOT EXISTS folders ( id TEXT PRIMARY KEY, @@ -83,12 +85,13 @@ pub async fn init_schema(pool: &AnyPool) { .expect("Failed to execute Postgres schema"); } - // Idempotent migration for existing databases with TIMESTAMP columns - sqlx::query("ALTER TABLE documents ADD COLUMN IF NOT EXISTS public_role TEXT") - .execute(pool) - .await - .unwrap_or_else(|e| { - eprintln!("Warning: public_role migration: {}", e); - Default::default() - }); + // Idempotent migrations for existing databases + let migrations = [ + "ALTER TABLE documents ADD COLUMN IF NOT EXISTS public_role TEXT", + "ALTER TABLE users ADD COLUMN IF NOT EXISTS is_admin BOOLEAN NOT NULL DEFAULT FALSE", + "ALTER TABLE users ADD COLUMN IF NOT EXISTS created_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS')", + ]; + for stmt in &migrations { + sqlx::query(stmt).execute(pool).await.unwrap_or_else(|_| Default::default()); + } } diff --git a/server/src/db/sqlite.rs b/server/src/db/sqlite.rs index b2279ab..1240f31 100644 --- a/server/src/db/sqlite.rs +++ b/server/src/db/sqlite.rs @@ -11,7 +11,9 @@ pub async fn init_schema(pool: &AnyPool) { id TEXT PRIMARY KEY, username TEXT NOT NULL UNIQUE, email TEXT UNIQUE, - password_hash TEXT NOT NULL + password_hash TEXT NOT NULL, + is_admin INTEGER NOT NULL DEFAULT 0, + created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now')) )", "CREATE TABLE IF NOT EXISTS folders ( id TEXT PRIMARY KEY, @@ -87,4 +89,13 @@ pub async fn init_schema(pool: &AnyPool) { .await .expect("Failed to execute SQLite schema"); } + + // Idempotent migrations for existing databases + let migrations = [ + "ALTER TABLE users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0", + "ALTER TABLE users ADD COLUMN created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now'))", + ]; + for stmt in &migrations { + let _ = sqlx::query(stmt).execute(pool).await; + } } diff --git a/server/src/main.rs b/server/src/main.rs index 0deb4d4..2789072 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -12,6 +12,7 @@ use tower_http::services::{ServeDir, ServeFile}; use tower_http::trace::TraceLayer; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +mod admin; mod auth; mod compiler; mod db; @@ -20,6 +21,7 @@ mod folders; mod files; mod handlers; mod models; +mod setup; mod world; mod collab; @@ -32,6 +34,7 @@ pub struct AppState { pub bcast_map: Arc>>>, pub db: AnyPool, pub key: Key, + pub registration_enabled: bool, } impl axum::extract::FromRef for Key { @@ -71,14 +74,22 @@ async fn main() { } }; + let registration_enabled = std::env::var("ALLOW_REGISTRATION") + .map(|v| v.to_lowercase() != "false") + .unwrap_or(true); + let state = AppState { compiler: Arc::new(Mutex::new(TypstCompiler::new())), bcast_map: Arc::new(Mutex::new(HashMap::new())), db, key, + registration_enabled, }; let api_routes = Router::new() + .route("/setup", get(setup::setup_status).post(setup::run_setup)) + .route("/admin/users", get(admin::list_users).post(admin::create_user)) + .route("/admin/users/{id}", patch(admin::update_user).delete(admin::delete_user)) .route("/compile", post(compile_handler)) .route("/export/{format}", post(export_handler)) .route("/export/pandoc/{format}", post(handlers::pandoc_export_handler)) diff --git a/server/src/models.rs b/server/src/models.rs index 1f2eebf..037375d 100644 --- a/server/src/models.rs +++ b/server/src/models.rs @@ -1,6 +1,19 @@ use serde::{Deserialize, Serialize}; use sqlx::FromRow; +// sqlx::Any maps SQLite INTEGER to i64 (BIGINT), not bool. +// These helpers let us store is_admin as i64 in DB-mapped structs +// while still serializing it as a JSON boolean for the frontend. +mod serde_i64_bool { + use serde::{Deserialize, Deserializer, Serializer}; + pub fn serialize(v: &i64, s: S) -> Result { + s.serialize_bool(*v != 0) + } + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result { + Ok(if bool::deserialize(d)? { 1 } else { 0 }) + } +} + #[derive(Debug, Serialize, Deserialize, FromRow)] pub struct User { pub id: String, @@ -8,6 +21,18 @@ pub struct User { pub email: String, #[serde(skip_serializing)] pub password_hash: String, + #[serde(with = "serde_i64_bool")] + pub is_admin: i64, +} + +#[derive(Debug, Serialize, Deserialize, FromRow)] +pub struct AdminUserView { + pub id: String, + pub username: String, + pub email: String, + #[serde(with = "serde_i64_bool")] + pub is_admin: i64, + pub created_at: String, } #[derive(Debug, Serialize, Deserialize, FromRow)] @@ -161,3 +186,31 @@ pub struct InviteRequest { pub email: String, pub role: String, } + +#[derive(Debug, Serialize, Deserialize)] +pub struct AdminCreateUserRequest { + pub username: String, + pub email: String, + pub password: String, + pub is_admin: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SetupRequest { + pub username: String, + pub email: String, + pub password: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SetupStatus { + pub needs_setup: bool, + pub registration_enabled: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct UpdateUserRequest { + pub is_admin: Option, + pub username: Option, + pub email: Option, +} diff --git a/server/src/setup.rs b/server/src/setup.rs new file mode 100644 index 0000000..50ca52f --- /dev/null +++ b/server/src/setup.rs @@ -0,0 +1,73 @@ +use axum::{extract::State, http::StatusCode, Json}; +use axum_extra::extract::cookie::{Cookie, SameSite, SignedCookieJar}; +use uuid::Uuid; + +use argon2::{ + password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, + Argon2, +}; + +use crate::{ + models::{SetupRequest, SetupStatus, User}, + AppState, +}; + +pub async fn setup_status( + State(state): State, +) -> Json { + let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users") + .fetch_one(&state.db) + .await + .unwrap_or((0,)); + + Json(SetupStatus { + needs_setup: count.0 == 0, + registration_enabled: state.registration_enabled, + }) +} + +pub async fn run_setup( + State(state): State, + jar: SignedCookieJar, + Json(payload): Json, +) -> Result<(SignedCookieJar, Json), (StatusCode, String)> { + let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users") + .fetch_one(&state.db) + .await + .unwrap_or((0,)); + + if count.0 > 0 { + return Err((StatusCode::FORBIDDEN, "Setup has already been completed".to_string())); + } + + if payload.username.is_empty() || payload.password.is_empty() || payload.email.is_empty() { + return Err((StatusCode::BAD_REQUEST, "Username, email, and password cannot be empty".to_string())); + } + + let salt = SaltString::generate(&mut OsRng); + let password_hash = Argon2::default() + .hash_password(payload.password.as_bytes(), &salt) + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? + .to_string(); + + let user_id = Uuid::new_v4().to_string(); + + let user = sqlx::query_as::<_, User>( + "INSERT INTO users (id, username, email, password_hash, is_admin) VALUES (?, ?, ?, ?, ?) RETURNING id, username, email, password_hash, is_admin" + ) + .bind(&user_id) + .bind(&payload.username) + .bind(&payload.email) + .bind(&password_hash) + .bind(1i64) + .fetch_one(&state.db) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + + let mut cookie = Cookie::new("session_user_id", user.id.clone()); + cookie.set_http_only(true); + cookie.set_same_site(SameSite::Lax); + cookie.set_path("/"); + + Ok((jar.add(cookie), Json(user))) +} diff --git a/src/lib/components/Toolbar.svelte b/src/lib/components/Toolbar.svelte index a30f0fe..fb7a40a 100644 --- a/src/lib/components/Toolbar.svelte +++ b/src/lib/components/Toolbar.svelte @@ -1,7 +1,7 @@ @@ -98,12 +106,14 @@ + {#if registrationEnabled}
New to TypstDrive? Create an account
+ {/if}