chore: update database schema and enhance authentication features

- Add `username` column to the `user` table and enforce uniqueness constraint.
- Introduce new SQL migration file for the `username` addition.
- Update `schema.ts` to reflect changes in the `user`, `session`, and `account` tables.
- Enhance authentication client by integrating `usernameClient` plugin for better user management.
- Adjust session management settings for improved caching and freshness.
- Refactor metadata generation in the home page for consistency.
This commit is contained in:
javayhu 2025-03-15 00:54:13 +08:00
parent 0045ecf91e
commit 2ba2eebbaa
8 changed files with 456 additions and 75 deletions

View File

@ -15,7 +15,8 @@
"build/**",
"src/components/ui/*.tsx",
"src/components/magicui/*.tsx",
"tailwind.config.ts"
"tailwind.config.ts",
"src/db/schema.ts"
]
},
"formatter": {
@ -52,7 +53,8 @@
"build/**",
"src/components/ui/*.tsx",
"src/components/magicui/*.tsx",
"tailwind.config.ts"
"tailwind.config.ts",
"src/db/schema.ts"
]
},
"javascript": {

View File

@ -0,0 +1,2 @@
ALTER TABLE "user" ADD COLUMN "username" text;--> statement-breakpoint
ALTER TABLE "user" ADD CONSTRAINT "user_username_unique" UNIQUE("username");

View File

@ -0,0 +1,362 @@
{
"id": "21c4862e-1256-46f8-ac51-312d90aa61f8",
"prevId": "53387f51-ed0d-4c44-b8ed-5af936eef75e",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.account": {
"name": "account",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"account_id": {
"name": "account_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"provider_id": {
"name": "provider_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"id_token": {
"name": "id_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"access_token_expires_at": {
"name": "access_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"refresh_token_expires_at": {
"name": "refresh_token_expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"account_user_id_user_id_fk": {
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.session": {
"name": "session",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"ip_address": {
"name": "ip_address",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_agent": {
"name": "user_agent",
"type": "text",
"primaryKey": false,
"notNull": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"impersonated_by": {
"name": "impersonated_by",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"session_user_id_user_id_fk": {
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"session_token_unique": {
"name": "session_token_unique",
"nullsNotDistinct": false,
"columns": [
"token"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true
},
"email_verified": {
"name": "email_verified",
"type": "boolean",
"primaryKey": false,
"notNull": true
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": false
},
"banned": {
"name": "banned",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"ban_reason": {
"name": "ban_reason",
"type": "text",
"primaryKey": false,
"notNull": false
},
"ban_expires": {
"name": "ban_expires",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
},
"user_username_unique": {
"name": "user_username_unique",
"nullsNotDistinct": false,
"columns": [
"username"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.verification": {
"name": "verification",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@ -15,6 +15,13 @@
"when": 1740323860731,
"tag": "0001_shocking_wild_pack",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1741971106943,
"tag": "0002_talented_greymalkin",
"breakpoints": true
}
]
}

View File

@ -26,7 +26,7 @@ export async function generateMetadata({
return constructMetadata({
title: t('title'),
description: t('description'),
canonicalUrl: `${getBaseUrlWithLocale(locale)}/`,
canonicalUrl: `${getBaseUrlWithLocale(locale)}`,
});
}

View File

@ -1,62 +1,53 @@
import {
pgTable,
text,
integer,
timestamp,
boolean,
} from 'drizzle-orm/pg-core';
import { pgTable, text, integer, timestamp, boolean } from "drizzle-orm/pg-core";
export const user = pgTable("user", {
id: text("id").primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
emailVerified: boolean('email_verified').notNull(),
image: text('image'),
createdAt: timestamp('created_at').notNull(),
updatedAt: timestamp('updated_at').notNull(),
username: text('username').unique(),
role: text('role'),
banned: boolean('banned'),
banReason: text('ban_reason'),
banExpires: timestamp('ban_expires')
});
export const user = pgTable('user', {
id: text('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
emailVerified: boolean('email_verified').notNull(),
image: text('image'),
createdAt: timestamp('created_at').notNull(),
updatedAt: timestamp('updated_at').notNull(),
role: text('role'),
banned: boolean('banned'),
banReason: text('ban_reason'),
banExpires: timestamp('ban_expires'),
});
export const session = pgTable("session", {
id: text("id").primaryKey(),
expiresAt: timestamp('expires_at').notNull(),
token: text('token').notNull().unique(),
createdAt: timestamp('created_at').notNull(),
updatedAt: timestamp('updated_at').notNull(),
ipAddress: text('ip_address'),
userAgent: text('user_agent'),
userId: text('user_id').notNull().references(()=> user.id, { onDelete: 'cascade' }),
impersonatedBy: text('impersonated_by')
});
export const session = pgTable('session', {
id: text('id').primaryKey(),
expiresAt: timestamp('expires_at').notNull(),
token: text('token').notNull().unique(),
createdAt: timestamp('created_at').notNull(),
updatedAt: timestamp('updated_at').notNull(),
ipAddress: text('ip_address'),
userAgent: text('user_agent'),
userId: text('user_id')
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
impersonatedBy: text('impersonated_by'),
});
export const account = pgTable("account", {
id: text("id").primaryKey(),
accountId: text('account_id').notNull(),
providerId: text('provider_id').notNull(),
userId: text('user_id').notNull().references(()=> user.id, { onDelete: 'cascade' }),
accessToken: text('access_token'),
refreshToken: text('refresh_token'),
idToken: text('id_token'),
accessTokenExpiresAt: timestamp('access_token_expires_at'),
refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
scope: text('scope'),
password: text('password'),
createdAt: timestamp('created_at').notNull(),
updatedAt: timestamp('updated_at').notNull()
});
export const account = pgTable('account', {
id: text('id').primaryKey(),
accountId: text('account_id').notNull(),
providerId: text('provider_id').notNull(),
userId: text('user_id')
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
accessToken: text('access_token'),
refreshToken: text('refresh_token'),
idToken: text('id_token'),
accessTokenExpiresAt: timestamp('access_token_expires_at'),
refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
scope: text('scope'),
password: text('password'),
createdAt: timestamp('created_at').notNull(),
updatedAt: timestamp('updated_at').notNull(),
});
export const verification = pgTable('verification', {
id: text('id').primaryKey(),
identifier: text('identifier').notNull(),
value: text('value').notNull(),
expiresAt: timestamp('expires_at').notNull(),
createdAt: timestamp('created_at'),
updatedAt: timestamp('updated_at'),
});
export const verification = pgTable("verification", {
id: text("id").primaryKey(),
identifier: text('identifier').notNull(),
value: text('value').notNull(),
expiresAt: timestamp('expires_at').notNull(),
createdAt: timestamp('created_at'),
updatedAt: timestamp('updated_at')
});

View File

@ -1,5 +1,5 @@
import { createAuthClient } from 'better-auth/react';
import { adminClient } from 'better-auth/client/plugins';
import { adminClient, usernameClient } from 'better-auth/client/plugins';
import { getBaseUrl } from './urls/get-base-url';
/**
@ -8,6 +8,8 @@ import { getBaseUrl } from './urls/get-base-url';
export const authClient = createAuthClient({
baseURL: getBaseUrl(),
plugins: [
// https://www.better-auth.com/docs/plugins/username
usernameClient(),
// https://www.better-auth.com/docs/plugins/admin#add-the-client-plugin
adminClient(),
],

View File

@ -1,24 +1,22 @@
import { getWebsiteInfo } from '@/config';
import db from '@/db/index';
import { account, session, user, verification } from '@/db/schema';
import { createTranslator } from '@/i18n/translator';
import { getLocaleFromRequest } from '@/lib/utils';
import { send } from '@/mail';
import { betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { admin } from 'better-auth/plugins';
// Create a simple translator function for default values
const t = createTranslator((key: string) => key);
const websiteInfo = getWebsiteInfo(t);
import { admin, username } from 'better-auth/plugins';
/**
* https://www.better-auth.com/docs/reference/options
*/
export const auth = betterAuth({
appName: websiteInfo.name,
appName: 'MkSaaS',
database: drizzleAdapter(db, {
provider: 'pg', // or "mysql", "sqlite"
// The schema object that defines the tables and fields
// [BetterAuthError]: [# Drizzle Adapter]: The model "verification" was not found in the schema object.
// Please pass the schema directly to the adapter options.
// https://www.better-auth.com/docs/adapters/drizzle#additional-information
schema: {
user: user,
session: session,
@ -30,12 +28,13 @@ export const auth = betterAuth({
// https://www.better-auth.com/docs/concepts/session-management#cookie-cache
cookieCache: {
enabled: true,
maxAge: 5 * 60, // Cache duration in seconds
maxAge: 60 * 60, // Cache duration in seconds
},
// https://www.better-auth.com/docs/concepts/session-management#session-expiration
expiresIn: 60 * 60 * 24 * 7,
updateAge: 60 * 60 * 24,
freshAge: 60 * 5,
// https://www.better-auth.com/docs/concepts/session-management#session-freshness
freshAge: 60 * 60 * 24,
},
emailAndPassword: {
enabled: true,
@ -44,7 +43,6 @@ export const auth = betterAuth({
// https://www.better-auth.com/docs/authentication/email-password#forget-password
async sendResetPassword({ user, url }, request) {
const locale = getLocaleFromRequest(request);
console.log('sendResetPassword, locale:', locale);
await send({
to: user.email,
template: 'forgotPassword',
@ -62,7 +60,6 @@ export const auth = betterAuth({
// https://www.better-auth.com/docs/authentication/email-password#require-email-verification
sendVerificationEmail: async ({ user, url, token }, request) => {
const locale = getLocaleFromRequest(request);
console.log('sendVerificationEmail, locale:', locale);
await send({
to: user.email,
template: 'verifyEmail',
@ -93,8 +90,26 @@ export const auth = betterAuth({
trustedProviders: ['google', 'github'],
},
},
user: {
// https://www.better-auth.com/docs/concepts/users-accounts#delete-user
deleteUser: {
enabled: true,
},
},
plugins: [
// https://www.better-auth.com/docs/plugins/username
username({
minUsernameLength: 3,
maxUsernameLength: 30,
}),
// https://www.better-auth.com/docs/plugins/admin
admin(),
],
onAPIError: {
// https://www.better-auth.com/docs/reference/options#onapierror
errorURL: '/auth/error',
onError: (error, ctx) => {
console.error('auth error:', error);
},
},
});