feat: add production auth and diagnosis schema
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tensorflow/tfjs": "4.22.0",
|
"@tensorflow/tfjs": "4.22.0",
|
||||||
"@zeavis/shared": "workspace:*",
|
"@zeavis/shared": "workspace:*",
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
"drizzle-orm": "^0.36.4",
|
"drizzle-orm": "^0.36.4",
|
||||||
"elysia": "^1.1.25",
|
"elysia": "^1.1.25",
|
||||||
"jpeg-js": "0.4.4",
|
"jpeg-js": "0.4.4",
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
"postgres": "^3.4.5"
|
"postgres": "^3.4.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"drizzle-kit": "^0.27.1",
|
"drizzle-kit": "^0.27.1",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
|
|||||||
@@ -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 = {
|
export const env = {
|
||||||
port: Number(Bun.env.API_PORT ?? 3000),
|
port: Number(Bun.env.API_PORT ?? 3000),
|
||||||
databaseUrl: Bun.env.DATABASE_URL,
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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', {
|
export const appEvents = pgTable('app_events', {
|
||||||
id: uuid('id').primaryKey().defaultRandom(),
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
@@ -21,6 +32,31 @@ export const diseaseCatalog = pgTable('disease_catalog', {
|
|||||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
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', {
|
export const manualClassifications = pgTable('manual_classifications', {
|
||||||
id: uuid('id').primaryKey().defaultRandom(),
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
diseaseSlug: varchar('disease_slug', { length: 80 })
|
diseaseSlug: varchar('disease_slug', { length: 80 })
|
||||||
@@ -44,3 +80,46 @@ export const imageClassifications = pgTable('image_classifications', {
|
|||||||
uploaderPayload: jsonb('uploader_payload').notNull(),
|
uploaderPayload: jsonb('uploader_payload').notNull(),
|
||||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
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),
|
||||||
|
}));
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tensorflow/tfjs": "4.22.0",
|
"@tensorflow/tfjs": "4.22.0",
|
||||||
"@zeavis/shared": "packages/shared",
|
"@zeavis/shared": "packages/shared",
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
"drizzle-orm": "^0.36.4",
|
"drizzle-orm": "^0.36.4",
|
||||||
"elysia": "^1.1.25",
|
"elysia": "^1.1.25",
|
||||||
"jpeg-js": "0.4.4",
|
"jpeg-js": "0.4.4",
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
"postgres": "^3.4.5",
|
"postgres": "^3.4.5",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"drizzle-kit": "^0.27.1",
|
"drizzle-kit": "^0.27.1",
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.3",
|
||||||
@@ -283,6 +285,8 @@
|
|||||||
|
|
||||||
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
||||||
|
|
||||||
|
"@types/bcryptjs": ["@types/bcryptjs@2.4.6", "", {}, "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
@@ -307,7 +311,7 @@
|
|||||||
|
|
||||||
"@webgpu/types": ["@webgpu/types@0.1.38", "", {}, "sha512-7LrhVKz2PRh+DD7+S+PVaFd5HxaWQvoMqBbsV9fNJO1pjUs1P8bM2vQVNfk+3URTqbuTI7gkXi0rfsN0IadoBA=="],
|
"@webgpu/types": ["@webgpu/types@0.1.38", "", {}, "sha512-7LrhVKz2PRh+DD7+S+PVaFd5HxaWQvoMqBbsV9fNJO1pjUs1P8bM2vQVNfk+3URTqbuTI7gkXi0rfsN0IadoBA=="],
|
||||||
|
|
||||||
"@zeavis/api": ["@zeavis/api@workspace:apps/api", { "dependencies": { "@tensorflow/tfjs": "4.22.0", "@zeavis/shared": "packages/shared", "drizzle-orm": "^0.36.4", "elysia": "^1.1.25", "jpeg-js": "0.4.4", "pngjs": "7.0.0", "postgres": "^3.4.5" }, "devDependencies": { "@types/bun": "latest", "drizzle-kit": "^0.27.1", "typescript": "^5.6.3" } }],
|
"@zeavis/api": ["@zeavis/api@workspace:apps/api", { "dependencies": { "@tensorflow/tfjs": "4.22.0", "@zeavis/shared": "packages/shared", "bcryptjs": "^2.4.3", "drizzle-orm": "^0.36.4", "elysia": "^1.1.25", "jpeg-js": "0.4.4", "pngjs": "7.0.0", "postgres": "^3.4.5" }, "devDependencies": { "@types/bcryptjs": "^2.4.6", "@types/bun": "latest", "drizzle-kit": "^0.27.1", "typescript": "^5.6.3" } }],
|
||||||
|
|
||||||
"@zeavis/shared": ["@zeavis/shared@workspace:packages/shared", { "devDependencies": { "typescript": "^5.6.3" } }],
|
"@zeavis/shared": ["@zeavis/shared@workspace:packages/shared", { "devDependencies": { "typescript": "^5.6.3" } }],
|
||||||
|
|
||||||
@@ -331,6 +335,8 @@
|
|||||||
|
|
||||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.31", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q=="],
|
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.31", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q=="],
|
||||||
|
|
||||||
|
"bcryptjs": ["bcryptjs@2.4.3", "", {}, "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="],
|
||||||
|
|
||||||
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
||||||
|
|
||||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||||
|
|||||||
Reference in New Issue
Block a user