actaeon/src/auth.ts
2024-03-24 21:47:52 -04:00

107 lines
3.0 KiB
TypeScript

import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { db, GeneratedDB } from '@/db';
import bcrypt from 'bcrypt';
import { DBUserPayload } from '@/types/user';
import { cache } from 'react';
import { SelectQueryBuilder, sql } from 'kysely';
import { AimeUser } from '@/types/db';
import crypto from 'crypto';
let basePath = process.env.BASE_PATH ?? '';
if (basePath.endsWith('/')) basePath = basePath.slice(0, -1);
const selectUserProps = (builder: SelectQueryBuilder<GeneratedDB & { u: AimeUser }, 'u', {}>) => builder.leftJoin(
eb => eb.selectFrom('chuni_profile_data as chuni')
.where(({ eb, selectFrom }) => eb('chuni.version', '=', selectFrom('chuni_static_music')
.select(({ fn }) => fn.max('version').as('latest'))))
.selectAll()
.as('chuni'),
join => join.onRef('chuni.user', '=', 'u.id'))
.leftJoin('actaeon_user_ext as ext', 'ext.userId', 'u.id')
.select(({ fn }) => [
'u.username', 'u.password', 'u.id', 'u.email', 'u.permissions', 'u.created_date', 'u.last_login_date',
'u.suspend_expire_time',
'ext.uuid',
'ext.visibility',
'ext.homepage',
'ext.team',
fn<boolean>('not isnull', ['chuni.id']).as('chuni')
])
.executeTakeFirst();
const nextAuth = NextAuth({
pages: {
signIn: `${basePath}/auth/login`
},
basePath: `${basePath}/api/auth/`,
session: {
strategy: 'jwt'
},
trustHost: true,
callbacks: {
async jwt({ token, user }) {
token.user ??= user;
const dbUser = await selectUserProps(db.selectFrom('aime_user as u')
.where('u.id', '=', (token.user as any).id));
if (dbUser) {
const { password, ...payload } = dbUser;
token.user = { ...(token.user as any), ...payload };
}
return token;
},
session({ session, token, user }) {
session.user = { ...session.user, ...(token.user as any) };
return session;
},
async signIn({ user }) {
if ((user as any).visibility === null) {
const uuid = crypto.randomUUID();
await db.insertInto('actaeon_user_ext')
.values({
userId: (user as any).id,
uuid,
visibility: 0
})
.executeTakeFirst();
(user as any).uuid = uuid;
(user as any).visibility = 0;
}
return true;
}
},
providers: [CredentialsProvider({
name: 'Credentials',
credentials: {
username: { label: 'Username', type: 'text', placeholder: 'Username' },
password: { label: 'Password', type: 'password' }
},
async authorize({ username, password }, req) {
if (typeof username !== 'string' || typeof password !== 'string')
return null;
const user = await selectUserProps(db.selectFrom('aime_user as u')
.where(({ eb, fn }) =>
eb(fn<string>('lower', ['u.username']), '=', username.toLowerCase().trim())));
if (!user?.password || !await bcrypt.compare(password.trim(), user.password))
return null;
const { password: _, ...payload } = user satisfies { [K in keyof DBUserPayload]: DBUserPayload[K] | null };
return payload as any;
}
})]
});
export const auth = cache(nextAuth.auth);
export const {
handlers: { GET, POST },
signIn,
signOut
} = nextAuth;