pull/715/head
Travis Fischer 2025-04-30 15:27:33 +07:00
rodzic 80f6655ada
commit ac2d0c41a7
12 zmienionych plików z 249 dodań i 37 usunięć

Wyświetl plik

@ -1,14 +1,13 @@
/* eslint-disable no-process-env */
import 'dotenv/config'
import { defineConfig } from 'drizzle-kit'
import { env } from './src/lib/env'
export default defineConfig({
out: './drizzle',
schema: './src/db/schema',
schema: './src/db/schema/index.ts',
dialect: 'postgresql',
dbCredentials: {
url: env.DATABASE_URL
url: process.env.DATABASE_URL!
}
})

Wyświetl plik

@ -62,6 +62,7 @@
},
"devDependencies": {
"@types/jsonwebtoken": "^9.0.9",
"drizzle-kit": "^0.31.0"
"drizzle-kit": "^0.31.0",
"drizzle-orm": "^0.43.1"
}
}

Wyświetl plik

@ -4,6 +4,7 @@ import type { AuthenticatedEnv } from '@/lib/types'
import * as middleware from '@/lib/middleware'
import { registerHealthCheck } from './health-check'
import { registerV1ProjectsGetProject } from './projects/get-project'
import { registerV1TeamsCreateTeam } from './teams/create-team'
import { registerV1TeamsDeleteTeam } from './teams/delete-team'
import { registerV1TeamsGetTeam } from './teams/get-team'
@ -41,6 +42,9 @@ registerV1TeamsMembersCreateTeamMember(pri)
registerV1TeamsMembersUpdateTeamMember(pri)
registerV1TeamsMembersDeleteTeamMember(pri)
// Projects crud
registerV1ProjectsGetProject(pri)
// Setup routes and middleware
apiV1.route('/', pub)
apiV1.use(middleware.authenticate)

Wyświetl plik

@ -0,0 +1,56 @@
import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
import type { AuthenticatedEnv } from '@/lib/types'
import {
db,
eq,
// populateProjectSchema,
schema
} from '@/db'
import { acl } from '@/lib/acl'
import { assert, parseZodSchema } from '@/lib/utils'
import { ProjectIdParamsSchema } from './schemas'
const route = createRoute({
description: 'Gets a project',
tags: ['projects'],
operationId: 'getProject',
method: 'get',
path: 'projects/{projectId}',
security: [{ bearerAuth: [] }],
request: {
params: ProjectIdParamsSchema
// query: populateProjectSchema
},
responses: {
200: {
description: 'A project',
content: {
'application/json': {
schema: schema.projectSelectSchema
}
}
}
// TODO
// ...openApiErrorResponses
}
})
export function registerV1ProjectsGetProject(
app: OpenAPIHono<AuthenticatedEnv>
) {
return app.openapi(route, async (c) => {
const { projectId } = c.req.valid('param')
// const { populate = [] } = c.req.valid('query')
const project = await db.query.projects.findFirst({
where: eq(schema.projects.id, projectId)
// with: Object.fromEntries(populate.map((field) => [field, true]))
})
assert(project, 404, `Project not found "${projectId}"`)
await acl(c, project, { label: 'Project' })
return c.json(parseZodSchema(schema.projectSelectSchema, project))
})
}

Wyświetl plik

@ -0,0 +1,13 @@
import { z } from '@hono/zod-openapi'
import { projectIdSchema } from '@/db'
export const ProjectIdParamsSchema = z.object({
projectId: projectIdSchema.openapi({
param: {
description: 'Project ID',
name: 'projectId',
in: 'path'
}
})
})

Wyświetl plik

@ -6,3 +6,4 @@ export * from './team'
export * from './team-member'
export type * from './types'
export * from './user'
export * from './utils'

Wyświetl plik

