feat(auth): better-auth mit passwort, passkeys und app-shell
Registrierung des ersten Nutzers macht ihn zum Administrator, danach ist die Registrierung geschlossen (Einladungen folgen in Phase 2). Login per Passwort oder Passkey inklusive Conditional UI, Verwaltung der Passkeys unter "Konto" mit Umbenennen und Loeschen. Rate-Limiting liegt in der Datenbank und ueberlebt Neustarts; Session-Cookies sind httpOnly, sameSite=lax und im Produktivbetrieb secure. Die Client-IP wird hinter dem Reverse Proxy aus x-forwarded-for gelesen. Dazu die mobile-first Grundstruktur: Bottom-Navigation, Header mit Konto-Menue, Healthcheck unter /api/health und ein Dashboard mit den vier Einstiegspunkten. Bedienelemente sind auf 44px Touch-Groesse angehoben. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+33
-33
@@ -3,23 +3,23 @@ import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
/**
|
||||
* Tables owned by better-auth.
|
||||
*
|
||||
* Column names are camelCase here (unlike the rest of the schema) because that
|
||||
* is exactly what `@better-auth/cli generate` produces. Renaming them is a
|
||||
* reliable source of breakage whenever a plugin is added or updated.
|
||||
* The drizzle adapter resolves models through the TypeScript property names
|
||||
* (`userId`, `expiresAt`, ...), so the column names below are free to follow
|
||||
* the snake_case convention used by the rest of the schema.
|
||||
*/
|
||||
|
||||
export const user = sqliteTable('user', {
|
||||
id: text('id').primaryKey(),
|
||||
name: text('name').notNull(),
|
||||
email: text('email').notNull().unique(),
|
||||
emailVerified: integer('emailVerified', { mode: 'boolean' }).notNull().default(false),
|
||||
emailVerified: integer('email_verified', { mode: 'boolean' }).notNull().default(false),
|
||||
image: text('image'),
|
||||
/** App specific: the first registered user becomes admin, everyone else member. */
|
||||
role: text('role', { enum: ['admin', 'member'] })
|
||||
.notNull()
|
||||
.default('member'),
|
||||
createdAt: integer('createdAt', { mode: 'timestamp' }).notNull(),
|
||||
updatedAt: integer('updatedAt', { mode: 'timestamp' }).notNull(),
|
||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
||||
});
|
||||
|
||||
export const session = sqliteTable(
|
||||
@@ -27,14 +27,14 @@ export const session = sqliteTable(
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
token: text('token').notNull().unique(),
|
||||
userId: text('userId')
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: 'cascade' }),
|
||||
expiresAt: integer('expiresAt', { mode: 'timestamp' }).notNull(),
|
||||
ipAddress: text('ipAddress'),
|
||||
userAgent: text('userAgent'),
|
||||
createdAt: integer('createdAt', { mode: 'timestamp' }).notNull(),
|
||||
updatedAt: integer('updatedAt', { mode: 'timestamp' }).notNull(),
|
||||
expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(),
|
||||
ipAddress: text('ip_address'),
|
||||
userAgent: text('user_agent'),
|
||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
||||
},
|
||||
(t) => [index('session_user_idx').on(t.userId)],
|
||||
);
|
||||
@@ -44,20 +44,20 @@ export const account = sqliteTable(
|
||||
'account',
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
accountId: text('accountId').notNull(),
|
||||
providerId: text('providerId').notNull(),
|
||||
userId: text('userId')
|
||||
accountId: text('account_id').notNull(),
|
||||
providerId: text('provider_id').notNull(),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: 'cascade' }),
|
||||
accessToken: text('accessToken'),
|
||||
refreshToken: text('refreshToken'),
|
||||
idToken: text('idToken'),
|
||||
accessTokenExpiresAt: integer('accessTokenExpiresAt', { mode: 'timestamp' }),
|
||||
refreshTokenExpiresAt: integer('refreshTokenExpiresAt', { mode: 'timestamp' }),
|
||||
accessToken: text('access_token'),
|
||||
refreshToken: text('refresh_token'),
|
||||
idToken: text('id_token'),
|
||||
accessTokenExpiresAt: integer('access_token_expires_at', { mode: 'timestamp' }),
|
||||
refreshTokenExpiresAt: integer('refresh_token_expires_at', { mode: 'timestamp' }),
|
||||
scope: text('scope'),
|
||||
password: text('password'),
|
||||
createdAt: integer('createdAt', { mode: 'timestamp' }).notNull(),
|
||||
updatedAt: integer('updatedAt', { mode: 'timestamp' }).notNull(),
|
||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
||||
},
|
||||
(t) => [index('account_user_idx').on(t.userId)],
|
||||
);
|
||||
@@ -68,9 +68,9 @@ export const verification = sqliteTable(
|
||||
id: text('id').primaryKey(),
|
||||
identifier: text('identifier').notNull(),
|
||||
value: text('value').notNull(),
|
||||
expiresAt: integer('expiresAt', { mode: 'timestamp' }).notNull(),
|
||||
createdAt: integer('createdAt', { mode: 'timestamp' }).notNull(),
|
||||
updatedAt: integer('updatedAt', { mode: 'timestamp' }).notNull(),
|
||||
expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(),
|
||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
||||
},
|
||||
(t) => [index('verification_identifier_idx').on(t.identifier)],
|
||||
);
|
||||
@@ -81,27 +81,27 @@ export const passkey = sqliteTable(
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
name: text('name'),
|
||||
publicKey: text('publicKey').notNull(),
|
||||
userId: text('userId')
|
||||
publicKey: text('public_key').notNull(),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: 'cascade' }),
|
||||
credentialID: text('credentialID').notNull().unique(),
|
||||
credentialID: text('credential_id').notNull().unique(),
|
||||
counter: integer('counter').notNull(),
|
||||
deviceType: text('deviceType').notNull(),
|
||||
backedUp: integer('backedUp', { mode: 'boolean' }).notNull(),
|
||||
deviceType: text('device_type').notNull(),
|
||||
backedUp: integer('backed_up', { mode: 'boolean' }).notNull(),
|
||||
transports: text('transports'),
|
||||
aaguid: text('aaguid'),
|
||||
createdAt: integer('createdAt', { mode: 'timestamp' }),
|
||||
createdAt: integer('created_at', { mode: 'timestamp' }),
|
||||
},
|
||||
(t) => [index('passkey_user_idx').on(t.userId)],
|
||||
);
|
||||
|
||||
/** Persistent rate limiting so restarts do not reset login throttling. */
|
||||
export const rateLimit = sqliteTable('rateLimit', {
|
||||
export const rateLimit = sqliteTable('rate_limit', {
|
||||
id: text('id').primaryKey(),
|
||||
key: text('key'),
|
||||
count: integer('count'),
|
||||
lastRequest: integer('lastRequest'),
|
||||
lastRequest: integer('last_request'),
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user