Added Auth

This commit is contained in:
2025-12-22 10:47:15 +07:00
parent 2c244b77fb
commit fa029365d0
79 changed files with 19643 additions and 2830 deletions

29
src/lib/auth-client.ts Normal file
View File

@@ -0,0 +1,29 @@
import { createAuthClient } from 'better-auth/react'
import { adminClient, organizationClient } from 'better-auth/client/plugins'
import { ac, admin, user } from '@/lib/auth/permissions'
import { acOrg, adminOrg, member, owner } from './auth/organization-permissions'
export const authClient = createAuthClient({
baseURL: 'http://localhost:3000',
plugins: [
adminClient({
ac,
roles: { admin, user },
defaultRole: 'user',
}),
organizationClient({
ac: acOrg,
roles: { owner, admin: adminOrg, member },
schema: {
organization: {
additionalFields: {
color: {
type: 'string',
defaultValue: '#000000',
},
},
},
},
}),
],
})

60
src/lib/auth.ts Normal file
View File

@@ -0,0 +1,60 @@
import { betterAuth } from 'better-auth'
import { admin as adminPlugin, organization } from 'better-auth/plugins'
import { prismaAdapter } from 'better-auth/adapters/prisma'
import { prisma } from '@/db'
import { ac, admin, user } from '@/lib/auth/permissions'
import {
acOrg,
adminOrg,
member,
owner,
} from '@/lib/auth/organization-permissions'
export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: 'postgresql',
}),
experimental: { joins: true },
emailAndPassword: {
enabled: true,
requireEmailVerification: false,
},
trustedOrigins: ['http://localhost:3001'],
plugins: [
adminPlugin({
ac,
roles: { admin, user },
defaultRole: 'user',
}),
organization({
ac: acOrg,
roles: { owner, admin: adminOrg, member },
schema: {
organization: {
additionalFields: {
color: {
type: 'string',
defaultValue: '#000000',
},
},
},
},
}),
],
databaseHooks: {
user: {
create: {
after: async (user) => {
await auth.api.createOrganization({
body: {
name: `${user.name || 'User'}'s Organization`,
slug: `${user.name?.toLowerCase().replace(/\s+/g, '-')}-${Date.now()}`,
userId: user.id,
color: '#000000',
},
})
},
},
},
},
})

View File

@@ -0,0 +1,37 @@
import { createAccessControl } from 'better-auth/plugins/access'
import {
defaultStatements,
adminAc,
ownerAc,
} from 'better-auth/plugins/organization/access'
const statement = {
...defaultStatements,
house: ['list', 'create', 'update', 'delete'],
box: ['list', 'create', 'update', 'delete'],
item: ['list', 'create', 'update', 'delete'],
} as const
const acOrg = createAccessControl(statement)
const owner = acOrg.newRole({
...ownerAc.statements,
house: ['list', 'create', 'update', 'delete'],
box: ['list', 'create', 'update', 'delete'],
item: ['list', 'create', 'update', 'delete'],
})
const adminOrg = acOrg.newRole({
...adminAc.statements,
house: ['list', 'create', 'update', 'delete'],
box: ['list', 'create', 'update', 'delete'],
item: ['list', 'create', 'update', 'delete'],
})
const member = acOrg.newRole({
house: ['list', 'create', 'update', 'delete'],
box: ['list', 'create', 'update', 'delete'],
item: ['list', 'create', 'update', 'delete'],
})
export { acOrg, owner, adminOrg, member }

View File

@@ -0,0 +1,29 @@
import { defaultStatements, adminAc } from 'better-auth/plugins/admin/access'
import { createAccessControl } from 'better-auth/plugins/access'
const statement = {
...defaultStatements,
audit: ['list'],
setting: ['list', 'create', 'update', 'delete'],
house: ['list', 'create', 'update', 'delete'],
box: ['list', 'create', 'update', 'delete'],
item: ['list', 'create', 'update', 'delete'],
} as const
const ac = createAccessControl(statement)
const admin = ac.newRole({
...adminAc.statements,
audit: ['list'],
setting: ['list', 'create', 'update', 'delete'],
house: ['list', 'create', 'update', 'delete'],
box: ['list', 'create', 'update', 'delete'],
item: ['list', 'create', 'update', 'delete'],
})
const user = ac.newRole({
setting: ['list', 'update'],
house: ['list', 'create', 'update', 'delete'],
})
export { ac, admin, user }

11
src/lib/auth/session.ts Normal file
View File

@@ -0,0 +1,11 @@
import { createServerFn } from '@tanstack/react-start'
import { getRequestHeaders } from '@tanstack/react-start/server'
import { auth } from '../auth'
export const getSession = createServerFn({ method: 'GET' }).handler(
async () => {
const headers = getRequestHeaders()
const session = await auth.api.getSession({ headers })
return session
},
)

44
src/lib/i18n.ts Normal file
View File

@@ -0,0 +1,44 @@
import { createIsomorphicFn } from '@tanstack/react-start'
import { getCookie } from '@tanstack/react-start/server'
import i18n from 'i18next'
import LanguageDetector from 'i18next-browser-languagedetector'
import { initReactI18next } from 'react-i18next'
import enTranslations from '../locales/en.json'
import viTranslations from '../locales/vi.json'
export const resources = {
en: {
translation: enTranslations,
},
vi: {
translation: viTranslations,
},
} as const
export const defaultNS = 'translation'
const i18nCookieName = 'i18nextLng'
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources,
defaultNS,
fallbackLng: 'vi',
supportedLngs: ['en', 'vi'],
detection: {
order: ['cookie'],
lookupCookie: i18nCookieName,
caches: ['cookie'],
cookieMinutes: 60 * 24 * 365,
},
interpolation: { escapeValue: false },
})
export const setSSRLanguage = createIsomorphicFn().server(async () => {
const language = getCookie(i18nCookieName)
await i18n.changeLanguage(language || 'vi')
})
export default i18n

12
src/lib/i18next.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
import 'i18next'
import translation from '../locales/vi.json'
import { defaultNS } from './i18n'
declare module 'i18next' {
interface CustomTypeOptions {
defaultNS: typeof defaultNS
resources: {
translation: typeof translation
}
}
}

15
src/lib/middleware.ts Normal file
View File

@@ -0,0 +1,15 @@
import { createMiddleware } from '@tanstack/react-start'
import { auth } from './auth'
import { redirect } from '@tanstack/react-router'
export const authMiddleware = createMiddleware().server(
async ({ next, request }) => {
const session = await auth.api.getSession({
headers: request.headers,
})
if (!session) {
throw redirect({ to: '/sign-in' })
}
return await next()
},
)

View File

@@ -1,6 +1,7 @@
import { clsx, type ClassValue } from 'clsx'
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
import type { ClassValue } from 'clsx'
export function cn(...inputs: ClassValue[]) {
export function cn(...inputs: Array<ClassValue>) {
return twMerge(clsx(inputs))
}