This commit is contained in:
seb
2026-11-03 03:54:49 +01:00
parent de2957acf1
commit 44e91a6973
4 changed files with 21 additions and 6 deletions

View File

@@ -17,6 +17,7 @@ CORS_ORIGINS=http://localhost:5173
# Auth — Google OAuth client ID and JWT signing secret # Auth — Google OAuth client ID and JWT signing secret
GOOGLE_CLIENT_ID= GOOGLE_CLIENT_ID=
JWT_SECRET=fallback_secret_please_change_in_production 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 IMPLICIT_CREATE=false
# Database Provider — 'sqlite' or 'mssql' (defaults to 'mssql', or 'sqlite' if SQLITE_PATH is set) # Database Provider — 'sqlite' or 'mssql' (defaults to 'mssql', or 'sqlite' if SQLITE_PATH is set)

View File

@@ -4,7 +4,8 @@ Public. No cookie required.
Login with Google ID token 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. 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). `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":"…"}` | | 400 | Missing token or Google client not configured — `{"error":"…"}` |
| 401 | Invalid Google token or account — `{"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":"…"}` | | 500 | Server error — `{"error":"…"}` |
--- ---

View File

@@ -2,6 +2,7 @@ import jwt from 'jsonwebtoken';
import { OAuth2Client } from 'google-auth-library'; import { OAuth2Client } from 'google-auth-library';
import env from '../../../config/env.js'; import env from '../../../config/env.js';
import * as db from '../../../db/googleAuth.js'; import * as db from '../../../db/googleAuth.js';
import * as autoinviteDb from '../../../db/autoinvite.js';
import { clearCachedUser } from '../../middleware/auth.js'; import { clearCachedUser } from '../../middleware/auth.js';
import { downloadPicture, toPictureBuffer } from '../../../util/pictureBinary.js'; import { downloadPicture, toPictureBuffer } from '../../../util/pictureBinary.js';
@@ -20,7 +21,8 @@ export function register(app, basePath = '') {
* tags: [Auth] * tags: [Auth]
* summary: Login with Google ID token * summary: Login with Google ID token
* description: | * 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. * 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). * `user.picture` is the profile image as base64 (same as GET /auth/me).
* security: [] * security: []
@@ -64,7 +66,7 @@ export function register(app, basePath = '') {
* schema: * schema:
* $ref: '#/components/schemas/Error' * $ref: '#/components/schemas/Error'
* 403: * 403:
* description: Email not on allowlist * description: Email not on allowlist or invited
* content: * content:
* application/json: * application/json:
* schema: * schema:
@@ -104,7 +106,7 @@ export function register(app, basePath = '') {
} }
let allowed = await db.getAllowedEmail(email); let allowed = await db.getAllowedEmail(email);
if (!allowed && env.implicitCreate) { if (!allowed && (env.implicitCreate || (await autoinviteDb.hasPendingInvite(email)))) {
await db.addAllowedEmail(email); await db.addAllowedEmail(email);
allowed = await db.getAllowedEmail(email); allowed = await db.getAllowedEmail(email);
} }

View File

@@ -1,4 +1,4 @@
import { getStore } from './store.js'; import { getStore, getProvider } from './store.js';
export async function list(googleAuthId) { export async function list(googleAuthId) {
const store = await getStore(); const store = await getStore();
@@ -14,3 +14,14 @@ export async function remove(googleAuthId, id) {
const store = await getStore(); const store = await getStore();
return store.autoinvite.remove(googleAuthId, id); 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;
}