| 1 | import { Auth, type AuthConfig, createActionURL, setEnvDefaults } from "@auth/core";
|
|---|
| 2 | import CredentialsProvider from "@auth/core/providers/credentials";
|
|---|
| 3 | import type { Session } from "@auth/core/types";
|
|---|
| 4 | import { enhance, type UniversalHandler, type UniversalMiddleware } from "@universal-middleware/core";
|
|---|
| 5 | import {eq, or} from "drizzle-orm";
|
|---|
| 6 | import { db } from "../database/drizzle/db";
|
|---|
| 7 | import {adminsTable, usersTable} from "../database/drizzle/schema";
|
|---|
| 8 | import bcrypt from "bcrypt";
|
|---|
| 9 | import { AuthDrizzleAdapter } from "./drizzle/auth-adapter";
|
|---|
| 10 |
|
|---|
| 11 | const authjsConfig = {
|
|---|
| 12 | basePath: "/api/auth",
|
|---|
| 13 | trustHost: true,
|
|---|
| 14 | secret: process.env.AUTH_SECRET,
|
|---|
| 15 | adapter: AuthDrizzleAdapter(),
|
|---|
| 16 | session: {
|
|---|
| 17 | strategy: "jwt",
|
|---|
| 18 | },
|
|---|
| 19 | providers: [
|
|---|
| 20 | CredentialsProvider({
|
|---|
| 21 | name: "Credentials",
|
|---|
| 22 | credentials: {
|
|---|
| 23 | username: { label: "Username", type: "text" },
|
|---|
| 24 | password: { label: "Password", type: "password" },
|
|---|
| 25 | },
|
|---|
| 26 | async authorize(credentials) {
|
|---|
| 27 | const username = typeof credentials?.username === "string" ? credentials.username : null;
|
|---|
| 28 | const password = typeof credentials?.password === "string" ? credentials.password : null;
|
|---|
| 29 |
|
|---|
| 30 | if(!username || !password || typeof username !== 'string' || typeof password !== 'string') { return null; }
|
|---|
| 31 |
|
|---|
| 32 | const trimmedUsername = username.trim();
|
|---|
| 33 |
|
|---|
| 34 | const user = await db.query.usersTable.findFirst({
|
|---|
| 35 | where: or(
|
|---|
| 36 | eq(usersTable.username, trimmedUsername),
|
|---|
| 37 | eq(usersTable.email, trimmedUsername)
|
|---|
| 38 | ),
|
|---|
| 39 | });
|
|---|
| 40 |
|
|---|
| 41 | if(!user) return null;
|
|---|
| 42 |
|
|---|
| 43 | let isPasswordValid = false;
|
|---|
| 44 |
|
|---|
| 45 | if (user.passwordHash.startsWith('$2b$') || user.passwordHash.startsWith('$2a$')) {
|
|---|
| 46 | isPasswordValid = await bcrypt.compare(password, user.passwordHash);
|
|---|
| 47 | } else {
|
|---|
| 48 | isPasswordValid = password === user.passwordHash;
|
|---|
| 49 | }
|
|---|
| 50 |
|
|---|
| 51 | if(!isPasswordValid) return null;
|
|---|
| 52 |
|
|---|
| 53 | const admin = await db.query.adminsTable.findFirst({
|
|---|
| 54 | where: eq(adminsTable.userId, user.id),
|
|---|
| 55 | });
|
|---|
| 56 |
|
|---|
| 57 | return {
|
|---|
| 58 | id: String(user.id),
|
|---|
| 59 | username: user.username,
|
|---|
| 60 | email: user.email,
|
|---|
| 61 | isAdmin: !!admin
|
|---|
| 62 | }
|
|---|
| 63 | },
|
|---|
| 64 | }),
|
|---|
| 65 | ],
|
|---|
| 66 |
|
|---|
| 67 | callbacks: {
|
|---|
| 68 | async jwt({ token, user }) {
|
|---|
| 69 | if (user) {
|
|---|
| 70 | token.sub = user.id;
|
|---|
| 71 |
|
|---|
| 72 | const customUser = user as any;
|
|---|
| 73 | token.username = customUser.username;
|
|---|
| 74 | token.email = customUser.email;
|
|---|
| 75 | token.isAdmin = customUser.isAdmin;
|
|---|
| 76 | }
|
|---|
| 77 | return token;
|
|---|
| 78 | },
|
|---|
| 79 | async session({ session, token }) {
|
|---|
| 80 | if (session.user) {
|
|---|
| 81 | session.user.id = token.sub!;
|
|---|
| 82 | session.user.name = token.username as string;
|
|---|
| 83 |
|
|---|
| 84 | const customToken = token as any;
|
|---|
| 85 | session.user.email = customToken.email;
|
|---|
| 86 | session.user.isAdmin = customToken.isAdmin;
|
|---|
| 87 | }
|
|---|
| 88 | return session;
|
|---|
| 89 | },
|
|---|
| 90 | },
|
|---|
| 91 | } satisfies Omit<AuthConfig, "raw">;
|
|---|
| 92 |
|
|---|
| 93 | export async function getSession(req: Request, config: Omit<AuthConfig, "raw">): Promise<Session | null> {
|
|---|
| 94 | setEnvDefaults(process.env, config);
|
|---|
| 95 | const requestURL = new URL(req.url);
|
|---|
| 96 | const url = createActionURL("session", requestURL.protocol, req.headers, process.env, config);
|
|---|
| 97 |
|
|---|
| 98 | const response = await Auth(new Request(url, { headers: { cookie: req.headers.get("cookie") ?? "" } }), config);
|
|---|
| 99 |
|
|---|
| 100 | const { status = 200 } = response;
|
|---|
| 101 |
|
|---|
| 102 | const data = await response.json();
|
|---|
| 103 |
|
|---|
| 104 | if (!data || !Object.keys(data).length) return null;
|
|---|
| 105 | if (status === 200) return data as Session;
|
|---|
| 106 | throw new Error(typeof data === "object" && "message" in data ? (data.message as string) : undefined);
|
|---|
| 107 | }
|
|---|
| 108 |
|
|---|
| 109 | export const authjsSessionMiddleware: UniversalMiddleware = enhance(
|
|---|
| 110 | async (request, context) => {
|
|---|
| 111 | try {
|
|---|
| 112 | return {
|
|---|
| 113 | ...context,
|
|---|
| 114 | session: await getSession(request, authjsConfig),
|
|---|
| 115 | };
|
|---|
| 116 | } catch (error) {
|
|---|
| 117 | console.debug("authjsSessionMiddleware:", error);
|
|---|
| 118 | return {
|
|---|
| 119 | ...context,
|
|---|
| 120 | session: null,
|
|---|
| 121 | };
|
|---|
| 122 | }
|
|---|
| 123 | },
|
|---|
| 124 | {
|
|---|
| 125 | name: "pc-forge:authjs-middleware",
|
|---|
| 126 | immutable: false,
|
|---|
| 127 | },
|
|---|
| 128 | );
|
|---|
| 129 |
|
|---|
| 130 | export const authjsHandler = enhance(
|
|---|
| 131 | async (request) => {
|
|---|
| 132 | return Auth(request, authjsConfig);
|
|---|
| 133 | },
|
|---|
| 134 | {
|
|---|
| 135 | name: "pc-forge:authjs-handler",
|
|---|
| 136 | path: "/api/auth/**",
|
|---|
| 137 | method: ["GET", "POST"],
|
|---|
| 138 | immutable: false,
|
|---|
| 139 | },
|
|---|
| 140 | ) satisfies UniversalHandler;
|
|---|