feat: WIP add better-auth

pull/715/head
Travis Fischer 2025-05-22 01:56:54 +07:00
rodzic 6a19da284a
commit 07dc9e60d1
23 zmienionych plików z 1045 dodań i 260 usunięć

Wyświetl plik

@ -1,6 +0,0 @@
# ------------------------------------------------------------------------------
# This is an example .env file.
#
# All of these environment vars must be defined either in your environment or in
# a local .env file in order to run this project.
# ------------------------------------------------------------------------------

Wyświetl plik

@ -7,10 +7,15 @@
DATABASE_URL=
JWT_SECRET=
BETTER_AUTH_SECRET=
BETTER_AUTH_URL=
JWT_SECRET=
SENTRY_DSN=
STRIPE_PUBLISHABLE_KEY=
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=

Wyświetl plik

@ -34,6 +34,7 @@
"@hono/zod-openapi": "^0.19.6",
"@redocly/openapi-core": "^1.34.3",
"@sentry/node": "^9.19.0",
"better-auth": "^1.2.8",
"eventid": "^2.0.1",
"exit-hook": "catalog:",
"hono": "^4.7.9",

Wyświetl plik

@ -2,6 +2,7 @@ import { OpenAPIHono } from '@hono/zod-openapi'
import { fromError } from 'zod-validation-error'
import type { AuthenticatedEnv } from '@/lib/types'
import { auth } from '@/lib/auth'
import * as middleware from '@/lib/middleware'
import { registerOpenAPIErrorResponses } from '@/lib/openapi-utils'
@ -17,6 +18,7 @@ import { registerV1DeploymentsListDeployments } from './deployments/list-deploym
import { registerV1DeploymentsPublishDeployment } from './deployments/publish-deployment'
import { registerV1DeploymentsUpdateDeployment } from './deployments/update-deployment'
import { registerHealthCheck } from './health-check'
// import { registerV1OAuthRedirect } from './oauth-redirect'
import { registerV1ProjectsCreateProject } from './projects/create-project'
import { registerV1ProjectsGetProject } from './projects/get-project'
import { registerV1ProjectsListProjects } from './projects/list-projects'
@ -98,11 +100,16 @@ registerV1DeploymentsUpdateDeployment(privateRouter)
registerV1DeploymentsListDeployments(privateRouter)
registerV1DeploymentsPublishDeployment(privateRouter)
// Internal admin routes
registerV1AdminConsumersGetConsumerByToken(privateRouter)
// Webhook event handlers
registerV1StripeWebhook(publicRouter)
// Admin routes
registerV1AdminConsumersGetConsumerByToken(privateRouter)
// OAuth redirect
// registerV1OAuthRedirect(publicRouter)
publicRouter.on(['POST', 'GET'], 'auth/**', (c) => auth.handler(c.req.raw))
// Setup routes and middleware
apiV1.route('/', publicRouter)

Wyświetl plik

@ -0,0 +1,42 @@
import type { OpenAPIHono } from '@hono/zod-openapi'
import { assert } from '@agentic/platform-core'
// TODO: Unused in favor of `better-auth`
export function registerV1OAuthRedirect(app: OpenAPIHono) {
return app.all('oauth', async (ctx) => {
if (ctx.req.query('state')) {
const { state: state64, ...query } = ctx.req.query()
// google oauth + others
const { uri, ...state } = JSON.parse(
Buffer.from(state64!, 'base64').toString()
) as any
assert(
uri,
404,
`Error oauth redirect not found "${new URLSearchParams(ctx.req.query()).toString()}"`
)
const searchParams = new URLSearchParams({
...state,
...query
})
ctx.redirect(`${uri}?${searchParams.toString()}`)
} else {
// github oauth
// https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/#redirect-urls
const { uri, ...params } = ctx.req.query()
assert(
uri,
404,
`Error oauth redirect not found "${new URLSearchParams(ctx.req.query()).toString()}"`
)
const searchParams = new URLSearchParams(params)
ctx.redirect(`${uri}?${searchParams.toString()}`)
}
})
}

Wyświetl plik

