From 44e91a69735a632ded60c04e59729431ce013982 Mon Sep 17 00:00:00 2001 From: seb Date: Tue, 3 Nov 2026 03:54:49 +0100 Subject: [PATCH] u --- .env.example | 1 + docs/api/routes/auth/login.md | 5 +++-- src/api/routes/auth/login.js | 8 +++++--- src/db/autoinvite.js | 13 ++++++++++++- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index 41d9b4b..c3d16c0 100644 --- a/.env.example +++ b/.env.example @@ -17,6 +17,7 @@ CORS_ORIGINS=http://localhost:5173 # Auth — Google OAuth client ID and JWT signing secret GOOGLE_CLIENT_ID= JWT_SECRET=fallback_secret_please_change_in_production +# Auto-create user on first login (when false, only allowlisted or invited emails can sign in / create an account) IMPLICIT_CREATE=false # Database Provider — 'sqlite' or 'mssql' (defaults to 'mssql', or 'sqlite' if SQLITE_PATH is set) diff --git a/docs/api/routes/auth/login.md b/docs/api/routes/auth/login.md index 9017043..47ac3a0 100644 --- a/docs/api/routes/auth/login.md +++ b/docs/api/routes/auth/login.md @@ -4,7 +4,8 @@ Public. No cookie required. Login with Google ID token -Verifies a Google ID token, checks allowlist (or creates entry when `IMPLICIT_CREATE=true`), +Verifies a Google ID token, checks allowlist (or creates entry when `IMPLICIT_CREATE=true` +or when the email has a pending auto-invitation), stores Google `sub` and profile image bytes, and sets an HTTP-only `token` cookie. `user.picture` is the profile image as base64 (same as GET /auth/me). @@ -43,7 +44,7 @@ Login successful; `token` cookie set |--------|-------------| | 400 | Missing token or Google client not configured — `{"error":"…"}` | | 401 | Invalid Google token or account — `{"error":"…"}` | -| 403 | Email not on allowlist — `{"error":"…"}` | +| 403 | Email not on allowlist or invited — `{"error":"…"}` | | 500 | Server error — `{"error":"…"}` | --- diff --git a/src/api/routes/auth/login.js b/src/api/routes/auth/login.js index 6f46743..3e444c6 100644 --- a/src/api/routes/auth/login.js +++ b/src/api/routes/auth/login.js @@ -2,6 +2,7 @@ import jwt from 'jsonwebtoken'; import { OAuth2Client } from 'google-auth-library'; import env from '../../../config/env.js'; import * as db from '../../../db/googleAuth.js'; +import * as autoinviteDb from '../../../db/autoinvite.js'; import { clearCachedUser } from '../../middleware/auth.js'; import { downloadPicture, toPictureBuffer } from '../../../util/pictureBinary.js'; @@ -20,7 +21,8 @@ export function register(app, basePath = '') { * tags: [Auth] * summary: Login with Google ID token * description: | - * Verifies a Google ID token, checks allowlist (or creates entry when `IMPLICIT_CREATE=true`), + * Verifies a Google ID token, checks allowlist (or creates entry when `IMPLICIT_CREATE=true` + * or when the email has a pending auto-invitation), * stores Google `sub` and profile image bytes, and sets an HTTP-only `token` cookie. * `user.picture` is the profile image as base64 (same as GET /auth/me). * security: [] @@ -64,7 +66,7 @@ export function register(app, basePath = '') { * schema: * $ref: '#/components/schemas/Error' * 403: - * description: Email not on allowlist + * description: Email not on allowlist or invited * content: * application/json: * schema: @@ -104,7 +106,7 @@ export function register(app, basePath = '') { } let allowed = await db.getAllowedEmail(email); - if (!allowed && env.implicitCreate) { + if (!allowed && (env.implicitCreate || (await autoinviteDb.hasPendingInvite(email)))) { await db.addAllowedEmail(email); allowed = await db.getAllowedEmail(email); } diff --git a/src/db/autoinvite.js b/src/db/autoinvite.js index d0e4a3e..5541ee0 100644 --- a/src/db/autoinvite.js +++ b/src/db/autoinvite.js @@ -1,4 +1,4 @@ -import { getStore } from './store.js'; +import { getStore, getProvider } from './store.js'; export async function list(googleAuthId) { const store = await getStore(); @@ -14,3 +14,14 @@ export async function remove(googleAuthId, id) { const store = await getStore(); return store.autoinvite.remove(googleAuthId, id); } + +export async function hasPendingInvite(email) { + if (!email || typeof email !== 'string') return false; + const normalized = email.trim().toLowerCase(); + const provider = await getProvider(); + const result = await provider.query( + 'SELECT 1 AS found FROM autoinvite WHERE email = @email', + { email: normalized } + ); + return result.rows.length > 0; +}