feat: add production auth and diagnosis schema

This commit is contained in:
Asep Haryana Saputra
2026-05-22 15:53:48 +00:00
parent b347ed4fd1
commit 794abfdb35
4 changed files with 117 additions and 2 deletions
+2
View File
@@ -13,6 +13,7 @@
"dependencies": {
"@tensorflow/tfjs": "4.22.0",
"@zeavis/shared": "workspace:*",
"bcryptjs": "^2.4.3",
"drizzle-orm": "^0.36.4",
"elysia": "^1.1.25",
"jpeg-js": "0.4.4",
@@ -20,6 +21,7 @@
"postgres": "^3.4.5"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/bun": "latest",
"drizzle-kit": "^0.27.1",
"typescript": "^5.6.3"
+28
View File
@@ -1,4 +1,32 @@
const uploadAllowedMimeTypes = (Bun.env.UPLOAD_ALLOWED_MIME_TYPES ?? 'image/jpeg,image/png')
.split(',')
.map((value) => value.trim())
.filter(Boolean);
const googleOAuthEnabled = Boolean(
Bun.env.GOOGLE_CLIENT_ID && Bun.env.GOOGLE_CLIENT_SECRET && Bun.env.GOOGLE_REDIRECT_URI,
);
export const env = {
port: Number(Bun.env.API_PORT ?? 3000),
databaseUrl: Bun.env.DATABASE_URL,
sessionSecret: Bun.env.SESSION_SECRET,
uploaderBaseUrl: Bun.env.UPLOADER_BASE_URL ?? 'https://upload.asepharyana.tech',
uploadMaxBytes: Number(Bun.env.UPLOAD_MAX_BYTES ?? 5 * 1024 * 1024),
uploadAllowedMimeTypes,
googleOAuthEnabled,
googleClientId: Bun.env.GOOGLE_CLIENT_ID,
googleClientSecret: Bun.env.GOOGLE_CLIENT_SECRET,
googleRedirectUri: Bun.env.GOOGLE_REDIRECT_URI,
webAppUrl: Bun.env.WEB_APP_URL ?? 'http://localhost:5173',
};
export function assertRequiredEnv() {
if (!env.databaseUrl) {
throw new Error('DATABASE_URL is required');
}
if (!env.sessionSecret || env.sessionSecret.length < 32) {
throw new Error('SESSION_SECRET is required and must be at least 32 characters');
}
}
+80 -1
View File
@@ -1,4 +1,15 @@
import { pgTable, timestamp, uuid, varchar, text, integer, jsonb, real } from 'drizzle-orm/pg-core';
import {
index,
integer,
jsonb,
pgTable,
real,
text,
timestamp,
uniqueIndex,
uuid,
varchar,
} from 'drizzle-orm/pg-core';
export const appEvents = pgTable('app_events', {
id: uuid('id').primaryKey().defaultRandom(),
@@ -21,6 +32,31 @@ export const diseaseCatalog = pgTable('disease_catalog', {
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: varchar('email', { length: 240 }).notNull(),
name: varchar('name', { length: 160 }).notNull(),
passwordHash: text('password_hash'),
role: varchar('role', { length: 20 }).notNull().default('user'),
googleId: varchar('google_id', { length: 240 }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => ({
emailIdx: uniqueIndex('users_email_idx').on(table.email),
googleIdx: uniqueIndex('users_google_id_idx').on(table.googleId),
}));
export const sessions = pgTable('sessions', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull().references(() => users.id),
tokenHash: text('token_hash').notNull(),
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => ({
tokenIdx: uniqueIndex('sessions_token_hash_idx').on(table.tokenHash),
userIdx: index('sessions_user_id_idx').on(table.userId),
}));
export const manualClassifications = pgTable('manual_classifications', {
id: uuid('id').primaryKey().defaultRandom(),
diseaseSlug: varchar('disease_slug', { length: 80 })
@@ -44,3 +80,46 @@ export const imageClassifications = pgTable('image_classifications', {
uploaderPayload: jsonb('uploader_payload').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
});
export const diagnoses = pgTable('diagnoses', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull().references(() => users.id),
predictedDiseaseSlug: varchar('predicted_disease_slug', { length: 80 }).references(() => diseaseCatalog.slug),
confidence: real('confidence'),
status: varchar('status', { length: 40 }).notNull(),
failureReason: text('failure_reason'),
imageUrl: text('image_url').notNull(),
uploaderPublicId: varchar('uploader_public_id', { length: 160 }).notNull(),
imageFileName: varchar('image_file_name', { length: 240 }).notNull(),
imageMimeType: varchar('image_mime_type', { length: 120 }).notNull(),
imageSizeBytes: integer('image_size_bytes').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => ({
userIdx: index('diagnoses_user_id_idx').on(table.userId),
statusIdx: index('diagnoses_status_idx').on(table.status),
}));
export const diagnosisPredictions = pgTable('diagnosis_predictions', {
id: uuid('id').primaryKey().defaultRandom(),
diagnosisId: uuid('diagnosis_id').notNull().references(() => diagnoses.id),
diseaseSlug: varchar('disease_slug', { length: 80 }).references(() => diseaseCatalog.slug),
modelLabel: varchar('model_label', { length: 120 }).notNull(),
confidence: real('confidence').notNull(),
rank: integer('rank').notNull(),
}, (table) => ({
diagnosisIdx: index('diagnosis_predictions_diagnosis_id_idx').on(table.diagnosisId),
}));
export const expertReviews = pgTable('expert_reviews', {
id: uuid('id').primaryKey().defaultRandom(),
diagnosisId: uuid('diagnosis_id').notNull().references(() => diagnoses.id),
expertId: uuid('expert_id').notNull().references(() => users.id),
verdict: varchar('verdict', { length: 20 }).notNull(),
correctedDiseaseSlug: varchar('corrected_disease_slug', { length: 80 }).references(() => diseaseCatalog.slug),
notes: text('notes').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
}, (table) => ({
diagnosisIdx: index('expert_reviews_diagnosis_id_idx').on(table.diagnosisId),
expertIdx: index('expert_reviews_expert_id_idx').on(table.expertId),
}));