feat(auth): implement better authentication system with user sessions and password management
- Added user, session, account, and verification models to Prisma schema. - Integrated better-auth for user authentication with email and password. - Created auth middleware for session validation. - Implemented auth context and client in frontend for managing user sessions. - Added login and settings pages for user authentication and password management. - Updated routes to include authentication checks and user-specific content. - Enhanced UI components to reflect authentication state.
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
"prisma:migrate:deploy": "prisma migrate deploy"
|
||||
},
|
||||
"dependencies": {
|
||||
"@noble/ciphers": "^2.1.1",
|
||||
"@personal-admin/common": "workspace:*",
|
||||
"@prisma/adapter-pg": "^7.8.0",
|
||||
"@prisma/client": "^7.8.0",
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "user" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"emailVerified" BOOLEAN NOT NULL DEFAULT false,
|
||||
"image" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "user_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "session" (
|
||||
"id" TEXT NOT NULL,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"token" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"ipAddress" TEXT,
|
||||
"userAgent" TEXT,
|
||||
"userId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "session_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "account" (
|
||||
"id" TEXT NOT NULL,
|
||||
"accountId" TEXT NOT NULL,
|
||||
"providerId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"accessToken" TEXT,
|
||||
"refreshToken" TEXT,
|
||||
"idToken" TEXT,
|
||||
"accessTokenExpiresAt" TIMESTAMP(3),
|
||||
"refreshTokenExpiresAt" TIMESTAMP(3),
|
||||
"scope" TEXT,
|
||||
"password" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "account_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "verification" (
|
||||
"id" TEXT NOT NULL,
|
||||
"identifier" TEXT NOT NULL,
|
||||
"value" TEXT NOT NULL,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "verification_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "user_email_key" ON "user"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "session_token_key" ON "session"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "session_userId_idx" ON "session"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "account_userId_idx" ON "account"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "verification_identifier_idx" ON "verification"("identifier");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "session" ADD CONSTRAINT "session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "account" ADD CONSTRAINT "account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,8 +1,3 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
// Get a free hosted Postgres database in seconds: `npx create-db`
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client"
|
||||
output = "../src/generated/prisma"
|
||||
@@ -12,6 +7,68 @@ datasource db {
|
||||
provider = "postgresql"
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id
|
||||
name String
|
||||
email String @unique
|
||||
emailVerified Boolean @default(false)
|
||||
image String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
sessions Session[]
|
||||
accounts Account[]
|
||||
|
||||
@@map("user")
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id
|
||||
expiresAt DateTime
|
||||
token String @unique
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
ipAddress String?
|
||||
userAgent String?
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@map("session")
|
||||
}
|
||||
|
||||
model Account {
|
||||
id String @id
|
||||
accountId String
|
||||
providerId String
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
accessToken String?
|
||||
refreshToken String?
|
||||
idToken String?
|
||||
accessTokenExpiresAt DateTime?
|
||||
refreshTokenExpiresAt DateTime?
|
||||
scope String?
|
||||
password String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([userId])
|
||||
@@map("account")
|
||||
}
|
||||
|
||||
model Verification {
|
||||
id String @id
|
||||
identifier String
|
||||
value String
|
||||
expiresAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([identifier])
|
||||
@@map("verification")
|
||||
}
|
||||
|
||||
enum QuoteType {
|
||||
BLUE
|
||||
BNA
|
||||
|
||||
58
apps/backend/prisma/seed.ts
Normal file
58
apps/backend/prisma/seed.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import "dotenv/config";
|
||||
import { betterAuth } from "better-auth";
|
||||
import { prismaAdapter } from "better-auth/adapters/prisma";
|
||||
import { PrismaClient } from "../src/generated/prisma/client";
|
||||
import { PrismaPg } from "@prisma/adapter-pg";
|
||||
|
||||
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! });
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
|
||||
async function main() {
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email: "jselesan@gmail.com" },
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
console.log("Admin user already exists, skipping seed.");
|
||||
return;
|
||||
}
|
||||
|
||||
const auth = betterAuth({
|
||||
database: prismaAdapter(prisma, {
|
||||
provider: "postgresql",
|
||||
}),
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
disableSignUp: false,
|
||||
minPasswordLength: 4,
|
||||
},
|
||||
secret: process.env.BETTER_AUTH_SECRET!,
|
||||
baseURL: process.env.BETTER_AUTH_URL ?? "http://localhost:3000",
|
||||
});
|
||||
|
||||
const password = process.env.ADMIN_PASSWORD;
|
||||
|
||||
if (!password) {
|
||||
console.error("ADMIN_PASSWORD environment variable is not set.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await auth.api.signUpEmail({
|
||||
body: {
|
||||
email: "jselesan@gmail.com",
|
||||
password,
|
||||
name: "Admin",
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Admin user created successfully.");
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Hono } from "hono";
|
||||
import { serveStatic } from "hono/bun";
|
||||
import { auth } from "./lib/auth";
|
||||
import { authMiddleware } from "./lib/auth-middleware";
|
||||
import { startQuoteJob } from "./modules/quotes/quote.job";
|
||||
import quotesRouter from "./modules/quotes/quotes.routes";
|
||||
import expensesRouter from "./modules/expenses/expenses.routes";
|
||||
@@ -12,6 +14,15 @@ app.get("/api/health", (c) => {
|
||||
return c.json({ status: "ok", timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
app.on(["POST", "GET"], "/api/auth/*", (c) => auth.handler(c.req.raw));
|
||||
|
||||
app.use("/api/*", async (c, next) => {
|
||||
if (c.req.path === "/api/health" || c.req.path.startsWith("/api/auth")) {
|
||||
return next();
|
||||
}
|
||||
return authMiddleware(c, next);
|
||||
});
|
||||
|
||||
app.route("/api/quotes", quotesRouter);
|
||||
app.route("/api", expensesRouter);
|
||||
|
||||
|
||||
14
apps/backend/src/lib/auth-middleware.ts
Normal file
14
apps/backend/src/lib/auth-middleware.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { Context, Next } from "hono";
|
||||
import { auth } from "./auth";
|
||||
|
||||
export async function authMiddleware(c: Context, next: Next) {
|
||||
const session = await auth.api.getSession({ headers: c.req.raw.headers });
|
||||
|
||||
if (!session) {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
|
||||
c.set("user", session.user);
|
||||
c.set("session", session.session);
|
||||
await next();
|
||||
}
|
||||
20
apps/backend/src/lib/auth.ts
Normal file
20
apps/backend/src/lib/auth.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { betterAuth } from "better-auth";
|
||||
import { prismaAdapter } from "better-auth/adapters/prisma";
|
||||
import { prisma } from "./prisma";
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: prismaAdapter(prisma, {
|
||||
provider: "postgresql",
|
||||
}),
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
disableSignUp: true,
|
||||
minPasswordLength: 4,
|
||||
},
|
||||
secret: process.env.BETTER_AUTH_SECRET!,
|
||||
baseURL: process.env.BETTER_AUTH_URL!,
|
||||
trustedOrigins: [
|
||||
process.env.BETTER_AUTH_URL!,
|
||||
"http://localhost:5173",
|
||||
],
|
||||
});
|
||||
@@ -4,6 +4,7 @@
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user