feat(auth): replace email/password registration with OAuth2 (Google + GitHub)
Some checks failed
CI / Test backend (pull_request) Successful in 19s
Release / Test backend (push) Successful in 18s
CI / Check ui (pull_request) Successful in 41s
Release / Check ui (push) Successful in 21s
CI / Docker / backend (pull_request) Successful in 1m43s
CI / Docker / runner (pull_request) Successful in 1m28s
Release / Docker / backend (push) Successful in 1m40s
CI / Docker / caddy (pull_request) Successful in 6m45s
Release / Docker / runner (push) Successful in 1m48s
Release / Docker / caddy (push) Successful in 7m12s
CI / Docker / ui (pull_request) Successful in 1m20s
Release / Docker / ui (push) Successful in 1m19s
Release / Gitea Release (push) Failing after 2s
Some checks failed
CI / Test backend (pull_request) Successful in 19s
Release / Test backend (push) Successful in 18s
CI / Check ui (pull_request) Successful in 41s
Release / Check ui (push) Successful in 21s
CI / Docker / backend (pull_request) Successful in 1m43s
CI / Docker / runner (pull_request) Successful in 1m28s
Release / Docker / backend (push) Successful in 1m40s
CI / Docker / caddy (pull_request) Successful in 6m45s
Release / Docker / runner (push) Successful in 1m48s
Release / Docker / caddy (push) Successful in 7m12s
CI / Docker / ui (pull_request) Successful in 1m20s
Release / Docker / ui (push) Successful in 1m19s
Release / Gitea Release (push) Failing after 2s
- New /auth/[provider] route: generates state cookie, redirects to provider - New /auth/[provider]/callback: exchanges code, fetches profile, auto-creates or links account, sets auth cookie - pocketbase.ts: add oauth_provider/oauth_id to User; new getUserByOAuth(), createOAuthUser(), linkOAuthToUser() helpers; loginUser() drops email_verified gate - pb-init-v3.sh: add oauth_provider + oauth_id fields (schema + migration) - docker-compose.yml: GOOGLE/GITHUB client ID/secret env vars (replaces SMTP vars) - Login page: two OAuth buttons (Google, GitHub) — register form removed - /verify-email route and email.ts removed (provider handles email verification) - /api/auth/register returns 410 (OAuth-only from now on)
This commit is contained in:
@@ -67,6 +67,8 @@ export interface User {
|
||||
email_verified?: boolean;
|
||||
verification_token?: string;
|
||||
verification_token_exp?: string;
|
||||
oauth_provider?: string;
|
||||
oauth_id?: string;
|
||||
}
|
||||
|
||||
// ─── Auth token cache ─────────────────────────────────────────────────────────
|
||||
@@ -496,8 +498,75 @@ export async function getUserByEmail(email: string): Promise<User | null> {
|
||||
return listOne<User>('app_users', `email="${email.replace(/"/g, '\\"')}"`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a user by OAuth provider + provider user ID. Returns null if not found.
|
||||
*/
|
||||
export async function getUserByOAuth(provider: string, oauthId: string): Promise<User | null> {
|
||||
return listOne<User>(
|
||||
'app_users',
|
||||
`oauth_provider="${provider.replace(/"/g, '\\"')}"&&oauth_id="${oauthId.replace(/"/g, '\\"')}"`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new user via OAuth (no password). email_verified is true since the
|
||||
* provider already verified it. Throws on DB errors.
|
||||
*/
|
||||
export async function createOAuthUser(
|
||||
username: string,
|
||||
email: string,
|
||||
provider: string,
|
||||
oauthId: string,
|
||||
avatarUrl?: string,
|
||||
role = 'user'
|
||||
): Promise<User> {
|
||||
log.info('pocketbase', 'createOAuthUser', { username, email, provider });
|
||||
const res = await pbPost('/api/collections/app_users/records', {
|
||||
username,
|
||||
password_hash: '',
|
||||
role,
|
||||
email,
|
||||
email_verified: true,
|
||||
oauth_provider: provider,
|
||||
oauth_id: oauthId,
|
||||
avatar_url: avatarUrl ?? '',
|
||||
created: new Date().toISOString()
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.text().catch(() => '');
|
||||
log.error('pocketbase', 'createOAuthUser: PocketBase rejected record', {
|
||||
username,
|
||||
status: res.status,
|
||||
body
|
||||
});
|
||||
throw new Error(`Failed to create OAuth user: ${res.status} ${body}`);
|
||||
}
|
||||
return res.json() as Promise<User>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Link an OAuth provider to an existing user account.
|
||||
*/
|
||||
export async function linkOAuthToUser(
|
||||
userId: string,
|
||||
provider: string,
|
||||
oauthId: string
|
||||
): Promise<void> {
|
||||
const res = await pbPatch(`/api/collections/app_users/records/${userId}`, {
|
||||
oauth_provider: provider,
|
||||
oauth_id: oauthId,
|
||||
email_verified: true
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.text().catch(() => '');
|
||||
log.error('pocketbase', 'linkOAuthToUser: PATCH failed', { userId, status: res.status, body });
|
||||
throw new Error(`Failed to link OAuth: ${res.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a user by verification token. Returns null if not found.
|
||||
* @deprecated Email verification removed — kept only for migration safety.
|
||||
*/
|
||||
export async function getUserByVerificationToken(token: string): Promise<User | null> {
|
||||
return listOne<User>('app_users', `verification_token="${token.replace(/"/g, '\\"')}"`);
|
||||
@@ -608,7 +677,7 @@ export async function changePassword(
|
||||
|
||||
/**
|
||||
* Verify username + password. Returns the user on success, null on failure.
|
||||
* Throws with message 'Email not verified' if the account exists but hasn't been verified.
|
||||
* Only used for legacy accounts that still have a password_hash.
|
||||
*/
|
||||
export async function loginUser(username: string, password: string): Promise<User | null> {
|
||||
log.debug('pocketbase', 'loginUser: lookup', { username });
|
||||
@@ -617,15 +686,15 @@ export async function loginUser(username: string, password: string): Promise<Use
|
||||
log.warn('pocketbase', 'loginUser: username not found', { username });
|
||||
return null;
|
||||
}
|
||||
if (!user.password_hash) {
|
||||
log.warn('pocketbase', 'loginUser: account has no password (OAuth-only)', { username });
|
||||
return null;
|
||||
}
|
||||
const ok = verifyPassword(password, user.password_hash);
|
||||
if (!ok) {
|
||||
log.warn('pocketbase', 'loginUser: wrong password', { username });
|
||||
return null;
|
||||
}
|
||||
if (!user.email_verified) {
|
||||
log.warn('pocketbase', 'loginUser: email not verified', { username });
|
||||
throw new Error('Email not verified');
|
||||
}
|
||||
log.info('pocketbase', 'loginUser: success', { username, role: user.role });
|
||||
return user;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user