@ -103,13 +103,12 @@ export const projects = pgTable(
(table) => [
index('project_userId_idx').on(table.userId),
index('project_teamId_idx').on(table.teamId),
index('project_teamId_idx').on(table.teamId),
index('project_createdAt_idx').on(table.createdAt),
index('project_updatedAt_idx').on(table.updatedAt)
]
)
export const projectsRelations = relations(projects, ({ one, many }) => ({
export const projectsRelations = relations(projects, ({ one }) => ({
user: one(users, {
fields: [projects.userId],
references: [users.id]
@ -127,13 +126,28 @@ export const projectsRelations = relations(projects, ({ one, many }) => ({
fields: [projects.lastDeploymentId],
references: [deployments.id],
relationName: 'lastDeployment'
}),
deployments: many(deployments, { relationName: 'deployments' }),
publishedDeployments: many(deployments, {
relationName: 'publishedDeployments'
})
// deployments: many(deployments, {
// relationName: 'deployments'
// }),
// publishedDeployments: many(deployments, {
// relationName: 'publishedDeployments'
// })
}))
export type ProjectRelationFields = keyof ReturnType<
(typeof projectsRelations)['config']
>
export const projectRelationsSchema: z.ZodType<ProjectRelationFields> = z.enum([
'user',
'team',
'lastPublishedDeployment',
'lastDeployment'
// 'deployments',
// 'publishedDeployments'
])
export const projectSelectSchema = createSelectSchema(projects, {
stripeMetricProductIds: z.record(z.string(), z.string()).optional()
// _webhooks: z.array(webhookSchema),

Wyświetl plik

@ -45,7 +45,7 @@ export const users = pgTable(
emailConfirmed: boolean().default(false).notNull(),
emailConfirmedAt: timestamp(),
emailConfirmToken: text().unique().default(sha256()).notNull(),
emailConfirmToken: text().unique().notNull(),
passwordResetToken: text().unique(),
isStripeConnectEnabledByDefault: boolean().default(true).notNull(),
@ -71,13 +71,6 @@ export const usersRelations = relations(users, ({ many }) => ({
export const userSelectSchema = createSelectSchema(users).openapi('User')
function userRefinementHook(user: Partial<typeof users.$inferInsert>) {
return {
...user,
password: user.password ? hashSync(user.password) : undefined
}
}
export const userInsertSchema = createInsertSchema(users, {
username: (schema) =>
schema.refine((username) => validators.username(username), {
@ -96,7 +89,13 @@ export const userInsertSchema = createInsertSchema(users, {
lastName: true,
image: true
})
.refine(userRefinementHook)
.refine((user) => {
return {
...user,
emailConfirmToken: sha256(),
password: user.password ? hashSync(user.password) : undefined
}
})
export const userUpdateSchema = createUpdateSchema(users)
.pick({
@ -106,4 +105,9 @@ export const userUpdateSchema = createUpdateSchema(users)
password: true,
isStripeConnectEnabledByDefault: true
})
.refine(userRefinementHook)
.refine((user) => {
return {
...user,
password: user.password ? hashSync(user.password) : undefined
}
})

Wyświetl plik

@ -1,6 +1,8 @@
import { validators } from '@agentic/validators'
import { z } from '@hono/zod-openapi'
import { projectRelationsSchema } from './schema/project'
function getCuidSchema(idLabel: string) {
return z.string().refine((id) => validators.cuid(id), {
message: `Invalid ${idLabel}`
@ -41,6 +43,10 @@ export const paginationSchema = z.object({
sortBy: z.enum(['createdAt', 'updatedAt']).default('createdAt').optional()
})
export const populateProjectSchema = z.object({
populate: z.array(projectRelationsSchema).default([]).optional()
})
// import type { PgTable, TableConfig } from '@fisch0920/drizzle-orm/pg-core'
// import type { AnyZodObject } from 'zod'
//

Wyświetl plik

@ -3,8 +3,8 @@ import { assert } from './utils'
export async function acl<
TModel extends Record<string, unknown>,
TUserField extends keyof TModel = 'user',
TTeamField extends keyof TModel = 'team'
TUserField extends keyof TModel = 'userId',
TTeamField extends keyof TModel = 'teamId'
>(
ctx: AuthenticatedContext,
model: TModel,

Wyświetl plik

@ -1,5 +1,5 @@
import { createMiddleware } from 'hono/factory'
import { jwt } from 'hono/jwt'
import * as jwt from 'hono/jwt'
import type { AuthenticatedEnv } from '@/lib/types'
import { db, eq, schema } from '@/db'
@ -7,24 +7,40 @@ import { env } from '@/lib/env'
import { assert } from '../utils'
const jwtMiddleware = jwt({
secret: env.JWT_SECRET
})
const token = await jwt.sign({ userId: 'test', type: 'user' }, env.JWT_SECRET)
console.log({ token })
export const authenticate = createMiddleware<AuthenticatedEnv>(
async function authenticateMiddleware(ctx, next) {
await jwtMiddleware(ctx, async () => {
const payload = ctx.get('jwtPayload')
assert(payload, 401, 'Unauthorized')
assert(payload.type === 'user', 401, 'Unauthorized')
const credentials = ctx.req.raw.headers.get('Authorization')
assert(credentials, 401, 'Unauthorized')
const user = await db.query.users.findFirst({
where: eq(schema.users.id, payload.userId)
})
assert(user, 401, 'Unauthorized')
ctx.set('user', user)
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')
await next()
const payload = await jwt.verify(token, env.JWT_SECRET)
console.log({ payload })
assert(payload, 401, 'Unauthorized')
assert(payload.type === 'user', 401, 'Unauthorized')
assert(
payload.userId && typeof payload.userId === 'string',
401,
'Unauthorized'
)
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

@ -201,6 +201,9 @@ importers:
drizzle-kit:
specifier: ^0.31.0
version: 0.31.0
drizzle-orm:
specifier: ^0.43.1
version: 0.43.1(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(postgres@3.4.5)
packages/validators:
dependencies:
@ -1730,6 +1733,95 @@ packages:
resolution: {integrity: sha512-pcKVT+GbfPA+bUovPIilgVOoq+onNBo/YQBG86sf3/GFHkN6lRJPm1l7dKN0IMAk57RQoIm4GUllRrasLlcaSg==}
hasBin: true
drizzle-orm@0.43.1:
resolution: {integrity: sha512-dUcDaZtE/zN4RV/xqGrVSMpnEczxd5cIaoDeor7Zst9wOe/HzC/7eAaulywWGYXdDEc9oBPMjayVEDg0ziTLJA==}
peerDependencies:
'@aws-sdk/client-rds-data': '>=3'
'@cloudflare/workers-types': '>=4'
'@electric-sql/pglite': '>=0.2.0'
'@libsql/client': '>=0.10.0'
'@libsql/client-wasm': '>=0.10.0'
'@neondatabase/serverless': '>=0.10.0'
'@op-engineering/op-sqlite': '>=2'
'@opentelemetry/api': ^1.4.1
'@planetscale/database': '>=1.13'
'@prisma/client': '*'
'@tidbcloud/serverless': '*'
'@types/better-sqlite3': '*'
'@types/pg': '*'
'@types/sql.js': '*'
'@vercel/postgres': '>=0.8.0'
'@xata.io/client': '*'
better-sqlite3: '>=7'
bun-types: '*'
expo-sqlite: '>=14.0.0'
gel: '>=2'
knex: '*'
kysely: '*'
mysql2: '>=2'
pg: '>=8'
postgres: '>=3'
prisma: '*'
sql.js: '>=1'
sqlite3: '>=5'
peerDependenciesMeta:
'@aws-sdk/client-rds-data':
optional: true
'@cloudflare/workers-types':
optional: true
'@electric-sql/pglite':
optional: true
'@libsql/client':
optional: true
'@libsql/client-wasm':
optional: true
'@neondatabase/serverless':
optional: true
'@op-engineering/op-sqlite':
optional: true
'@opentelemetry/api':
optional: true
'@planetscale/database':
optional: true
'@prisma/client':
optional: true
'@tidbcloud/serverless':
optional: true
'@types/better-sqlite3':
optional: true
'@types/pg':
optional: true
'@types/sql.js':
optional: true
'@vercel/postgres':
optional: true
'@xata.io/client':
optional: true
better-sqlite3:
optional: true
bun-types:
optional: true
expo-sqlite:
optional: true
gel:
optional: true
knex:
optional: true
kysely:
optional: true
mysql2:
optional: true
pg:
optional: true
postgres:
optional: true
prisma:
optional: true
sql.js:
optional: true
sqlite3:
optional: true
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
@ -5124,6 +5216,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
drizzle-orm@0.43.1(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(postgres@3.4.5):
optionalDependencies:
'@opentelemetry/api': 1.9.0
'@types/pg': 8.6.1
postgres: 3.4.5
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2