refactor: migrate authentication system and update Drizzle schema.

This commit is contained in:
2025-11-29 02:26:26 -05:00
parent c88e5d9d82
commit 3ebec7aa4a
36 changed files with 603 additions and 440 deletions

View File

@@ -78,7 +78,7 @@ export const invoicesRouter = createTRPCRouter({
});
// Return null if no draft invoice exists
if (!currentInvoice || currentInvoice.status !== "draft") {
if (currentInvoice?.status !== "draft") {
return null;
}

View File

@@ -11,7 +11,7 @@ import { initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson";
import { ZodError } from "zod";
import { auth } from "~/server/auth";
import { auth } from "~/lib/auth";
import { db } from "~/server/db";
/**
@@ -27,7 +27,9 @@ import { db } from "~/server/db";
* @see https://trpc.io/docs/server/context
*/
export const createTRPCContext = async (opts: { headers: Headers }) => {
const session = await auth();
const session = await auth.api.getSession({
headers: opts.headers,
});
return {
db,

View File

@@ -1,119 +0,0 @@
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import { type DefaultSession, type NextAuthConfig } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { eq } from "drizzle-orm";
import bcrypt from "bcryptjs";
import { db } from "~/server/db";
import {
accounts,
sessions,
users,
verificationTokens,
} from "~/server/db/schema";
/**
* Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
* object and keep type safety.
*
* @see https://next-auth.js.org/getting-started/typescript#module-augmentation
*/
declare module "next-auth" {
interface Session extends DefaultSession {
user: {
id: string;
// ...other properties
// role: UserRole;
} & DefaultSession["user"];
}
// interface User {
// // ...other properties
// // role: UserRole;
// }
}
/**
* Options for NextAuth.js used to configure adapters, providers, callbacks, etc.
*
* @see https://next-auth.js.org/configuration/options
*/
export const authConfig = {
session: {
strategy: "jwt",
},
providers: [
CredentialsProvider({
name: "credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
return null;
}
if (
typeof credentials.email !== "string" ||
typeof credentials.password !== "string"
) {
return null;
}
const user = await db.query.users.findFirst({
where: eq(users.email, credentials.email),
});
if (!user?.password) {
return null;
}
const isPasswordValid = await bcrypt.compare(
credentials.password,
user.password,
);
if (!isPasswordValid) {
return null;
}
return {
id: user.id,
email: user.email,
name: user.name,
};
},
}),
],
adapter: DrizzleAdapter(db, {
usersTable: users,
accountsTable: accounts,
sessionsTable: sessions,
verificationTokensTable: verificationTokens,
}),
callbacks: {
session: ({ session, token }) => ({
...session,
user: {
...session.user,
id: token.sub,
},
}),
jwt: ({ token, user }) => {
if (user) {
token.sub = user.id;
}
return token;
},
redirect: ({ url, baseUrl }) => {
// Allows relative callback URLs
if (url.startsWith("/")) return `${baseUrl}${url}`;
// Allows callback URLs on the same origin
else if (new URL(url).origin === baseUrl) return url;
return baseUrl + "/dashboard";
},
},
pages: {
signIn: "/auth/signin",
},
} satisfies NextAuthConfig;

View File

@@ -1,10 +0,0 @@
import NextAuth from "next-auth";
import { cache } from "react";
import { authConfig } from "./config";
const { auth: uncachedAuth, handlers, signIn, signOut } = NextAuth(authConfig);
const auth = cache(uncachedAuth);
export { auth, handlers, signIn, signOut };

View File

@@ -1,6 +1,6 @@
import { relations, sql } from "drizzle-orm";
import { index, primaryKey, pgTableCreator } from "drizzle-orm/pg-core";
import { type AdapterAccount } from "next-auth/adapters";
import { index, pgTableCreator } from "drizzle-orm/pg-core";
/**
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
@@ -17,14 +17,16 @@ export const users = createTable("user", (d) => ({
.notNull()
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
name: d.varchar({ length: 255 }),
email: d.varchar({ length: 255 }).notNull(),
password: d.varchar({ length: 255 }),
emailVerified: d.timestamp().default(sql`CURRENT_TIMESTAMP`),
name: d.varchar({ length: 255 }).notNull(),
email: d.varchar({ length: 255 }).notNull().unique(),
emailVerified: d.boolean().default(false).notNull(),
image: d.varchar({ length: 255 }),
resetToken: d.varchar({ length: 255 }),
createdAt: d.timestamp().notNull().defaultNow(),
updatedAt: d.timestamp().notNull().defaultNow().$onUpdate(() => new Date()),
password: d.varchar({ length: 255 }), // Matched DB: varchar(255)
resetToken: d.varchar({ length: 255 }), // Matched DB: varchar(255)
resetTokenExpiry: d.timestamp(),
// User UI/animation preferences
// Custom fields
prefersReducedMotion: d.boolean().default(false).notNull(),
animationSpeedMultiplier: d.real().default(1).notNull(),
}));
@@ -34,31 +36,31 @@ export const usersRelations = relations(users, ({ many }) => ({
clients: many(clients),
businesses: many(businesses),
invoices: many(invoices),
sessions: many(sessions), // Added missing relation
}));
export const accounts = createTable(
"account",
(d) => ({
id: d.text().notNull().primaryKey().$defaultFn(() => crypto.randomUUID()), // Matched DB: text
userId: d
.varchar({ length: 255 })
.notNull()
.references(() => users.id),
type: d.varchar({ length: 255 }).$type<AdapterAccount["type"]>().notNull(),
provider: d.varchar({ length: 255 }).notNull(),
providerAccountId: d.varchar({ length: 255 }).notNull(),
refresh_token: d.text(),
access_token: d.text(),
expires_at: d.integer(),
token_type: d.varchar({ length: 255 }),
accountId: d.varchar({ length: 255 }).notNull(),
providerId: d.varchar({ length: 255 }).notNull(),
accessToken: d.text(),
refreshToken: d.text(),
accessTokenExpiresAt: d.timestamp(),
refreshTokenExpiresAt: d.timestamp(),
scope: d.varchar({ length: 255 }),
id_token: d.text(),
session_state: d.varchar({ length: 255 }),
idToken: d.text(),
password: d.text(), // Matched DB: text
createdAt: d.timestamp().notNull().defaultNow(),
updatedAt: d.timestamp().notNull().defaultNow().$onUpdate(() => new Date()),
}),
(t) => [
primaryKey({
columns: [t.provider, t.providerAccountId],
}),
index("account_user_id_idx").on(t.userId),
index("account_userId_idx").on(t.userId),
],
);
@@ -69,12 +71,17 @@ export const accountsRelations = relations(accounts, ({ one }) => ({
export const sessions = createTable(
"session",
(d) => ({
sessionToken: d.varchar({ length: 255 }).notNull().primaryKey(),
id: d.text().notNull().primaryKey().$defaultFn(() => crypto.randomUUID()), // Matched DB: text
userId: d
.varchar({ length: 255 })
.notNull()
.references(() => users.id),
expires: d.timestamp().notNull(),
token: d.varchar({ length: 255 }).notNull().unique(),
expiresAt: d.timestamp().notNull(),
ipAddress: d.text(), // Matched DB: text
userAgent: d.text(), // Matched DB: text
createdAt: d.timestamp().notNull().defaultNow(),
updatedAt: d.timestamp().notNull().defaultNow().$onUpdate(() => new Date()),
}),
(t) => [index("session_userId_idx").on(t.userId)],
);
@@ -86,11 +93,14 @@ export const sessionsRelations = relations(sessions, ({ one }) => ({
export const verificationTokens = createTable(
"verification_token",
(d) => ({
id: d.text().notNull().primaryKey().$defaultFn(() => crypto.randomUUID()), // Matched DB: text
identifier: d.varchar({ length: 255 }).notNull(),
token: d.varchar({ length: 255 }).notNull(),
expires: d.timestamp().notNull(),
value: d.varchar({ length: 255 }).notNull(),
expiresAt: d.timestamp().notNull(),
createdAt: d.timestamp().notNull().defaultNow(),
updatedAt: d.timestamp().notNull().defaultNow().$onUpdate(() => new Date()),
}),
(t) => [primaryKey({ columns: [t.identifier, t.token] })],
(t) => [index("verification_token_identifier_idx").on(t.identifier)],
);
// Invoicing app tables