feat: add credentials authentication (#1)
This commit is contained in:
1
packages/auth/client.ts
Normal file
1
packages/auth/client.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { signIn, signOut } from "next-auth/react";
|
||||
76
packages/auth/configuration.ts
Normal file
76
packages/auth/configuration.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { cookies } from "next/headers";
|
||||
import { DrizzleAdapter } from "@auth/drizzle-adapter";
|
||||
import NextAuth from "next-auth";
|
||||
import Credentials from "next-auth/providers/credentials";
|
||||
|
||||
import { db } from "@alparr/db";
|
||||
|
||||
import { credentialsConfiguration } from "./providers/credentials";
|
||||
import { EmptyNextAuthProvider } from "./providers/empty";
|
||||
import { expireDateAfter, generateSessionToken } from "./session";
|
||||
|
||||
const adapter = DrizzleAdapter(db);
|
||||
const sessionMaxAgeInSeconds = 30 * 24 * 60 * 60; // 30 days
|
||||
|
||||
export const createConfiguration = (isCredentialsRequest: boolean) =>
|
||||
NextAuth({
|
||||
adapter,
|
||||
providers: [Credentials(credentialsConfiguration), EmptyNextAuthProvider()],
|
||||
callbacks: {
|
||||
session: ({ session, user }) => ({
|
||||
...session,
|
||||
user: {
|
||||
...session.user,
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
},
|
||||
}),
|
||||
signIn: async ({ user }) => {
|
||||
if (!isCredentialsRequest) return true;
|
||||
|
||||
if (!user) return true;
|
||||
|
||||
const sessionToken = generateSessionToken();
|
||||
const sessionExpiry = expireDateAfter(sessionMaxAgeInSeconds);
|
||||
|
||||
// https://github.com/nextauthjs/next-auth/issues/6106
|
||||
if (!adapter?.createSession) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await adapter.createSession({
|
||||
sessionToken: sessionToken,
|
||||
userId: user.id,
|
||||
expires: sessionExpiry,
|
||||
});
|
||||
|
||||
cookies().set("next-auth.session-token", sessionToken, {
|
||||
path: "/",
|
||||
expires: sessionExpiry,
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: true,
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
session: {
|
||||
strategy: "database",
|
||||
maxAge: sessionMaxAgeInSeconds,
|
||||
},
|
||||
pages: {
|
||||
signIn: "/auth/login",
|
||||
error: "/auth/login",
|
||||
},
|
||||
jwt: {
|
||||
encode() {
|
||||
const cookie = cookies().get("next-auth.session-token")?.value;
|
||||
return cookie ?? "";
|
||||
},
|
||||
|
||||
decode() {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -3,8 +3,6 @@ import { z } from "zod";
|
||||
|
||||
export const env = createEnv({
|
||||
server: {
|
||||
AUTH_DISCORD_ID: z.string().min(1),
|
||||
AUTH_DISCORD_SECRET: z.string().min(1),
|
||||
AUTH_SECRET:
|
||||
process.env.NODE_ENV === "production"
|
||||
? z.string().min(1)
|
||||
@@ -19,8 +17,6 @@ export const env = createEnv({
|
||||
},
|
||||
client: {},
|
||||
runtimeEnv: {
|
||||
AUTH_DISCORD_ID: process.env.AUTH_DISCORD_ID,
|
||||
AUTH_DISCORD_SECRET: process.env.AUTH_DISCORD_SECRET,
|
||||
AUTH_SECRET: process.env.AUTH_SECRET,
|
||||
AUTH_URL: process.env.AUTH_URL,
|
||||
},
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
/* @see https://github.com/nextauthjs/next-auth/pull/8932 */
|
||||
|
||||
import Discord from "@auth/core/providers/discord";
|
||||
import type { DefaultSession } from "@auth/core/types";
|
||||
import { DrizzleAdapter } from "@auth/drizzle-adapter";
|
||||
import NextAuth from "next-auth";
|
||||
|
||||
import { db, tableCreator } from "@alparr/db";
|
||||
import { createConfiguration } from "./configuration";
|
||||
|
||||
export type { Session } from "next-auth";
|
||||
|
||||
@@ -18,21 +12,8 @@ declare module "next-auth" {
|
||||
}
|
||||
}
|
||||
|
||||
export const {
|
||||
handlers: { GET, POST },
|
||||
auth,
|
||||
signIn,
|
||||
signOut,
|
||||
} = NextAuth({
|
||||
adapter: DrizzleAdapter(db, tableCreator),
|
||||
providers: [Discord],
|
||||
callbacks: {
|
||||
session: ({ session, user }) => ({
|
||||
...session,
|
||||
user: {
|
||||
...session.user,
|
||||
id: user.id,
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
export * from "./security";
|
||||
|
||||
export const createHandlers = (isCredentialsRequest: boolean) =>
|
||||
createConfiguration(isCredentialsRequest);
|
||||
export const { auth } = createConfiguration(false);
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
"@auth/core": "^0.18.4",
|
||||
"@auth/drizzle-adapter": "^0.3.9",
|
||||
"@t3-oss/env-nextjs": "^0.7.1",
|
||||
"bcrypt": "^5.1.1",
|
||||
"cookies": "^0.8.0",
|
||||
"next": "^14.0.3",
|
||||
"next-auth": "5.0.0-beta.4",
|
||||
"react": "18.2.0",
|
||||
@@ -26,6 +28,9 @@
|
||||
"@alparr/eslint-config": "workspace:^0.2.0",
|
||||
"@alparr/prettier-config": "workspace:^0.1.0",
|
||||
"@alparr/tsconfig": "workspace:^0.1.0",
|
||||
"@alparr/validation": "workspace:^0.1.0",
|
||||
"@types/bcrypt": "5.0.2",
|
||||
"@types/cookies": "0.7.10",
|
||||
"eslint": "^8.53.0",
|
||||
"prettier": "^3.1.0",
|
||||
"typescript": "^5.3.3"
|
||||
@@ -37,4 +42,4 @@
|
||||
]
|
||||
},
|
||||
"prettier": "@alparr/prettier-config"
|
||||
}
|
||||
}
|
||||
|
||||
49
packages/auth/providers/credentials.ts
Normal file
49
packages/auth/providers/credentials.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type Credentials from "@auth/core/providers/credentials";
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
import { db, eq } from "@alparr/db";
|
||||
import { users } from "@alparr/db/schema/sqlite";
|
||||
import { signInSchema } from "@alparr/validation";
|
||||
|
||||
type CredentialsConfiguration = Parameters<typeof Credentials>[0];
|
||||
|
||||
export const credentialsConfiguration = {
|
||||
type: "credentials",
|
||||
name: "Credentials",
|
||||
credentials: {
|
||||
name: {
|
||||
label: "Username",
|
||||
type: "text",
|
||||
},
|
||||
password: {
|
||||
label: "Password",
|
||||
type: "password",
|
||||
},
|
||||
},
|
||||
async authorize(credentials) {
|
||||
const data = await signInSchema.parseAsync(credentials);
|
||||
|
||||
const user = await db.query.users.findFirst({
|
||||
where: eq(users.name, data.name),
|
||||
});
|
||||
|
||||
if (!user?.password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`user ${user.name} is trying to log in. checking password...`);
|
||||
const isValidPassword = await bcrypt.compare(data.password, user.password);
|
||||
|
||||
if (!isValidPassword) {
|
||||
console.log(`password for user ${user.name} was incorrect`);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`user ${user.name} successfully authorized`);
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
};
|
||||
},
|
||||
} satisfies CredentialsConfiguration;
|
||||
16
packages/auth/providers/empty.ts
Normal file
16
packages/auth/providers/empty.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { OAuthConfig } from "next-auth/providers";
|
||||
|
||||
export function EmptyNextAuthProvider(): OAuthConfig<unknown> {
|
||||
return {
|
||||
id: "empty",
|
||||
name: "Empty",
|
||||
type: "oauth",
|
||||
profile: () => {
|
||||
throw new Error(
|
||||
"EmptyNextAuthProvider can not be used and is only a placeholder because credentials authentication can not be used as session authentication without additional providers.",
|
||||
);
|
||||
},
|
||||
issuer: "empty",
|
||||
authorization: new URL("https://example.empty"),
|
||||
};
|
||||
}
|
||||
9
packages/auth/security.ts
Normal file
9
packages/auth/security.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
export const createSalt = async () => {
|
||||
return bcrypt.genSalt(10);
|
||||
};
|
||||
|
||||
export const hashPassword = async (password: string, salt: string) => {
|
||||
return bcrypt.hash(password, salt);
|
||||
};
|
||||
9
packages/auth/session.ts
Normal file
9
packages/auth/session.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
export const expireDateAfter = (seconds: number) => {
|
||||
return new Date(Date.now() + seconds * 1000);
|
||||
};
|
||||
|
||||
export const generateSessionToken = () => {
|
||||
return randomUUID();
|
||||
};
|
||||
Reference in New Issue
Block a user