feat: migrate to Better Auth for authentication, update environment variables, and enhance database schema with accounts and sessions
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
DATABASE_URL=postgres://username:password@localhost:5432/database_name
|
DATABASE_URL=postgres://username:password@localhost:5432/database_name
|
||||||
JWT_SECRET=replace_with_a_long_random_secret
|
BETTER_AUTH_SECRET=replace_with_a_long_random_secret
|
||||||
JWT_EXPIRES_IN=7d
|
BETTER_AUTH_URL=http://localhost:3000
|
||||||
|
BETTER_AUTH_TRUSTED_ORIGINS=http://localhost:5173
|
||||||
PORT=3000
|
PORT=3000
|
||||||
MINIO_ENDPOINT=localhost
|
MINIO_ENDPOINT=localhost
|
||||||
MINIO_PORT=9000
|
MINIO_PORT=9000
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
## Overview
|
## Overview
|
||||||
Backend for the video upload prototype providing:
|
Backend for the video upload prototype providing:
|
||||||
|
|
||||||
- JWT-based authentication
|
- Better Auth email/password authentication
|
||||||
- Presigned MinIO uploads/downloads
|
- Presigned MinIO uploads/downloads
|
||||||
- An authenticated video administration surface at `/admin`
|
- An authenticated video administration surface at `/admin`
|
||||||
|
|
||||||
@@ -30,8 +30,9 @@ Required env vars:
|
|||||||
| Name | Purpose |
|
| Name | Purpose |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `DATABASE_URL` | Postgres connection string |
|
| `DATABASE_URL` | Postgres connection string |
|
||||||
| `JWT_SECRET` | Secret used to sign access tokens |
|
| `BETTER_AUTH_SECRET` | Secret used to sign sessions |
|
||||||
| `JWT_EXPIRES_IN` | Token expiry (e.g., `7d`) |
|
| `BETTER_AUTH_URL` | Public base URL for the backend (e.g., `http://localhost:3000`) |
|
||||||
|
| `BETTER_AUTH_TRUSTED_ORIGINS` | Comma-separated list of allowed frontend origins |
|
||||||
| `PORT` | HTTP port (default `3000`) |
|
| `PORT` | HTTP port (default `3000`) |
|
||||||
| `MINIO_*` | Connection settings for the MinIO/S3 endpoint |
|
| `MINIO_*` | Connection settings for the MinIO/S3 endpoint |
|
||||||
| `ADMIN_USERNAME` / `ADMIN_PASSWORD` | Basic auth for `/admin` dashboard |
|
| `ADMIN_USERNAME` / `ADMIN_PASSWORD` | Basic auth for `/admin` dashboard |
|
||||||
@@ -54,23 +55,23 @@ bun run dev
|
|||||||
```bash
|
```bash
|
||||||
bun run db:migrate
|
bun run db:migrate
|
||||||
```
|
```
|
||||||
|
- Backfill Better Auth credential accounts for existing users:
|
||||||
|
```bash
|
||||||
|
bun run auth:migrate
|
||||||
|
```
|
||||||
- Open Drizzle Studio:
|
- Open Drizzle Studio:
|
||||||
```bash
|
```bash
|
||||||
bun run db:studio
|
bun run db:studio
|
||||||
```
|
```
|
||||||
|
|
||||||
## API
|
## API
|
||||||
All `/videos` and `/admin` routes require a valid JWT Bearer token except for the admin dashboard access, which uses HTTP Basic auth with `ADMIN_USERNAME`/`ADMIN_PASSWORD`.
|
All `/videos` and `/admin` routes require a valid Better Auth session except for the admin dashboard access, which uses HTTP Basic auth with `ADMIN_USERNAME`/`ADMIN_PASSWORD`.
|
||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
| Endpoint | Description |
|
Authentication is handled by Better Auth under `/api/auth/*` (for example `/api/auth/sign-in` and `/api/auth/sign-up`).
|
||||||
| --- | --- |
|
|
||||||
| `POST /auth/register` | Create a user (`email`, `password`, `name`) |
|
|
||||||
| `POST /auth/login` | Receive a token using `email`/`password` |
|
|
||||||
| `GET /auth/me` | Get the current user ([Authorization](#authorization)) |
|
|
||||||
|
|
||||||
### Authorization
|
### Authorization
|
||||||
All authenticated endpoints expect an `Authorization: Bearer <token>` header containing the JWT issued at login.
|
All authenticated endpoints expect a Better Auth session cookie sent by the client.
|
||||||
|
|
||||||
### Video Management
|
### Video Management
|
||||||
| Endpoint | Purpose |
|
| Endpoint | Purpose |
|
||||||
@@ -96,4 +97,4 @@ The dashboard UI submits to `/admin/upload-url`, `/admin/objects`, and `/admin/o
|
|||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
- MinIO bucket creation happens during startup, so the service must be able to reach the endpoint.
|
- MinIO bucket creation happens during startup, so the service must be able to reach the endpoint.
|
||||||
- Keep JWT and MinIO secrets out of source control.
|
- Keep Better Auth and MinIO secrets out of source control.
|
||||||
|
|||||||
32
Backend/auth.ts
Normal file
32
Backend/auth.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { betterAuth } from 'better-auth';
|
||||||
|
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
|
||||||
|
|
||||||
|
import { db } from './db/client';
|
||||||
|
import { schema } from './db/schema';
|
||||||
|
import { hashPassword, verifyPassword } from './utils/password';
|
||||||
|
|
||||||
|
const trustedOrigins = process.env.BETTER_AUTH_TRUSTED_ORIGINS
|
||||||
|
? process.env.BETTER_AUTH_TRUSTED_ORIGINS.split(',').map((origin) => origin.trim()).filter(Boolean)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
export const auth = betterAuth({
|
||||||
|
database: drizzleAdapter(db, {
|
||||||
|
provider: 'pg',
|
||||||
|
schema: {
|
||||||
|
...schema,
|
||||||
|
user: schema.users,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
emailAndPassword: {
|
||||||
|
enabled: true,
|
||||||
|
password: {
|
||||||
|
hash: async (password) => hashPassword(password),
|
||||||
|
verify: async ({ hash, password }) => verifyPassword(password, hash),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
secret: process.env.BETTER_AUTH_SECRET,
|
||||||
|
baseURL: process.env.BETTER_AUTH_URL,
|
||||||
|
trustedOrigins,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AuthSession = Awaited<ReturnType<typeof auth.api.getSession>>;
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'dotenv/config';
|
|
||||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||||
import { Pool } from 'pg';
|
import { Pool } from 'pg';
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ export const users = pgTable('users', {
|
|||||||
email: varchar('email', { length: 255 }).notNull().unique(),
|
email: varchar('email', { length: 255 }).notNull().unique(),
|
||||||
name: varchar('name', { length: 255 }).notNull(),
|
name: varchar('name', { length: 255 }).notNull(),
|
||||||
passwordHash: varchar('password_hash', { length: 255 }).notNull(),
|
passwordHash: varchar('password_hash', { length: 255 }).notNull(),
|
||||||
|
emailVerified: boolean('email_verified').default(false).notNull(),
|
||||||
|
image: text('image'),
|
||||||
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const devices = pgTable('devices', {
|
export const devices = pgTable('devices', {
|
||||||
@@ -56,3 +59,50 @@ export const notifications = pgTable('notifications', {
|
|||||||
status: varchar('status', { length: 32 }).default('queued').notNull(),
|
status: varchar('status', { length: 32 }).default('queued').notNull(),
|
||||||
isRead: boolean('is_read').default(false).notNull(),
|
isRead: boolean('is_read').default(false).notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const accounts = pgTable('account', {
|
||||||
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
|
userId: uuid('user_id').notNull().references(() => users.id),
|
||||||
|
accountId: text('account_id').notNull(),
|
||||||
|
providerId: text('provider_id').notNull(),
|
||||||
|
accessToken: text('access_token'),
|
||||||
|
refreshToken: text('refresh_token'),
|
||||||
|
accessTokenExpiresAt: timestamp('access_token_expires_at', { withTimezone: true }),
|
||||||
|
refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { withTimezone: true }),
|
||||||
|
idToken: text('id_token'),
|
||||||
|
scope: text('scope'),
|
||||||
|
password: text('password'),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const sessions = pgTable('session', {
|
||||||
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
|
userId: uuid('user_id').notNull().references(() => users.id),
|
||||||
|
token: text('token').notNull().unique(),
|
||||||
|
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
||||||
|
ipAddress: text('ip_address'),
|
||||||
|
userAgent: text('user_agent'),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const verifications = pgTable('verification', {
|
||||||
|
id: uuid('id').defaultRandom().primaryKey(),
|
||||||
|
identifier: text('identifier').notNull(),
|
||||||
|
value: text('value').notNull(),
|
||||||
|
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const schema = {
|
||||||
|
users,
|
||||||
|
devices,
|
||||||
|
events,
|
||||||
|
videos,
|
||||||
|
notifications,
|
||||||
|
accounts,
|
||||||
|
sessions,
|
||||||
|
verifications,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import 'dotenv/config';
|
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
|
import { toNodeHandler } from 'better-auth/node';
|
||||||
|
|
||||||
import authRoutes from './routes/auth';
|
import { auth } from './auth';
|
||||||
import videosRoutes from './routes/videos';
|
import videosRoutes from './routes/videos';
|
||||||
import adminRoutes from './routes/admin';
|
import adminRoutes from './routes/admin';
|
||||||
import { ensureMinioBucket } from './utils/minio';
|
import { ensureMinioBucket } from './utils/minio';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.use(express.json());
|
|
||||||
|
|
||||||
app.get('/', (_req, res) => {
|
app.get('/', (_req, res) => {
|
||||||
res.send('API is running');
|
res.send('API is running');
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use('/auth', authRoutes);
|
app.all('/api/auth/*splat', toNodeHandler(auth));
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
app.use('/videos', videosRoutes);
|
app.use('/videos', videosRoutes);
|
||||||
app.use('/admin', adminRoutes);
|
app.use('/admin', adminRoutes);
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
import type { NextFunction, Request, Response } from 'express';
|
import type { NextFunction, Request, Response } from 'express';
|
||||||
|
|
||||||
import { verifyAccessToken } from '../utils/jwt';
|
import { fromNodeHeaders } from 'better-auth/node';
|
||||||
|
|
||||||
export function requireAuth(req: Request, res: Response, next: NextFunction): void {
|
import { auth } from '../auth';
|
||||||
const authorization = req.headers.authorization;
|
|
||||||
|
|
||||||
if (!authorization?.startsWith('Bearer ')) {
|
|
||||||
res.status(401).json({ message: 'Missing or invalid authorization header' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = authorization.slice(7);
|
|
||||||
|
|
||||||
|
export async function requireAuth(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const payload = verifyAccessToken(token);
|
const session = await auth.api.getSession({
|
||||||
req.user = payload;
|
headers: fromNodeHeaders(req.headers),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
res.status(401).json({ message: 'Unauthorized' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
req.auth = session;
|
||||||
next();
|
next();
|
||||||
} catch {
|
} catch (error) {
|
||||||
res.status(401).json({ message: 'Invalid or expired token' });
|
console.error('Auth session lookup failed', error);
|
||||||
|
res.status(401).json({ message: 'Unauthorized' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
|
||||||
"@types/node": "^25.2.1",
|
"@types/node": "^25.2.1",
|
||||||
"@types/pg": "^8.16.0",
|
"@types/pg": "^8.16.0",
|
||||||
"drizzle-kit": "^0.31.0",
|
"drizzle-kit": "^0.31.0",
|
||||||
@@ -26,7 +25,7 @@
|
|||||||
"drizzle-orm": "^0.44.0",
|
"drizzle-orm": "^0.44.0",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"helmet": "^8.1.0",
|
"helmet": "^8.1.0",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"better-auth": "^1.4.0",
|
||||||
"minio": "^8.0.6",
|
"minio": "^8.0.6",
|
||||||
"openai": "^6.18.0",
|
"openai": "^6.18.0",
|
||||||
"pg": "^8.18.0",
|
"pg": "^8.18.0",
|
||||||
@@ -40,6 +39,7 @@
|
|||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"db:push": "drizzle-kit push",
|
"db:push": "drizzle-kit push",
|
||||||
"db:studio": "drizzle-kit studio"
|
"db:studio": "drizzle-kit studio",
|
||||||
|
"auth:migrate": "bun run scripts/migrate-better-auth.ts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
import { Router } from 'express';
|
|
||||||
import { and, eq } from 'drizzle-orm';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { db } from '../db/client';
|
|
||||||
import { users } from '../db/schema';
|
|
||||||
import { requireAuth } from '../middleware/auth';
|
|
||||||
import { signAccessToken } from '../utils/jwt';
|
|
||||||
import { hashPassword, verifyPassword } from '../utils/password';
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
const registerSchema = z.object({
|
|
||||||
email: z.email().trim().toLowerCase(),
|
|
||||||
password: z.string().min(8),
|
|
||||||
name: z.string().trim().min(1).max(255),
|
|
||||||
});
|
|
||||||
|
|
||||||
const loginSchema = z.object({
|
|
||||||
email: z.email().trim().toLowerCase(),
|
|
||||||
password: z.string().min(1),
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/register', async (req, res) => {
|
|
||||||
const parsed = registerSchema.safeParse(req.body);
|
|
||||||
|
|
||||||
if (!parsed.success) {
|
|
||||||
res.status(400).json({ message: 'Invalid request body', errors: parsed.error.flatten() });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { email, password, name } = parsed.data;
|
|
||||||
|
|
||||||
const existingUser = await db.query.users.findFirst({
|
|
||||||
where: eq(users.email, email),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingUser) {
|
|
||||||
res.status(409).json({ message: 'Email already in use' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const passwordHash = await hashPassword(password);
|
|
||||||
|
|
||||||
const [newUser] = await db
|
|
||||||
.insert(users)
|
|
||||||
.values({
|
|
||||||
email,
|
|
||||||
name,
|
|
||||||
passwordHash,
|
|
||||||
})
|
|
||||||
.returning({
|
|
||||||
id: users.id,
|
|
||||||
email: users.email,
|
|
||||||
name: users.name,
|
|
||||||
createdAt: users.createdAt,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!newUser) {
|
|
||||||
res.status(500).json({ message: 'Failed to create user' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = signAccessToken({ userId: newUser.id, email: newUser.email });
|
|
||||||
|
|
||||||
res.status(201).json({ token, user: newUser });
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/login', async (req, res) => {
|
|
||||||
const parsed = loginSchema.safeParse(req.body);
|
|
||||||
|
|
||||||
if (!parsed.success) {
|
|
||||||
res.status(400).json({ message: 'Invalid request body', errors: parsed.error.flatten() });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { email, password } = parsed.data;
|
|
||||||
|
|
||||||
const user = await db.query.users.findFirst({
|
|
||||||
where: eq(users.email, email),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
res.status(401).json({ message: 'Invalid email or password' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isPasswordValid = await verifyPassword(password, user.passwordHash);
|
|
||||||
|
|
||||||
if (!isPasswordValid) {
|
|
||||||
res.status(401).json({ message: 'Invalid email or password' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = signAccessToken({ userId: user.id, email: user.email });
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
token,
|
|
||||||
user: {
|
|
||||||
id: user.id,
|
|
||||||
email: user.email,
|
|
||||||
name: user.name,
|
|
||||||
createdAt: user.createdAt,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/me', requireAuth, async (req, res) => {
|
|
||||||
const authenticatedUser = req.user;
|
|
||||||
|
|
||||||
if (!authenticatedUser) {
|
|
||||||
res.status(401).json({ message: 'Unauthorized' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await db.query.users.findFirst({
|
|
||||||
where: and(eq(users.id, authenticatedUser.userId), eq(users.email, authenticatedUser.email)),
|
|
||||||
columns: {
|
|
||||||
id: true,
|
|
||||||
email: true,
|
|
||||||
name: true,
|
|
||||||
createdAt: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
res.status(404).json({ message: 'User not found' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({ user });
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -46,16 +46,16 @@ router.post('/upload-url', async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = req.user;
|
const authSession = req.auth;
|
||||||
|
|
||||||
if (!user) {
|
if (!authSession?.user) {
|
||||||
res.status(401).json({ message: 'Unauthorized' });
|
res.status(401).json({ message: 'Unauthorized' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ensureMinioBucket();
|
await ensureMinioBucket();
|
||||||
|
|
||||||
const objectKey = buildObjectKey(user.userId, parsed.data.fileName, parsed.data.prefix);
|
const objectKey = buildObjectKey(authSession.user.id, parsed.data.fileName, parsed.data.prefix);
|
||||||
const uploadUrl = await minioClient.presignedPutObject(minioBucket, objectKey, minioPresignedExpirySeconds);
|
const uploadUrl = await minioClient.presignedPutObject(minioBucket, objectKey, minioPresignedExpirySeconds);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const expiresAt = new Date(now.getTime() + minioPresignedExpirySeconds * 1000);
|
const expiresAt = new Date(now.getTime() + minioPresignedExpirySeconds * 1000);
|
||||||
@@ -63,7 +63,7 @@ router.post('/upload-url', async (req, res) => {
|
|||||||
const [videoRecord] = await db
|
const [videoRecord] = await db
|
||||||
.insert(videos)
|
.insert(videos)
|
||||||
.values({
|
.values({
|
||||||
userId: user.userId,
|
userId: authSession.user.id,
|
||||||
objectKey,
|
objectKey,
|
||||||
bucket: minioBucket,
|
bucket: minioBucket,
|
||||||
uploadUrl,
|
uploadUrl,
|
||||||
|
|||||||
47
Backend/scripts/migrate-better-auth.ts
Normal file
47
Backend/scripts/migrate-better-auth.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { and, eq } from 'drizzle-orm';
|
||||||
|
|
||||||
|
import { db } from '../db/client';
|
||||||
|
import { accounts, users } from '../db/schema';
|
||||||
|
|
||||||
|
const PROVIDER_ID = 'credential';
|
||||||
|
|
||||||
|
const run = async (): Promise<void> => {
|
||||||
|
const existingUsers = await db.select({
|
||||||
|
id: users.id,
|
||||||
|
passwordHash: users.passwordHash,
|
||||||
|
}).from(users);
|
||||||
|
|
||||||
|
let created = 0;
|
||||||
|
|
||||||
|
for (const user of existingUsers) {
|
||||||
|
const account = await db.query.accounts.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(accounts.userId, user.id),
|
||||||
|
eq(accounts.providerId, PROVIDER_ID),
|
||||||
|
eq(accounts.accountId, user.id),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (account) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.insert(accounts).values({
|
||||||
|
userId: user.id,
|
||||||
|
accountId: user.id,
|
||||||
|
providerId: PROVIDER_ID,
|
||||||
|
password: user.passwordHash,
|
||||||
|
});
|
||||||
|
|
||||||
|
created += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Created ${created} credential account(s).`);
|
||||||
|
};
|
||||||
|
|
||||||
|
run()
|
||||||
|
.then(() => process.exit(0))
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to migrate Better Auth accounts', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
14
Backend/types/express.d.ts
vendored
14
Backend/types/express.d.ts
vendored
@@ -1,9 +1,17 @@
|
|||||||
import type { JwtPayload } from '../utils/jwt';
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace Express {
|
namespace Express {
|
||||||
interface Request {
|
interface Request {
|
||||||
user?: JwtPayload;
|
auth?: {
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name?: string | null;
|
||||||
|
};
|
||||||
|
session: {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import jwt from 'jsonwebtoken';
|
|
||||||
|
|
||||||
const JWT_EXPIRES_IN = (process.env.JWT_EXPIRES_IN ?? '7d') as jwt.SignOptions['expiresIn'];
|
|
||||||
|
|
||||||
function getJwtSecret(): string {
|
|
||||||
const secret = process.env.JWT_SECRET;
|
|
||||||
|
|
||||||
if (!secret) {
|
|
||||||
throw new Error('JWT_SECRET is not set');
|
|
||||||
}
|
|
||||||
|
|
||||||
return secret;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type JwtPayload = {
|
|
||||||
userId: string;
|
|
||||||
email: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function signAccessToken(payload: JwtPayload): string {
|
|
||||||
return jwt.sign(payload, getJwtSecret(), {
|
|
||||||
expiresIn: JWT_EXPIRES_IN,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function verifyAccessToken(token: string): JwtPayload {
|
|
||||||
return jwt.verify(token, getJwtSecret()) as JwtPayload;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user