@ -1,27 +0,0 @@
import type { hc } from 'hono/client'
import { expectTypeOf, test } from 'vitest'
import type { User } from '@/db'
import type { ApiRoutes } from './index'
type ApiClient = ReturnType<typeof hc<ApiRoutes>>
type GetUserResponse = Awaited<
ReturnType<Awaited<ReturnType<ApiClient['users'][':userId']['$get']>>['json']>
>
test('User types are compatible', async () => {
expectTypeOf<GetUserResponse>().toEqualTypeOf<User>()
// const client = hc<ApiRoutes>('http://localhost:3000/v1')
// const user = await client.users[':userId'].$post({
// param: {
// userId: '123'
// },
// json: {
// firstName: 'John'
// }
// })
})

Wyświetl plik

@ -12,7 +12,7 @@ const relevantStripeEvents = new Set<Stripe.Event.Type>([
])
export function registerV1StripeWebhook(app: OpenAPIHono) {
return app.post('/webhooks/stripe', async (ctx) => {
return app.post('webhooks/stripe', async (ctx) => {
const body = await ctx.req.text()
const signature = ctx.req.header('Stripe-Signature')
assert(signature, 400, 'missing signature')

Wyświetl plik

@ -0,0 +1,61 @@
// import { validators } from '@agentic/platform-validators'
import { betterAuth } from 'better-auth'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { createIdForModel, db } from '@/db'
import { env } from './env'
export const auth = betterAuth({
adapter: drizzleAdapter(db, {
provider: 'pg'
}),
emailAndPassword: {
enabled: true
},
socialProviders: {
github: {
clientId: env.GITHUB_CLIENT_ID,
clientSecret: env.GITHUB_CLIENT_SECRET
}
},
user: {
modelName: 'users',
additionalFields: {
role: {
type: 'string',
required: false,
defaultValue: 'user',
input: false // don't allow user to set role
},
// username: {
// type: 'string',
// required: false
// },
stripeCustomerId: {
type: 'string',
required: false
}
}
},
session: {
modelName: 'sessions'
},
account: {
modelName: 'accounts'
},
verification: {
modelName: 'verifications'
},
advanced: {
database: {
generateId: ({ model }) => createIdForModel(model as any)
}
}
// TODO
// plugins: [
// username({
// usernameValidator: validators.username
// })
// ]
})

Wyświetl plik

@ -12,14 +12,19 @@ export const envSchema = z.object({
DATABASE_URL: z.string().url(),
BETTER_AUTH_SECRET: z.string().nonempty(),
BETTER_AUTH_URL: z.string().url(),
JWT_SECRET: z.string().nonempty(),
SENTRY_DSN: z.string().url(),
PORT: z.number().default(3000),
LOG_LEVEL: logLevelsSchema.default('info'),
STRIPE_SECRET_KEY: z.string().nonempty(),
STRIPE_PUBLISHABLE_KEY: z.string().nonempty(),
STRIPE_WEBHOOK_SECRET: z.string().nonempty()
STRIPE_WEBHOOK_SECRET: z.string().nonempty(),
GITHUB_CLIENT_ID: z.string().nonempty(),
GITHUB_CLIENT_SECRET: z.string().nonempty()
})
export type Env = z.infer<typeof envSchema>
@ -32,4 +37,4 @@ export const isDev = env.NODE_ENV === 'development'
export const isProd = env.NODE_ENV === 'production'
export const isBrowser = (globalThis as any).window !== undefined
export const isStripeLive = env.STRIPE_PUBLISHABLE_KEY.startsWith('pk_live_')
export const isStripeLive = env.STRIPE_SECRET_KEY.startsWith('sk_live_')

Wyświetl plik

@ -0,0 +1,158 @@
import ky from 'ky'
import { env } from './env'
const USER_AGENT = 'agentic-platform'
/**
* GitHub (user-level) OAuth token response.
*
* @see https://docs.github.com/apps/oauth
*/
export interface GitHubUserTokenResponse {
/**
* The user access token (always starts with `ghu_`).
* Example: `ghu_xxx…`
*/
access_token: string
/**
* Seconds until `access_token` expires.
* Omitted (`undefined`) if youve disabled token expiration.
* Constant `28800` (8 hours) when present.
*/
expires_in?: number
/**
* Refresh token for renewing the user access token (starts with `ghr_`).
* Omitted (`undefined`) if youve disabled token expiration.
*/
refresh_token?: string
/**
* Seconds until `refresh_token` expires.
* Omitted (`undefined`) if youve disabled token expiration.
* Constant `15897600` (6 months) when present.
*/
refresh_token_expires_in?: number
/**
* Scopes granted to the token.
* Always an empty string because the token is limited to
* the intersection of app-level and user-level permissions.
*/
scope: ''
/**
* Token type always `'bearer'`.
*/
token_type: 'bearer'
}
export interface GitHubUser {
login: string
id: number
user_view_type?: string
node_id: string
avatar_url: string
gravatar_id: string | null
url: string
html_url: string
followers_url: string
following_url: string
gists_url: string
starred_url: string
subscriptions_url: string
organizations_url: string
repos_url: string
events_url: string
received_events_url: string
type: string
site_admin: boolean
name: string | null
company: string | null
blog: string | null
location: string | null
email: string | null
notification_email?: string | null
hireable: boolean | null
bio: string | null
twitter_username?: string | null
public_repos: number
public_gists: number
followers: number
following: number
created_at: string
updated_at: string
plan?: {
collaborators: number
name: string
space: number
private_repos: number
[k: string]: unknown
}
private_gists?: number
total_private_repos?: number
owned_private_repos?: number
disk_usage?: number
collaborators?: number
}
export interface GitHubUserEmail {
email: string
primary: boolean
verified: boolean
visibility?: string | null
}
export async function exchangeOAuthCodeForAccessToken({
code,
clientId = env.GITHUB_CLIENT_ID,
clientSecret = env.GITHUB_CLIENT_SECRET,
redirectUri
}: {
code: string
clientId?: string
clientSecret?: string
redirectUri?: string
}): Promise<GitHubUserTokenResponse> {
return ky
.post('https://github.com/login/oauth/access_token', {
json: {
code,
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectUri
},
headers: {
'user-agent': USER_AGENT
}
})
.json<GitHubUserTokenResponse>()
}
export async function getMe({ token }: { token: string }): Promise<GitHubUser> {
return ky
.get('https://api.github.com/user', {
headers: {
Authorization: `Bearer ${token}`,
'user-agent': USER_AGENT
}
})
.json<GitHubUser>()
}
export async function getUserEmails({
token
}: {
token: string
}): Promise<GitHubUserEmail[]> {
return ky
.get('https://api.github.com/user/emails', {
headers: {
Authorization: `Bearer ${token}`,
'user-agent': USER_AGENT
}
})
.json<GitHubUserEmail[]>()
}

Wyświetl plik

@ -1,42 +1,55 @@
import { assert } from '@agentic/platform-core'
import { createMiddleware } from 'hono/factory'
import * as jwt from 'hono/jwt'
// import * as jwt from 'hono/jwt'
import type { AuthenticatedEnv } from '@/lib/types'
import { db, eq, schema } from '@/db'
import { env } from '@/lib/env'
import { auth } from '@/lib/auth'
export const authenticate = createMiddleware<AuthenticatedEnv>(
async function authenticateMiddleware(ctx, next) {
const credentials = ctx.req.raw.headers.get('Authorization')
assert(credentials, 401, 'Unauthorized')
const parts = credentials.split(/\s+/)
assert(
parts.length === 1 ||
(parts.length === 2 && parts[0]?.toLowerCase() === 'bearer'),
401,
'Unauthorized'
)
const token = parts.at(-1)
assert(token, 401, 'Unauthorized')
const payload = await jwt.verify(token, env.JWT_SECRET)
assert(payload, 401, 'Unauthorized')
assert(payload.type === 'user', 401, 'Unauthorized')
assert(
payload.userId && typeof payload.userId === 'string',
401,
'Unauthorized'
)
ctx.set('userId', payload.userId)
const user = await db.query.users.findFirst({
where: eq(schema.users.id, payload.userId)
const session = await auth.api.getSession({
// TODO: investigate this type issue
headers: ctx.req.raw.headers as any
})
assert(user, 401, 'Unauthorized')
ctx.set('user', user as any)
assert(session, 401, 'Unauthorized')
assert(session.user?.id, 401, 'Unauthorized')
assert(session.session, 401, 'Unauthorized')
ctx.set('userId', session.user.id)
ctx.set('user', session.user)
ctx.set('session', session.session)
await next()
// const credentials = ctx.req.raw.headers.get('Authorization')
// assert(credentials, 401, 'Unauthorized')
// const parts = credentials.split(/\s+/)
// assert(
// parts.length === 1 ||
// (parts.length === 2 && parts[0]?.toLowerCase() === 'bearer'),
// 401,
// 'Unauthorized'
// )
// const token = parts.at(-1)
// assert(token, 401, 'Unauthorized')
// const payload = await jwt.verify(token, env.JWT_SECRET)
// assert(payload, 401, 'Unauthorized')
// assert(payload.type === 'user', 401, 'Unauthorized')
// assert(
// payload.userId && typeof payload.userId === 'string',
// 401,
// 'Unauthorized'
// )
// ctx.set('userId', payload.userId)
// const user = await db.query.users.findFirst({
// where: eq(schema.users.id, payload.userId)
// })
// assert(user, 401, 'Unauthorized')
// ctx.set('user', user as any)
// await next()
}
)

Wyświetl plik

@ -12,13 +12,13 @@ import { aclTeamMember } from '@/lib/acl-team-member'
export const team = createMiddleware<AuthenticatedEnv>(
async function teamMiddleware(ctx, next) {
const teamId = ctx.req.query('teamId')
const user = ctx.get('user')
const userId = ctx.get('userId')
if (teamId && user) {
if (teamId && userId) {
const teamMember = await db.query.teamMembers.findFirst({
where: and(
eq(schema.teamMembers.teamId, teamId),
eq(schema.teamMembers.userId, user.id)
eq(schema.teamMembers.userId, userId)
)
})
assert(teamMember, 403, 'Unauthorized')

Wyświetl plik

@ -1,7 +1,8 @@
import type { Context } from 'hono'
import type { RawTeamMember, RawUser } from '@/db'
import type { RawTeamMember } from '@/db'
import type { auth } from './auth'
import type { Env } from './env'
import type { Logger } from './logger'
@ -10,6 +11,9 @@ export type { OpenAPI3 as LooseOpenAPI3Spec } from 'openapi-typescript'
export type Environment = Env['NODE_ENV']
export type Service = 'api'
export type AuthUser = typeof auth.$Infer.Session.user
export type AuthSession = typeof auth.$Infer.Session.session
export type DefaultEnvVariables = {
requestId: string
logger: Logger
@ -17,7 +21,8 @@ export type DefaultEnvVariables = {
export type AuthenticatedEnvVariables = DefaultEnvVariables & {
userId: string
user?: RawUser
user?: AuthUser
session?: AuthSession
teamMember?: RawTeamMember
}

Wyświetl plik

@ -1,8 +1,8 @@
import type { Simplify } from 'type-fest'
import { getEnv, sanitizeSearchParams } from '@agentic/platform-core'
import defaultKy, { type KyInstance } from 'ky'
import type { operations } from './openapi'
import { getEnv, sanitizeSearchParams } from './utils'
export class AgenticApiClient {
static readonly DEFAULT_API_BASE_URL = 'https://api.agentic.so'

Wyświetl plik

@ -1,57 +0,0 @@
export function getEnv(name: string): string | undefined {
try {
return typeof process !== 'undefined'
? // eslint-disable-next-line no-process-env
process.env?.[name]
: undefined
} catch {
return undefined
}
}
/**
* Creates a new `URLSearchParams` object with all values coerced to strings
* that correctly handles arrays of values as repeated keys (or CSV) and
* correctly removes `undefined` keys and values.
*/
export function sanitizeSearchParams(
searchParams:
| Record<
string,
string | number | boolean | string[] | number[] | boolean[] | undefined
>
| object,
{
csv = false
}: {
/**
* Whether to use comma-separated-values for arrays or multiple entries.
*
* Defaults to `false` and will use multiple entries.
*/
csv?: boolean
} = {}
): URLSearchParams {
const entries = Object.entries(searchParams).flatMap(([key, value]) => {
if (key === undefined || value === undefined) {
return []
}
if (Array.isArray(value)) {
return value.map((v) => [key, String(v)])
}
return [[key, String(value)]]
}) as [string, string][]
if (!csv) {
return new URLSearchParams(entries)
}
const csvEntries: Record<string, string> = {}
for (const [key, value] of entries) {
csvEntries[key] = csvEntries[key] ? `${csvEntries[key]},${value}` : value
}
return new URLSearchParams(csvEntries)
}

Wyświetl plik

@ -115,3 +115,61 @@ export function hashObject(
...options
})
}
export function getEnv(name: string): string | undefined {
try {
return typeof process !== 'undefined'
? // eslint-disable-next-line no-process-env
process.env?.[name]
: undefined
} catch {
return undefined
}
}
/**
* Creates a new `URLSearchParams` object with all values coerced to strings
* that correctly handles arrays of values as repeated keys (or CSV) and
* correctly removes `undefined` keys and values.
*/
export function sanitizeSearchParams(
searchParams:
| Record<
string,
string | number | boolean | string[] | number[] | boolean[] | undefined
>
| object,
{
csv = false
}: {
/**
* Whether to use comma-separated-values for arrays or multiple entries.
*
* Defaults to `false` and will use multiple entries.
*/
csv?: boolean
} = {}
): URLSearchParams {
const entries = Object.entries(searchParams).flatMap(([key, value]) => {
if (key === undefined || value === undefined) {
return []
}
if (Array.isArray(value)) {
return value.map((v) => [key, String(v)])
}
return [[key, String(value)]]
}) as [string, string][]
if (!csv) {
return new URLSearchParams(entries)
}
const csvEntries: Record<string, string> = {}
for (const [key, value] of entries) {
csvEntries[key] = csvEntries[key] ? `${csvEntries[key]},${value}` : value
}
return new URLSearchParams(csvEntries)
}

Wyświetl plik

@ -0,0 +1,50 @@
import { pgTable, text, timestamp } from '@fisch0920/drizzle-orm/pg-core'
import {
accountPrimaryId,
sessionPrimaryId,
timestamps,
userId,
verificationPrimaryId
} from './common'
import { users } from './user'
// These tables are all managed by better-auth.
export const sessions = pgTable('sessions', {
...sessionPrimaryId,
...timestamps,
expiresAt: timestamp('expiresAt').notNull(),
ipAddress: text('ipAddress'),
userAgent: text('userAgent'),
userId: userId()
.notNull()
.references(() => users.id)
})
export const accounts = pgTable('accounts', {
...accountPrimaryId,
...timestamps,
accountId: text('accountId').notNull(),
providerId: text('providerId').notNull(),
userId: userId()
.notNull()
.references(() => users.id),
accessToken: text('accessToken'),
refreshToken: text('refreshToken'),
idToken: text('idToken'),
expiresAt: timestamp('expiresAt').notNull(),
password: text('password')
})
export const verifications = pgTable('verifications', {
...verificationPrimaryId,
...timestamps,
identifier: text('identifier').notNull(),
value: text('value').notNull(),
expiresAt: timestamp('expiresAt').notNull()
})

Wyświetl plik

@ -16,19 +16,31 @@ import { createId as createCuid2 } from '@paralleldrive/cuid2'
const usernameAndTeamSlugLength = 64 as const
// prefix is max 4 characters
// prefix is max 5 characters
// separator is 1 character
// cuid2 is max 24 characters
// so use 32 characters to be safe for storing ids
export const idMaxLength = 32 as const
export const idPrefixMap = {
user: 'user',
team: 'team',
project: 'proj',
deployment: 'depl',
consumer: 'csmr',
logEntry: 'log'
logEntry: 'log',
// better-auth
user: 'user',
account: 'acct',
session: 'sess',
verification: 'veri',
'rate-limit': 'ratel',
organization: 'org',
member: 'mem',
invitation: 'inv',
jwks: 'jwks',
passkey: 'passk',
'two-factor': '2fa'
} as const
export type ModelType = keyof typeof idPrefixMap
@ -57,6 +69,9 @@ export const consumerPrimaryId = getPrimaryId('consumer')
export const logEntryPrimaryId = getPrimaryId('logEntry')
export const teamPrimaryId = getPrimaryId('team')
export const userPrimaryId = getPrimaryId('user')
export const sessionPrimaryId = getPrimaryId('session')
export const accountPrimaryId = getPrimaryId('account')
export const verificationPrimaryId = getPrimaryId('verification')
/**
* All of our model primary ids have the following format:

Wyświetl plik

@ -1,3 +1,4 @@
export * from './auth'
export * from './common'
export * from './consumer'
export * from './deployment'

Wyświetl plik

@ -1,61 +1,61 @@
import { z } from '@hono/zod-openapi'
import parseJson from 'parse-json'
export const authProviderTypeSchema = z
.union([
z.literal('github'),
z.literal('google'),
z.literal('spotify'),
z.literal('twitter'),
z.literal('linkedin'),
z.literal('stripe')
])
.openapi('AuthProviderType')
export type AuthProviderType = z.infer<typeof authProviderTypeSchema>
// export const authProviderTypeSchema = z
// .union([
// z.literal('github'),
// z.literal('google'),
// z.literal('spotify'),
// z.literal('twitter'),
// z.literal('linkedin'),
// z.literal('stripe')
// ])
// .openapi('AuthProviderType')
// export type AuthProviderType = z.infer<typeof authProviderTypeSchema>
export const authProviderSchema = z.object({
provider: authProviderTypeSchema,
// export const authProviderSchema = z.object({
// provider: authProviderTypeSchema,
/** Provider-specific user id */
id: z.string(),
// /** Provider-specific user id */
// id: z.string(),
/** Provider-specific username */
username: z.string().optional(),
// /** Provider-specific username */
// username: z.string().optional(),
/** Standard oauth2 access token */
accessToken: z.string().optional(),
// /** Standard oauth2 access token */
// accessToken: z.string().optional(),
/** Standard oauth2 refresh token */
refreshToken: z.string().optional(),
// /** Standard oauth2 refresh token */
// refreshToken: z.string().optional(),
/** Stripe public key */
publicKey: z.string().optional(),
// /** Stripe public key */
// publicKey: z.string().optional(),
/** OAuth scope(s) */
scope: z.string().optional()
})
export type AuthProvider = z.infer<typeof authProviderSchema>
// /** OAuth scope(s) */
// scope: z.string().optional()
// })
// export type AuthProvider = z.infer<typeof authProviderSchema>
export const publicAuthProviderSchema = authProviderSchema
.omit({
accessToken: true,
refreshToken: true,
publicKey: true
})
.strip()
.openapi('AuthProvider')
export type PublicAuthProvider = z.infer<typeof publicAuthProviderSchema>
// export const publicAuthProviderSchema = authProviderSchema
// .omit({
// accessToken: true,
// refreshToken: true,
// publicKey: true
// })
// .strip()
// .openapi('AuthProvider')
// export type PublicAuthProvider = z.infer<typeof publicAuthProviderSchema>
export const authProvidersSchema = z.record(
authProviderTypeSchema,
authProviderSchema.optional()
)
export type AuthProviders = z.infer<typeof authProvidersSchema>
// export const authProvidersSchema = z.record(
// authProviderTypeSchema,
// authProviderSchema.optional()
// )
// export type AuthProviders = z.infer<typeof authProvidersSchema>
export const publicAuthProvidersSchema = z
.record(authProviderTypeSchema, publicAuthProviderSchema.optional())
.openapi('AuthProviders')
export type PublicAuthProviders = z.infer<typeof publicAuthProvidersSchema>
// export const publicAuthProvidersSchema = z
// .record(authProviderTypeSchema, publicAuthProviderSchema.optional())
// .openapi('AuthProviders')
// export type PublicAuthProviders = z.infer<typeof publicAuthProvidersSchema>
export const webhookSchema = z
.object({

Wyświetl plik

@ -1,64 +1,46 @@
import { sha256 } from '@agentic/platform-core'
import { validators } from '@agentic/platform-validators'
import { relations } from '@fisch0920/drizzle-orm'
import {
boolean,
index,
jsonb,
pgTable,
text,
uniqueIndex
} from '@fisch0920/drizzle-orm/pg-core'
import { hashSync } from 'bcryptjs'
import {
createInsertSchema,
createSelectSchema,
createUpdateSchema,
stripeId,
timestamp,
timestamps,
username,
// username,
userPrimaryId,
userRoleEnum
} from './common'
import { type AuthProviders, publicAuthProvidersSchema } from './schemas'
import { teams } from './team'
// This table is mostly managed by better-auth.
export const users = pgTable(
'users',
{
...userPrimaryId,
...timestamps,
username: username().notNull().unique(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
emailVerified: boolean('emailVerified').default(false).notNull(),
image: text('image'),
// TODO: re-add username
// username: username().notNull().unique(),
role: userRoleEnum().default('user').notNull(),
email: text().unique(),
password: text(),
// metadata
firstName: text(),
lastName: text(),
image: text(),
emailConfirmed: boolean().default(false).notNull(),
emailConfirmedAt: timestamp(),
emailConfirmToken: text().unique().notNull(),
passwordResetToken: text().unique(),
isStripeConnectEnabledByDefault: boolean().default(true).notNull(),
// third-party auth providers
authProviders: jsonb().$type<AuthProviders>().default({}).notNull(),
stripeCustomerId: stripeId()
},
(table) => [
uniqueIndex('user_email_idx').on(table.email),
uniqueIndex('user_username_idx').on(table.username),
uniqueIndex('user_emailConfirmToken_idx').on(table.emailConfirmToken),
uniqueIndex('user_passwordResetToken_idx').on(table.passwordResetToken),
// uniqueIndex('user_username_idx').on(table.username),
index('user_createdAt_idx').on(table.createdAt),
index('user_updatedAt_idx').on(table.updatedAt),
index('user_deletedAt_idx').on(table.deletedAt)
@ -70,49 +52,49 @@ export const usersRelations = relations(users, ({ many }) => ({
}))
export const userSelectSchema = createSelectSchema(users, {
authProviders: publicAuthProvidersSchema
// authProviders: publicAuthProvidersSchema
})
.omit({ password: true, emailConfirmToken: true, passwordResetToken: true })
// .omit({ password: true, emailConfirmToken: true, passwordResetToken: true })
.strip()
.openapi('User')
export const userInsertSchema = createInsertSchema(users, {
username: (schema) =>
schema.refine((username) => validators.username(username), {
message: 'Invalid username'
}),
// export const userInsertSchema = createInsertSchema(users, {
// username: (schema) =>
// schema.refine((username) => validators.username(username), {
// message: 'Invalid username'
// }),
email: (schema) => schema.email().optional()
})
.pick({
username: true,
email: true,
password: true,
firstName: true,
lastName: true,
image: true
})
.strict()
.transform((user) => {
return {
...user,
emailConfirmToken: sha256(),
password: user.password ? hashSync(user.password) : undefined
}
})
// email: (schema) => schema.email().optional()
// })
// .pick({
// username: true,
// email: true,
// password: true,
// firstName: true,
// lastName: true,
// image: true
// })
// .strict()
// .transform((user) => {
// return {
// ...user,
// emailConfirmToken: sha256(),
// password: user.password ? hashSync(user.password) : undefined
// }
// })
export const userUpdateSchema = createUpdateSchema(users)
.pick({
firstName: true,
lastName: true,
image: true,
password: true,
isStripeConnectEnabledByDefault: true
})
.strict()
.transform((user) => {
return {
...user,
password: user.password ? hashSync(user.password) : undefined
}
})
// export const userUpdateSchema = createUpdateSchema(users)
// .pick({
// firstName: true,
// lastName: true,
// image: true,
// password: true,
// isStripeConnectEnabledByDefault: true
// })
// .strict()
// .transform((user) => {
// return {
// ...user,
// password: user.password ? hashSync(user.password) : undefined
// }
// })

Wyświetl plik

@ -77,3 +77,5 @@ export type RawConsumerUpdate = Partial<
export type LogEntry = z.infer<typeof schema.logEntrySelectSchema>
export type RawLogEntry = InferSelectModel<typeof schema.logEntries>
export type RawSession = InferSelectModel<typeof schema.sessions>

Plik diff jest za duży Load Diff