mirror of
https://github.com/bretzel-app/crumbs.git
synced 2026-10-12 20:06:57 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c2028e187 | ||
|
|
cb9857324a | ||
|
|
e90c9c041c | ||
|
|
d9b3bd9ff6 | ||
|
|
8ec278c49a | ||
|
|
4a280b5932 | ||
|
|
7297dc0616 | ||
|
|
4e2e0353df |
@@ -215,5 +215,6 @@ Keep docs in `docs/` up to date when making changes. If a feature, API endpoint,
|
||||
|-----|--------|
|
||||
| `docs/FEATURES.md` | User-facing feature list |
|
||||
| `docs/ARCHITECTURE.md` | System design, data flow, schema, tech stack |
|
||||
| `docs/AUTH.md` | Authentication setup, OAuth/SSO provider guides (Authentik, Keycloak, etc.) |
|
||||
| `docs/DEPLOYMENT.md` | Docker, env vars, reverse proxy, backups |
|
||||
| `docs/openapi.yaml` | API spec (run `pnpm docs:api` to regenerate `docs/API.md`) |
|
||||
|
||||
@@ -21,7 +21,7 @@ docker compose up -d
|
||||
|
||||
Open http://localhost:3000 and create your admin account on first visit.
|
||||
|
||||
#### With OAuth (optional)
|
||||
#### With SSO (optional)
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
@@ -34,14 +34,18 @@ services:
|
||||
- crumbs-data:/data
|
||||
environment:
|
||||
- ORIGIN=https://notes.example.com
|
||||
- AUTH_GOOGLE_ID=your-google-client-id
|
||||
- AUTH_GOOGLE_SECRET=your-google-client-secret
|
||||
- AUTH_OIDC_ISSUER=https://authentik.example.com/application/o/crumbs/
|
||||
- AUTH_OIDC_CLIENT_ID=your-client-id
|
||||
- AUTH_OIDC_CLIENT_SECRET=your-client-secret
|
||||
- AUTH_OIDC_DISPLAY_NAME=Authentik
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
crumbs-data:
|
||||
```
|
||||
|
||||
Google and GitHub OAuth are also supported. See [docs/AUTH.md](docs/AUTH.md) for all providers and setup guides.
|
||||
|
||||
### Manual (Node.js)
|
||||
|
||||
```bash
|
||||
@@ -99,6 +103,7 @@ See [docs/FEATURES.md](docs/FEATURES.md) for detailed feature documentation.
|
||||
|----------|-------------|
|
||||
| [Features](docs/FEATURES.md) | Detailed feature list and behavior |
|
||||
| [Architecture](docs/ARCHITECTURE.md) | Local-first sync design, tech rationale, DB schema |
|
||||
| [Auth](docs/AUTH.md) | Password auth, OAuth/SSO setup (Authentik, Keycloak, etc.) |
|
||||
| [Deployment](docs/DEPLOYMENT.md) | Docker, Node.js, reverse proxy, backups, env vars |
|
||||
| [API](docs/API.md) | REST API reference (auto-generated) |
|
||||
| [Contributing](CONTRIBUTING.md) | How to contribute |
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
crumbs:
|
||||
image: ghcr.io/bretzel-app/crumbs:0.9.0
|
||||
image: ghcr.io/bretzel-app/crumbs:0.10.0
|
||||
container_name: crumbs
|
||||
ports:
|
||||
- "3000:3000"
|
||||
|
||||
@@ -83,9 +83,8 @@ For a single-user app with multiple devices:
|
||||
- New v4 uses Vite plugin (faster builds)
|
||||
|
||||
### Argon2
|
||||
- Current best practice for password hashing
|
||||
- Memory-hard (resists GPU attacks)
|
||||
- Winner of the Password Hashing Competition
|
||||
- Memory-hard password hashing (resists GPU attacks)
|
||||
- See [AUTH.md](AUTH.md) for authentication details
|
||||
|
||||
## Database Schema
|
||||
|
||||
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
# Authentication
|
||||
|
||||
Crumbs supports password-based authentication and optional OAuth/SSO via Google, GitHub, or any OpenID Connect (OIDC) provider (e.g. Authentik, Keycloak, Okta, Auth0).
|
||||
|
||||
## First-run setup
|
||||
|
||||
On first launch, Crumbs presents a setup screen to create the initial admin account with email and password (minimum 8 characters). This account becomes the instance administrator.
|
||||
|
||||
## Password authentication
|
||||
|
||||
- Passwords are hashed with **Argon2** (argon2id variant)
|
||||
- Sessions use httpOnly cookies with 30-day expiry
|
||||
- Admins can reset passwords and revoke sessions for any user via Settings
|
||||
|
||||
## OAuth / SSO
|
||||
|
||||
OAuth providers are **auto-enabled** when their environment variables are set — no code changes needed. The login page dynamically shows buttons for each configured provider.
|
||||
|
||||
### How it works
|
||||
|
||||
1. User clicks an OAuth button on the login page
|
||||
2. Crumbs redirects to the provider's authorization endpoint (with PKCE)
|
||||
3. Provider authenticates the user and redirects back to Crumbs
|
||||
4. Crumbs validates the response, extracts the user's email and name
|
||||
5. If a matching user exists (by provider ID or email), a session is created
|
||||
6. If no matching user exists, the login is rejected (invite-only model)
|
||||
|
||||
### Invite-only model
|
||||
|
||||
OAuth does **not** auto-create accounts. Users must be pre-created by an admin:
|
||||
|
||||
1. Go to **Settings > Users > Invite User**
|
||||
2. Create the user with the **same email** they use on the OAuth provider
|
||||
3. The user can now log in via OAuth — their account is automatically linked on first sign-in
|
||||
|
||||
After initial linking, the user is identified by their provider-specific ID (`sub` claim), so email changes on the provider side won't break login.
|
||||
|
||||
### Environment variables
|
||||
|
||||
#### Google OAuth
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `AUTH_GOOGLE_CLIENT_ID` | Google OAuth 2.0 client ID |
|
||||
| `AUTH_GOOGLE_CLIENT_SECRET` | Google OAuth 2.0 client secret |
|
||||
|
||||
**Callback URL:** `https://<your-domain>/api/auth/oauth/google/callback`
|
||||
|
||||
Setup:
|
||||
1. Go to [Google Cloud Console](https://console.cloud.google.com/apis/credentials)
|
||||
2. Create an OAuth 2.0 Client ID (Web application)
|
||||
3. Add the callback URL above as an authorized redirect URI
|
||||
4. Copy the client ID and secret into your environment
|
||||
|
||||
#### GitHub OAuth
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `AUTH_GITHUB_CLIENT_ID` | GitHub OAuth app client ID |
|
||||
| `AUTH_GITHUB_CLIENT_SECRET` | GitHub OAuth app client secret |
|
||||
|
||||
**Callback URL:** `https://<your-domain>/api/auth/oauth/github/callback`
|
||||
|
||||
Setup:
|
||||
1. Go to [GitHub Developer Settings](https://github.com/settings/developers)
|
||||
2. Create a new OAuth App
|
||||
3. Set the authorization callback URL to the callback URL above
|
||||
4. Copy the client ID and secret into your environment
|
||||
|
||||
#### Generic OIDC
|
||||
|
||||
Any OpenID Connect-compliant provider works via the generic OIDC integration. Crumbs automatically discovers endpoints via the provider's `/.well-known/openid-configuration` document.
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `AUTH_OIDC_ISSUER` | OIDC issuer URL (must serve `/.well-known/openid-configuration`) |
|
||||
| `AUTH_OIDC_CLIENT_ID` | OIDC client ID |
|
||||
| `AUTH_OIDC_CLIENT_SECRET` | OIDC client secret |
|
||||
| `AUTH_OIDC_DISPLAY_NAME` | Button label on login page (default: `SSO`) |
|
||||
|
||||
**Callback URL:** `https://<your-domain>/api/auth/oauth/oidc/callback`
|
||||
|
||||
**Required scopes:** `openid`, `profile`, `email`
|
||||
|
||||
### Provider guides
|
||||
|
||||
#### Authentik
|
||||
|
||||
[Authentik](https://goauthentik.io/) is a self-hosted identity provider that works with Crumbs via the generic OIDC integration.
|
||||
|
||||
**1. Create a provider in Authentik:**
|
||||
|
||||
1. Open the Authentik admin panel
|
||||
2. Go to **Applications > Providers > Create**
|
||||
3. Select **OAuth2/OpenID Connect**
|
||||
4. Configure:
|
||||
- **Name:** `Crumbs`
|
||||
- **Authorization flow:** `default-provider-authorization-implicit-consent` (or `explicit-consent` to prompt users)
|
||||
- **Client type:** `Confidential`
|
||||
- **Redirect URIs:** `https://<your-crumbs-domain>/api/auth/oauth/oidc/callback`
|
||||
- **Scopes:** `openid`, `profile`, `email`
|
||||
5. Save and note the **Client ID** and **Client Secret**
|
||||
|
||||
**2. Create an application in Authentik:**
|
||||
|
||||
1. Go to **Applications > Applications > Create**
|
||||
2. Configure:
|
||||
- **Name:** `Crumbs`
|
||||
- **Slug:** `crumbs`
|
||||
- **Provider:** Select the `Crumbs` provider you just created
|
||||
- **Launch URL:** `https://<your-crumbs-domain>` (optional, for Authentik's app dashboard)
|
||||
3. Save
|
||||
|
||||
**3. Configure Crumbs:**
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
crumbs:
|
||||
image: ghcr.io/bretzel-app/crumbs:latest
|
||||
environment:
|
||||
- ORIGIN=https://notes.example.com
|
||||
- AUTH_OIDC_ISSUER=https://authentik.example.com/application/o/crumbs/
|
||||
- AUTH_OIDC_CLIENT_ID=<client-id>
|
||||
- AUTH_OIDC_CLIENT_SECRET=<client-secret>
|
||||
- AUTH_OIDC_DISPLAY_NAME=Authentik
|
||||
```
|
||||
|
||||
> **Note:** The issuer URL for Authentik follows the pattern `https://<authentik-domain>/application/o/<app-slug>/`. This URL must serve a valid `/.well-known/openid-configuration` document — you can verify by visiting `https://<authentik-domain>/application/o/crumbs/.well-known/openid-configuration` in your browser.
|
||||
|
||||
**4. Pre-create users:**
|
||||
|
||||
In Crumbs, go to **Settings > Users > Invite User** and create accounts with emails matching your Authentik users. They can then log in via the "Authentik" button on the login page.
|
||||
|
||||
#### Keycloak
|
||||
|
||||
1. Create a new client in your Keycloak realm
|
||||
2. Set **Client type** to `OpenID Connect`, **Client authentication** to `On`
|
||||
3. Add the callback URL: `https://<your-crumbs-domain>/api/auth/oauth/oidc/callback`
|
||||
4. Set the issuer URL to `https://<keycloak-domain>/realms/<realm-name>`
|
||||
|
||||
```env
|
||||
AUTH_OIDC_ISSUER=https://keycloak.example.com/realms/myrealm
|
||||
AUTH_OIDC_CLIENT_ID=crumbs
|
||||
AUTH_OIDC_CLIENT_SECRET=<client-secret>
|
||||
AUTH_OIDC_DISPLAY_NAME=Keycloak
|
||||
```
|
||||
|
||||
#### Okta / Auth0
|
||||
|
||||
Use the issuer URL from your Okta org or Auth0 tenant:
|
||||
|
||||
```env
|
||||
# Okta
|
||||
AUTH_OIDC_ISSUER=https://your-org.okta.com
|
||||
|
||||
# Auth0
|
||||
AUTH_OIDC_ISSUER=https://your-tenant.auth0.com
|
||||
```
|
||||
|
||||
### Docker example with multiple providers
|
||||
|
||||
```yaml
|
||||
services:
|
||||
crumbs:
|
||||
image: ghcr.io/bretzel-app/crumbs:latest
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- crumbs-data:/data
|
||||
environment:
|
||||
- ORIGIN=https://notes.example.com
|
||||
# Google
|
||||
- AUTH_GOOGLE_CLIENT_ID=123456789.apps.googleusercontent.com
|
||||
- AUTH_GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxx
|
||||
# GitHub
|
||||
- AUTH_GITHUB_CLIENT_ID=Iv1.abc123
|
||||
- AUTH_GITHUB_CLIENT_SECRET=abc123secret
|
||||
# Authentik (OIDC)
|
||||
- AUTH_OIDC_ISSUER=https://authentik.example.com/application/o/crumbs/
|
||||
- AUTH_OIDC_CLIENT_ID=crumbs-client-id
|
||||
- AUTH_OIDC_CLIENT_SECRET=crumbs-client-secret
|
||||
- AUTH_OIDC_DISPLAY_NAME=Authentik
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
crumbs-data:
|
||||
```
|
||||
|
||||
All configured providers will appear as buttons on the login page.
|
||||
|
||||
## Security notes
|
||||
|
||||
- All OAuth flows use **PKCE** (Proof Key for Code Exchange) with S256 challenge method
|
||||
- OAuth state and code verifier are stored as httpOnly cookies (10-minute expiry)
|
||||
- Passwords are hashed with **Argon2** (never stored in plain text)
|
||||
- Sessions expire after **30 days**
|
||||
- The `ORIGIN` environment variable must match your actual domain for CSRF protection
|
||||
- Always use **HTTPS** in production (via reverse proxy)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| OAuth button doesn't appear | Provider env vars not set or missing a required variable | Check all required env vars are set and restart |
|
||||
| "No account found" after OAuth | User doesn't exist in Crumbs | Create the user via Settings with matching email |
|
||||
| OIDC discovery fails | Issuer URL is wrong or unreachable | Verify `AUTH_OIDC_ISSUER` serves `/.well-known/openid-configuration` |
|
||||
| Redirect URI mismatch | Callback URL in provider doesn't match `ORIGIN` | Ensure `ORIGIN` matches your domain and the callback URL uses the same origin |
|
||||
| PKCE error | Provider doesn't support S256 PKCE | Most modern providers support it; check provider docs |
|
||||
+4
-62
@@ -110,48 +110,9 @@ node build
|
||||
| `PORT` | `3000` | HTTP port |
|
||||
| `NODE_ENV` | `development` | Set to `production` for deployment |
|
||||
|
||||
### OAuth Providers (optional)
|
||||
### OAuth / SSO (optional)
|
||||
|
||||
Enable social login by setting the client ID and secret for any provider. Users who sign in via OAuth are created automatically (invite-only when combined with admin approval).
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `AUTH_GOOGLE_ID` | Google OAuth client ID |
|
||||
| `AUTH_GOOGLE_SECRET` | Google OAuth client secret |
|
||||
| `AUTH_GITHUB_ID` | GitHub OAuth client ID |
|
||||
| `AUTH_GITHUB_SECRET` | GitHub OAuth client secret |
|
||||
| `AUTH_OIDC_ISSUER` | Generic OIDC issuer URL (e.g. `https://accounts.example.com`) |
|
||||
| `AUTH_OIDC_ID` | OIDC client ID |
|
||||
| `AUTH_OIDC_SECRET` | OIDC client secret |
|
||||
| `AUTH_OIDC_NAME` | Display name for the OIDC button (default: "SSO") |
|
||||
|
||||
**Callback URLs** — register these in your provider's dashboard:
|
||||
|
||||
| Provider | Callback URL |
|
||||
|----------|-------------|
|
||||
| Google | `https://your-domain/api/auth/callback/google` |
|
||||
| GitHub | `https://your-domain/api/auth/callback/github` |
|
||||
| OIDC | `https://your-domain/api/auth/callback/oidc` |
|
||||
|
||||
#### Docker example with Google OAuth
|
||||
|
||||
```yaml
|
||||
services:
|
||||
crumbs:
|
||||
image: ghcr.io/bretzel-app/crumbs:latest
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- crumbs-data:/data
|
||||
environment:
|
||||
- ORIGIN=https://notes.example.com
|
||||
- AUTH_GOOGLE_ID=123456789.apps.googleusercontent.com
|
||||
- AUTH_GOOGLE_SECRET=GOCSPX-xxxxxxxxxxxxx
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
crumbs-data:
|
||||
```
|
||||
Google, GitHub, and generic OIDC (Authentik, Keycloak, Okta, etc.) are supported. Providers are auto-enabled when their env vars are set. See **[AUTH.md](AUTH.md)** for environment variables, callback URLs, provider-specific setup guides, and troubleshooting.
|
||||
|
||||
## Health Check
|
||||
|
||||
@@ -163,7 +124,7 @@ docker inspect --format='{{.State.Health.Status}}' crumbs
|
||||
|
||||
## MCP Server Configuration
|
||||
|
||||
Crumbs includes a built-in MCP (Model Context Protocol) server that allows AI assistants to interact with your notes.
|
||||
Crumbs includes a built-in MCP (Model Context Protocol) server that allows AI assistants to interact with your notes. See [ARCHITECTURE.md](ARCHITECTURE.md#mcp-server) for technical details and the full tool list.
|
||||
|
||||
### Setup
|
||||
|
||||
@@ -186,28 +147,9 @@ Crumbs includes a built-in MCP (Model Context Protocol) server that allows AI as
|
||||
}
|
||||
```
|
||||
|
||||
### Available Tools
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `list_notes` | List notes (filter by status/tag) |
|
||||
| `get_note` | Get a note by ID |
|
||||
| `create_note` | Create a new note |
|
||||
| `update_note` | Update a note |
|
||||
| `trash_note` / `restore_note` | Move to/from trash |
|
||||
| `archive_note` / `unarchive_note` | Archive/unarchive |
|
||||
| `delete_note` | Permanently delete |
|
||||
| `search_notes` | Full-text search |
|
||||
| `list_tags` | List all tags |
|
||||
| `pin_note` | Pin/unpin a note |
|
||||
| `reorder_notes` | Set sort orders |
|
||||
| `upload_image` | Attach image from URL |
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Always use HTTPS in production (via reverse proxy)
|
||||
- Set a strong password (min 8 characters recommended)
|
||||
- The `ORIGIN` variable must match your actual domain for CSRF protection
|
||||
- Sessions expire after 30 days
|
||||
- Passwords are hashed with Argon2
|
||||
- API keys are SHA-256 hashed (never stored in plain text)
|
||||
- See [AUTH.md](AUTH.md#security-notes) for authentication security details (password hashing, session expiry, PKCE)
|
||||
|
||||
+6
-14
@@ -86,20 +86,12 @@
|
||||
- Clear search to restore original view
|
||||
- Case-insensitive matching
|
||||
|
||||
### Authentication
|
||||
- Multi-user support with role-based access (admin / user)
|
||||
- First-run setup: create admin account (min 8 chars password)
|
||||
- Argon2 password hashing (industry standard)
|
||||
- Session-based auth with 30-day expiry
|
||||
- Active session management (view, revoke individual or all other sessions)
|
||||
- All routes protected except /login and /setup
|
||||
- Account deletion with password confirmation
|
||||
|
||||
### OAuth / SSO
|
||||
- Optional social login via Google, GitHub, or generic OIDC
|
||||
- OAuth users created automatically on first sign-in
|
||||
- Invite-only mode when combined with admin controls
|
||||
- Configure via environment variables (`AUTH_GOOGLE_ID`, `AUTH_GITHUB_ID`, `AUTH_OIDC_ISSUER`, etc.)
|
||||
### Authentication & OAuth / SSO
|
||||
- Multi-user with role-based access (admin / user)
|
||||
- Password auth (Argon2) + optional OAuth/SSO (Google, GitHub, any OIDC provider)
|
||||
- Session-based auth with active session management
|
||||
- Invite-only OAuth model with PKCE-secured flows
|
||||
- See [AUTH.md](AUTH.md) for full details and provider setup guides
|
||||
|
||||
### User Management (admin)
|
||||
- Admin dashboard to create, list, and delete users
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "crumbs",
|
||||
"private": true,
|
||||
"version": "0.9.1",
|
||||
"version": "0.10.1",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"author": "Bretzel (https://bretzel.app)",
|
||||
|
||||
@@ -194,10 +194,10 @@ export function revokeAllSessions(userId: number, exceptToken?: string): void {
|
||||
export async function createUser(
|
||||
email: string,
|
||||
displayName: string,
|
||||
password: string,
|
||||
password: string | null,
|
||||
role: 'admin' | 'user' = 'user'
|
||||
): Promise<User> {
|
||||
const hash = await argon2.hash(password);
|
||||
const hash = password ? await argon2.hash(password) : null;
|
||||
const result = db
|
||||
.insert(users)
|
||||
.values({
|
||||
@@ -205,7 +205,7 @@ export async function createUser(
|
||||
displayName,
|
||||
role,
|
||||
passwordHash: hash,
|
||||
authProvider: 'password',
|
||||
authProvider: password ? 'password' : 'none',
|
||||
createdAt: new Date()
|
||||
})
|
||||
.returning()
|
||||
|
||||
@@ -201,6 +201,29 @@ try {
|
||||
// Column already exists
|
||||
}
|
||||
|
||||
// Migration: make password_hash nullable (for OAuth-only users)
|
||||
const pwCol = (
|
||||
sqlite.prepare("PRAGMA table_info('users')").all() as { name: string; notnull: number }[]
|
||||
).find((col) => col.name === 'password_hash');
|
||||
if (pwCol && pwCol.notnull) {
|
||||
sqlite.exec(`
|
||||
CREATE TABLE __new_users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL DEFAULT '',
|
||||
display_name TEXT NOT NULL DEFAULT '',
|
||||
role TEXT NOT NULL DEFAULT 'user',
|
||||
password_hash TEXT,
|
||||
auth_provider TEXT NOT NULL DEFAULT 'password',
|
||||
provider_id TEXT,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
INSERT INTO __new_users(id, email, display_name, role, password_hash, auth_provider, provider_id, created_at)
|
||||
SELECT id, email, display_name, role, password_hash, auth_provider, provider_id, created_at FROM users;
|
||||
DROP TABLE users;
|
||||
ALTER TABLE __new_users RENAME TO users;
|
||||
`);
|
||||
}
|
||||
|
||||
// Re-enable foreign keys
|
||||
sqlite.pragma('foreign_keys = ON');
|
||||
|
||||
|
||||
@@ -11,17 +11,23 @@
|
||||
let newDisplayName = $state('');
|
||||
let newPassword = $state('');
|
||||
let newRole = $state<'admin' | 'user'>('user');
|
||||
let oauthOnly = $state(false);
|
||||
let createMsg = $state('');
|
||||
let createError = $state(false);
|
||||
|
||||
async function createUser() {
|
||||
createMsg = '';
|
||||
if (!newEmail || !newPassword) {
|
||||
createMsg = 'Email and password are required';
|
||||
if (!newEmail) {
|
||||
createMsg = 'Email is required';
|
||||
createError = true;
|
||||
return;
|
||||
}
|
||||
if (newPassword.length < 8) {
|
||||
if (!oauthOnly && !newPassword) {
|
||||
createMsg = 'Password is required (or enable OAuth-only)';
|
||||
createError = true;
|
||||
return;
|
||||
}
|
||||
if (!oauthOnly && newPassword.length < 8) {
|
||||
createMsg = 'Password must be at least 8 characters';
|
||||
createError = true;
|
||||
return;
|
||||
@@ -33,7 +39,7 @@
|
||||
body: JSON.stringify({
|
||||
email: newEmail,
|
||||
displayName: newDisplayName || newEmail.split('@')[0],
|
||||
password: newPassword,
|
||||
password: oauthOnly ? undefined : newPassword,
|
||||
role: newRole
|
||||
})
|
||||
});
|
||||
@@ -44,6 +50,7 @@
|
||||
newDisplayName = '';
|
||||
newPassword = '';
|
||||
newRole = 'user';
|
||||
oauthOnly = false;
|
||||
createMsg = 'User created';
|
||||
createError = false;
|
||||
} else {
|
||||
@@ -125,13 +132,19 @@
|
||||
placeholder="Display name (optional)"
|
||||
class="w-full rounded-sm border border-[var(--border-subtle)] bg-[var(--bg-base)] px-4 py-2 text-sm text-[var(--text)] outline-none placeholder:text-[var(--text-muted)] focus:border-[var(--primary)]"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
bind:value={newPassword}
|
||||
placeholder="Password (min 8 characters)"
|
||||
class="w-full rounded-sm border border-[var(--border-subtle)] bg-[var(--bg-base)] px-4 py-2 text-sm text-[var(--text)] outline-none placeholder:text-[var(--text-muted)] focus:border-[var(--primary)]"
|
||||
/>
|
||||
<PasswordStrengthMeter password={newPassword} />
|
||||
<label class="flex items-center gap-2 text-sm text-[var(--text)]">
|
||||
<input type="checkbox" bind:checked={oauthOnly} />
|
||||
OAuth-only (no password)
|
||||
</label>
|
||||
{#if !oauthOnly}
|
||||
<input
|
||||
type="password"
|
||||
bind:value={newPassword}
|
||||
placeholder="Password (min 8 characters)"
|
||||
class="w-full rounded-sm border border-[var(--border-subtle)] bg-[var(--bg-base)] px-4 py-2 text-sm text-[var(--text)] outline-none placeholder:text-[var(--text-muted)] focus:border-[var(--primary)]"
|
||||
/>
|
||||
<PasswordStrengthMeter password={newPassword} />
|
||||
{/if}
|
||||
<select
|
||||
bind:value={newRole}
|
||||
class="w-full rounded-sm border border-[var(--border-subtle)] bg-[var(--bg-base)] px-4 py-2 text-sm text-[var(--text)] outline-none focus:border-[var(--primary)]"
|
||||
|
||||
@@ -12,14 +12,14 @@ export const POST: RequestHandler = async ({ request, ...event }) => {
|
||||
requireAdmin(event);
|
||||
const { email, displayName, password, role } = await request.json();
|
||||
|
||||
if (!email || !password) {
|
||||
throw error(400, 'Email and password are required');
|
||||
if (!email) {
|
||||
throw error(400, 'Email is required');
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
if (password && password.length < 8) {
|
||||
throw error(400, 'Password must be at least 8 characters');
|
||||
}
|
||||
|
||||
const user = await createUser(email, displayName || email.split('@')[0], password, role || 'user');
|
||||
const user = await createUser(email, displayName || email.split('@')[0], password || null, role || 'user');
|
||||
return json(user, { status: 201 });
|
||||
};
|
||||
|
||||
+1
-1
@@ -249,7 +249,7 @@
|
||||
<li><a href="https://github.com/bretzel-app/crumbs/blob/main/docs/DEPLOYMENT.md" target="_blank" rel="noopener">Docs</a></li>
|
||||
<li><a href="https://github.com/bretzel-app/crumbs/blob/main/LICENSE" target="_blank" rel="noopener">License</a></li>
|
||||
</ul>
|
||||
<span class="footer-version">v0.9.1</span>
|
||||
<span class="footer-version">v0.10.1</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user