Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cf18318410 | |||
| e2ed25059a |
+7
-1
@@ -13,6 +13,12 @@ PORT=3000
|
|||||||
CORS_ORIGINS=http://localhost:4174
|
CORS_ORIGINS=http://localhost:4174
|
||||||
LOG_LEVEL=warn
|
LOG_LEVEL=warn
|
||||||
|
|
||||||
|
# Public base URL used for notification action links.
|
||||||
|
# Required for intake reminder action buttons/links.
|
||||||
|
# PUBLIC_APP_URL=https://medassist.example.com
|
||||||
|
# For mobile testing on the same LAN, use your laptop IP instead of localhost,
|
||||||
|
# e.g. PUBLIC_APP_URL=http://192.168.0.113:5173 and add that origin to CORS_ORIGINS.
|
||||||
|
|
||||||
# Levels: debug, info, warn, error, silent
|
# Levels: debug, info, warn, error, silent
|
||||||
# Controls: backend Fastify logging, frontend nginx access logs (Docker),
|
# Controls: backend Fastify logging, frontend nginx access logs (Docker),
|
||||||
# and frontend browser console (via build-time injection)
|
# and frontend browser console (via build-time injection)
|
||||||
@@ -149,6 +155,6 @@ EXPIRY_WARNING_DAYS=30 # Days before expiry to show yellow warning
|
|||||||
# UI defaults
|
# UI defaults
|
||||||
# DEFAULT_LANGUAGE=en # en or de
|
# DEFAULT_LANGUAGE=en # en or de
|
||||||
# DEFAULT_STOCK_CALCULATION_MODE=automatic # automatic or manual
|
# DEFAULT_STOCK_CALCULATION_MODE=automatic # automatic or manual
|
||||||
# DEFAULT_SHARE_STOCK_STATUS=true # Show stock status on shared schedule links
|
# DEFAULT_SHARE_MEDICATION_OVERVIEW=false # Show medication overview section on shared schedule links
|
||||||
# DEFAULT_UPCOMING_TODAY_ONLY=false
|
# DEFAULT_UPCOMING_TODAY_ONLY=false
|
||||||
# DEFAULT_SHARE_SCHEDULE_TODAY_ONLY=false
|
# DEFAULT_SHARE_SCHEDULE_TODAY_ONLY=false
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
CREATE TABLE `notification_action_groups` (
|
||||||
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`user_id` integer NOT NULL,
|
||||||
|
`group_key` text(255) NOT NULL,
|
||||||
|
`sequence_id` text(255) NOT NULL,
|
||||||
|
`dose_ids_json` text NOT NULL,
|
||||||
|
`title` text(255) NOT NULL,
|
||||||
|
`message` text NOT NULL,
|
||||||
|
`language` text(10) DEFAULT 'en' NOT NULL,
|
||||||
|
`scheduled_for` integer,
|
||||||
|
`expires_at` integer NOT NULL,
|
||||||
|
`resolved_action` text(20),
|
||||||
|
`resolved_at` integer,
|
||||||
|
`created_at` integer DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
`updated_at` integer DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `notification_action_groups_group_key_unique` ON `notification_action_groups` (`group_key`);--> statement-breakpoint
|
||||||
|
CREATE TABLE `notification_action_tokens` (
|
||||||
|
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`group_id` integer NOT NULL,
|
||||||
|
`token_hash` text(128) NOT NULL,
|
||||||
|
`kind` text(20) NOT NULL,
|
||||||
|
`used_at` integer,
|
||||||
|
`created_at` integer DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
FOREIGN KEY (`group_id`) REFERENCES `notification_action_groups`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `notification_action_tokens_token_hash_unique` ON `notification_action_tokens` (`token_hash`);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `notification_action_groups` ADD `ntfy_original_message_id` text(255) DEFAULT '' NOT NULL;
|
||||||
@@ -59,6 +59,7 @@ export async function runAlterMigrations(client: Client): Promise<{ success: boo
|
|||||||
`ALTER TABLE user_settings ADD COLUMN last_stock_reminder_sent text`,
|
`ALTER TABLE user_settings ADD COLUMN last_stock_reminder_sent text`,
|
||||||
`ALTER TABLE user_settings ADD COLUMN last_stock_reminder_channel text`,
|
`ALTER TABLE user_settings ADD COLUMN last_stock_reminder_channel text`,
|
||||||
`ALTER TABLE user_settings ADD COLUMN last_stock_reminder_med_names text`,
|
`ALTER TABLE user_settings ADD COLUMN last_stock_reminder_med_names text`,
|
||||||
|
// Keep the removed legacy setting column for backward compatibility with older SQLite files.
|
||||||
`ALTER TABLE user_settings ADD COLUMN share_stock_status integer NOT NULL DEFAULT 1`,
|
`ALTER TABLE user_settings ADD COLUMN share_stock_status integer NOT NULL DEFAULT 1`,
|
||||||
`ALTER TABLE user_settings ADD COLUMN share_medication_overview integer NOT NULL DEFAULT 0`,
|
`ALTER TABLE user_settings ADD COLUMN share_medication_overview integer NOT NULL DEFAULT 0`,
|
||||||
`ALTER TABLE user_settings ADD COLUMN upcoming_today_only integer NOT NULL DEFAULT 0`,
|
`ALTER TABLE user_settings ADD COLUMN upcoming_today_only integer NOT NULL DEFAULT 0`,
|
||||||
@@ -75,6 +76,7 @@ export async function runAlterMigrations(client: Client): Promise<{ success: boo
|
|||||||
`ALTER TABLE user_settings ADD COLUMN last_prescription_reminder_channel text`,
|
`ALTER TABLE user_settings ADD COLUMN last_prescription_reminder_channel text`,
|
||||||
`ALTER TABLE user_settings ADD COLUMN last_prescription_reminder_med_names text`,
|
`ALTER TABLE user_settings ADD COLUMN last_prescription_reminder_med_names text`,
|
||||||
`ALTER TABLE refill_history ADD COLUMN used_prescription integer NOT NULL DEFAULT 0`,
|
`ALTER TABLE refill_history ADD COLUMN used_prescription integer NOT NULL DEFAULT 0`,
|
||||||
|
`ALTER TABLE notification_action_groups ADD COLUMN ntfy_original_message_id text NOT NULL DEFAULT ''`,
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const sql of alterMigrations) {
|
for (const sql of alterMigrations) {
|
||||||
@@ -96,6 +98,31 @@ export async function runAlterMigrations(client: Client): Promise<{ success: boo
|
|||||||
loose_pills_added INTEGER NOT NULL DEFAULT 0,
|
loose_pills_added INTEGER NOT NULL DEFAULT 0,
|
||||||
refill_date INTEGER NOT NULL DEFAULT (strftime('%s','now'))
|
refill_date INTEGER NOT NULL DEFAULT (strftime('%s','now'))
|
||||||
)`,
|
)`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS notification_action_groups (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
group_key TEXT NOT NULL UNIQUE,
|
||||||
|
sequence_id TEXT NOT NULL,
|
||||||
|
ntfy_original_message_id TEXT NOT NULL DEFAULT '',
|
||||||
|
dose_ids_json TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
language TEXT NOT NULL DEFAULT 'en',
|
||||||
|
scheduled_for INTEGER,
|
||||||
|
expires_at INTEGER NOT NULL,
|
||||||
|
resolved_action TEXT,
|
||||||
|
resolved_at INTEGER,
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||||
|
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS notification_action_tokens (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
group_id INTEGER NOT NULL REFERENCES notification_action_groups(id) ON DELETE CASCADE,
|
||||||
|
token_hash TEXT NOT NULL UNIQUE,
|
||||||
|
kind TEXT NOT NULL,
|
||||||
|
used_at INTEGER,
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
|
||||||
|
)`,
|
||||||
`CREATE TABLE IF NOT EXISTS api_keys (
|
`CREATE TABLE IF NOT EXISTS api_keys (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
@@ -124,6 +151,8 @@ export async function runAlterMigrations(client: Client): Promise<{ success: boo
|
|||||||
const createIndexMigrations = [
|
const createIndexMigrations = [
|
||||||
`CREATE UNIQUE INDEX IF NOT EXISTS users_username_lower_unique ON users(lower(username))`,
|
`CREATE UNIQUE INDEX IF NOT EXISTS users_username_lower_unique ON users(lower(username))`,
|
||||||
`CREATE UNIQUE INDEX IF NOT EXISTS api_keys_key_hash_unique ON api_keys(key_hash)`,
|
`CREATE UNIQUE INDEX IF NOT EXISTS api_keys_key_hash_unique ON api_keys(key_hash)`,
|
||||||
|
`CREATE UNIQUE INDEX IF NOT EXISTS notification_action_groups_group_key_unique ON notification_action_groups(group_key)`,
|
||||||
|
`CREATE UNIQUE INDEX IF NOT EXISTS notification_action_tokens_token_hash_unique ON notification_action_tokens(token_hash)`,
|
||||||
`CREATE INDEX IF NOT EXISTS api_keys_user_id_idx ON api_keys(user_id)`,
|
`CREATE INDEX IF NOT EXISTS api_keys_user_id_idx ON api_keys(user_id)`,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -108,8 +108,9 @@ export const userSettings = sqliteTable("user_settings", {
|
|||||||
timezone: text("timezone", { length: 64 }).notNull().default(""),
|
timezone: text("timezone", { length: 64 }).notNull().default(""),
|
||||||
// Stock calculation mode: "automatic" (schedule-based) or "manual" (only marked doses)
|
// Stock calculation mode: "automatic" (schedule-based) or "manual" (only marked doses)
|
||||||
stockCalculationMode: text("stock_calculation_mode", { length: 20 }).notNull().default("automatic"),
|
stockCalculationMode: text("stock_calculation_mode", { length: 20 }).notNull().default("automatic"),
|
||||||
// Whether shared schedule links show stock status (Critical/Low/Normal) to intake users
|
// Legacy column kept only so existing SQLite files continue to open cleanly after upgrades.
|
||||||
shareStockStatus: integer("share_stock_status", { mode: "boolean" }).notNull().default(true),
|
// Current MedAssist versions no longer read or expose this setting in product flows.
|
||||||
|
legacyShareStockStatusCompat: integer("share_stock_status", { mode: "boolean" }).notNull().default(true),
|
||||||
// Whether shared schedule links also embed the medication overview section
|
// Whether shared schedule links also embed the medication overview section
|
||||||
shareMedicationOverview: integer("share_medication_overview", { mode: "boolean" }).notNull().default(false),
|
shareMedicationOverview: integer("share_medication_overview", { mode: "boolean" }).notNull().default(false),
|
||||||
// UI timeline visibility preferences
|
// UI timeline visibility preferences
|
||||||
@@ -183,6 +184,43 @@ export const shareTokens = sqliteTable("share_tokens", {
|
|||||||
expiresAt: integer("expires_at", { mode: "timestamp" }), // NULL = never expires
|
expiresAt: integer("expires_at", { mode: "timestamp" }), // NULL = never expires
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Notification Action Groups - Shared action state for reminder notifications
|
||||||
|
// =============================================================================
|
||||||
|
export const notificationActionGroups = sqliteTable("notification_action_groups", {
|
||||||
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
|
userId: integer("user_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
|
groupKey: text("group_key", { length: 255 }).notNull().unique(),
|
||||||
|
sequenceId: text("sequence_id", { length: 255 }).notNull(),
|
||||||
|
ntfyOriginalMessageId: text("ntfy_original_message_id", { length: 255 }).notNull().default(""),
|
||||||
|
doseIdsJson: text("dose_ids_json").notNull(),
|
||||||
|
title: text("title", { length: 255 }).notNull(),
|
||||||
|
message: text("message").notNull(),
|
||||||
|
language: text("language", { length: 10 }).notNull().default("en"),
|
||||||
|
scheduledFor: integer("scheduled_for", { mode: "timestamp" }),
|
||||||
|
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
|
||||||
|
resolvedAction: text("resolved_action", { length: 20 }),
|
||||||
|
resolvedAt: integer("resolved_at", { mode: "timestamp" }),
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp" }).notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||||
|
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Notification Action Tokens - Hashed tokens for public reminder responses
|
||||||
|
// =============================================================================
|
||||||
|
export const notificationActionTokens = sqliteTable("notification_action_tokens", {
|
||||||
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
|
groupId: integer("group_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => notificationActionGroups.id, { onDelete: "cascade" }),
|
||||||
|
tokenHash: text("token_hash", { length: 128 }).notNull().unique(),
|
||||||
|
kind: text("kind", { length: 20 }).notNull(),
|
||||||
|
usedAt: integer("used_at", { mode: "timestamp" }),
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp" }).notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||||
|
});
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Dose Tracking - Tracks when doses are marked as taken
|
// Dose Tracking - Tracks when doses are marked as taken
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -194,8 +232,8 @@ export const doseTracking = sqliteTable("dose_tracking", {
|
|||||||
doseId: text("dose_id", { length: 255 }).notNull(), // e.g. "med-5-1-86400000-1735200000000"
|
doseId: text("dose_id", { length: 255 }).notNull(), // e.g. "med-5-1-86400000-1735200000000"
|
||||||
takenAt: integer("taken_at", { mode: "timestamp" }).notNull().default(sql`(strftime('%s','now'))`),
|
takenAt: integer("taken_at", { mode: "timestamp" }).notNull().default(sql`(strftime('%s','now'))`),
|
||||||
markedBy: text("marked_by", { length: 100 }), // null = user, "Daniel" = via share link
|
markedBy: text("marked_by", { length: 100 }), // null = user, "Daniel" = via share link
|
||||||
takenSource: text("taken_source", { length: 20 }).notNull().default("manual"), // manual or automatic
|
takenSource: text("taken_source", { length: 20 }).notNull().default("manual"), // manual, automatic, or notification
|
||||||
dismissed: integer("dismissed", { mode: "boolean" }).notNull().default(false), // true = missed dose acknowledged without taking
|
dismissed: integer("dismissed", { mode: "boolean" }).notNull().default(false), // legacy column: true = intake skipped without stock deduction
|
||||||
});
|
});
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|||||||
+17
-16
@@ -10,10 +10,11 @@ const EnvSchema = z.object({
|
|||||||
NODE_ENV: z.enum(["development", "production", "test"]).default("production"),
|
NODE_ENV: z.enum(["development", "production", "test"]).default("production"),
|
||||||
PORT: z
|
PORT: z
|
||||||
.string()
|
.string()
|
||||||
.transform((v) => parseInt(v, 10))
|
.default("3000")
|
||||||
.default(3000),
|
.transform((v) => parseInt(v, 10)),
|
||||||
CORS_ORIGINS: z.string().default("http://localhost:5173,http://localhost:4173"),
|
CORS_ORIGINS: z.string().default("http://localhost:5173,http://localhost:4173"),
|
||||||
LOG_LEVEL: z.string().default("info"),
|
LOG_LEVEL: z.string().default("info"),
|
||||||
|
PUBLIC_APP_URL: z.string().url().optional(),
|
||||||
OPENAPI_DOCS_ENABLED: z
|
OPENAPI_DOCS_ENABLED: z
|
||||||
.string()
|
.string()
|
||||||
.transform((v) => v === "true")
|
.transform((v) => v === "true")
|
||||||
@@ -25,18 +26,18 @@ const EnvSchema = z.object({
|
|||||||
// Master switch: Enable/disable authentication (default: disabled for easy setup)
|
// Master switch: Enable/disable authentication (default: disabled for easy setup)
|
||||||
AUTH_ENABLED: z
|
AUTH_ENABLED: z
|
||||||
.string()
|
.string()
|
||||||
.transform((v) => v === "true")
|
.default("false")
|
||||||
.default(false),
|
.transform((v) => v === "true"),
|
||||||
// Allow new user registrations (auto-enabled if no users exist)
|
// Allow new user registrations (auto-enabled if no users exist)
|
||||||
REGISTRATION_ENABLED: z
|
REGISTRATION_ENABLED: z
|
||||||
.string()
|
.string()
|
||||||
.transform((v) => v === "true")
|
.default("false")
|
||||||
.default(false),
|
.transform((v) => v === "true"),
|
||||||
// Disable username/password form login (useful for OIDC-only setups)
|
// Disable username/password form login (useful for OIDC-only setups)
|
||||||
FORM_LOGIN_ENABLED: z
|
FORM_LOGIN_ENABLED: z
|
||||||
.string()
|
.string()
|
||||||
.transform((v) => v === "true")
|
.default("true")
|
||||||
.default(true),
|
.transform((v) => v === "true"),
|
||||||
|
|
||||||
// JWT Secrets - only required when AUTH_ENABLED=true
|
// JWT Secrets - only required when AUTH_ENABLED=true
|
||||||
JWT_SECRET: z.string().min(10).optional(),
|
JWT_SECRET: z.string().min(10).optional(),
|
||||||
@@ -46,20 +47,20 @@ const EnvSchema = z.object({
|
|||||||
// Token TTL settings
|
// Token TTL settings
|
||||||
ACCESS_TOKEN_TTL_MINUTES: z
|
ACCESS_TOKEN_TTL_MINUTES: z
|
||||||
.string()
|
.string()
|
||||||
.transform((v) => parseInt(v, 10))
|
.default("15")
|
||||||
.default(15),
|
.transform((v) => parseInt(v, 10)),
|
||||||
REFRESH_TOKEN_TTL_DAYS: z
|
REFRESH_TOKEN_TTL_DAYS: z
|
||||||
.string()
|
.string()
|
||||||
.transform((v) => parseInt(v, 10))
|
.default("7")
|
||||||
.default(7),
|
.transform((v) => parseInt(v, 10)),
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// OIDC SSO Configuration (Pocket ID, Authelia, etc.)
|
// OIDC SSO Configuration (Pocket ID, Authelia, etc.)
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
OIDC_ENABLED: z
|
OIDC_ENABLED: z
|
||||||
.string()
|
.string()
|
||||||
.transform((v) => v === "true")
|
.default("false")
|
||||||
.default(false),
|
.transform((v) => v === "true"),
|
||||||
OIDC_ISSUER_URL: z.string().url().optional(), // e.g., https://auth.example.com
|
OIDC_ISSUER_URL: z.string().url().optional(), // e.g., https://auth.example.com
|
||||||
OIDC_CLIENT_ID: z.string().optional(),
|
OIDC_CLIENT_ID: z.string().optional(),
|
||||||
OIDC_CLIENT_SECRET: z.string().optional(),
|
OIDC_CLIENT_SECRET: z.string().optional(),
|
||||||
@@ -67,8 +68,8 @@ const EnvSchema = z.object({
|
|||||||
OIDC_SCOPES: z.string().default("openid profile email"),
|
OIDC_SCOPES: z.string().default("openid profile email"),
|
||||||
OIDC_AUTO_CREATE_USERS: z
|
OIDC_AUTO_CREATE_USERS: z
|
||||||
.string()
|
.string()
|
||||||
.transform((v) => v === "true")
|
.default("true")
|
||||||
.default(true),
|
.transform((v) => v === "true"),
|
||||||
OIDC_USERNAME_CLAIM: z.string().default("preferred_username"), // or 'email', 'sub'
|
OIDC_USERNAME_CLAIM: z.string().default("preferred_username"), // or 'email', 'sub'
|
||||||
OIDC_PROVIDER_NAME: z.string().default("SSO"), // Display name for UI button
|
OIDC_PROVIDER_NAME: z.string().default("SSO"), // Display name for UI button
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,33 +10,34 @@ const EnvSchema = z.object({
|
|||||||
NODE_ENV: z.enum(["development", "production", "test"]).default("production"),
|
NODE_ENV: z.enum(["development", "production", "test"]).default("production"),
|
||||||
PORT: z
|
PORT: z
|
||||||
.string()
|
.string()
|
||||||
.transform((v) => parseInt(v, 10))
|
.default("3000")
|
||||||
.default(3000),
|
.transform((v) => parseInt(v, 10)),
|
||||||
CORS_ORIGINS: z.string().default("http://localhost:5173,http://localhost:4173"),
|
CORS_ORIGINS: z.string().default("http://localhost:5173,http://localhost:4173"),
|
||||||
LOG_LEVEL: z.string().default("info"),
|
LOG_LEVEL: z.string().default("info"),
|
||||||
|
PUBLIC_APP_URL: z.string().url().optional(),
|
||||||
AUTH_ENABLED: z
|
AUTH_ENABLED: z
|
||||||
.string()
|
.string()
|
||||||
.transform((v) => v === "true")
|
.default("false")
|
||||||
.default(false),
|
.transform((v) => v === "true"),
|
||||||
REGISTRATION_ENABLED: z
|
REGISTRATION_ENABLED: z
|
||||||
.string()
|
.string()
|
||||||
.transform((v) => v === "true")
|
.default("false")
|
||||||
.default(false),
|
.transform((v) => v === "true"),
|
||||||
JWT_SECRET: z.string().min(10).optional(),
|
JWT_SECRET: z.string().min(10).optional(),
|
||||||
REFRESH_SECRET: z.string().min(10).optional(),
|
REFRESH_SECRET: z.string().min(10).optional(),
|
||||||
COOKIE_SECRET: z.string().min(10).optional(),
|
COOKIE_SECRET: z.string().min(10).optional(),
|
||||||
ACCESS_TOKEN_TTL_MINUTES: z
|
ACCESS_TOKEN_TTL_MINUTES: z
|
||||||
.string()
|
.string()
|
||||||
.transform((v) => parseInt(v, 10))
|
.default("15")
|
||||||
.default(15),
|
.transform((v) => parseInt(v, 10)),
|
||||||
REFRESH_TOKEN_TTL_DAYS: z
|
REFRESH_TOKEN_TTL_DAYS: z
|
||||||
.string()
|
.string()
|
||||||
.transform((v) => parseInt(v, 10))
|
.default("7")
|
||||||
.default(7),
|
.transform((v) => parseInt(v, 10)),
|
||||||
OIDC_ENABLED: z
|
OIDC_ENABLED: z
|
||||||
.string()
|
.string()
|
||||||
.transform((v) => v === "true")
|
.default("false")
|
||||||
.default(false),
|
.transform((v) => v === "true"),
|
||||||
OIDC_ISSUER_URL: z.string().url().optional(),
|
OIDC_ISSUER_URL: z.string().url().optional(),
|
||||||
OIDC_CLIENT_ID: z.string().optional(),
|
OIDC_CLIENT_ID: z.string().optional(),
|
||||||
OIDC_CLIENT_SECRET: z.string().optional(),
|
OIDC_CLIENT_SECRET: z.string().optional(),
|
||||||
@@ -44,8 +45,8 @@ const EnvSchema = z.object({
|
|||||||
OIDC_SCOPES: z.string().default("openid profile email"),
|
OIDC_SCOPES: z.string().default("openid profile email"),
|
||||||
OIDC_AUTO_CREATE_USERS: z
|
OIDC_AUTO_CREATE_USERS: z
|
||||||
.string()
|
.string()
|
||||||
.transform((v) => v === "true")
|
.default("true")
|
||||||
.default(true),
|
.transform((v) => v === "true"),
|
||||||
OIDC_USERNAME_CLAIM: z.string().default("preferred_username"),
|
OIDC_USERNAME_CLAIM: z.string().default("preferred_username"),
|
||||||
OIDC_PROVIDER_NAME: z.string().default("SSO"),
|
OIDC_PROVIDER_NAME: z.string().default("SSO"),
|
||||||
});
|
});
|
||||||
@@ -81,6 +82,7 @@ describe("EnvSchema", () => {
|
|||||||
expect(result.PORT).toBe(3000);
|
expect(result.PORT).toBe(3000);
|
||||||
expect(result.CORS_ORIGINS).toBe("http://localhost:5173,http://localhost:4173");
|
expect(result.CORS_ORIGINS).toBe("http://localhost:5173,http://localhost:4173");
|
||||||
expect(result.LOG_LEVEL).toBe("info");
|
expect(result.LOG_LEVEL).toBe("info");
|
||||||
|
expect(result.PUBLIC_APP_URL).toBeUndefined();
|
||||||
expect(result.AUTH_ENABLED).toBe(false);
|
expect(result.AUTH_ENABLED).toBe(false);
|
||||||
expect(result.REGISTRATION_ENABLED).toBe(false);
|
expect(result.REGISTRATION_ENABLED).toBe(false);
|
||||||
expect(result.ACCESS_TOKEN_TTL_MINUTES).toBe(15);
|
expect(result.ACCESS_TOKEN_TTL_MINUTES).toBe(15);
|
||||||
@@ -188,6 +190,15 @@ describe("EnvSchema", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("OIDC URL validation", () => {
|
describe("OIDC URL validation", () => {
|
||||||
|
it("should accept valid PUBLIC_APP_URL", () => {
|
||||||
|
const result = EnvSchema.parse({ PUBLIC_APP_URL: "https://medassist.example.com" });
|
||||||
|
expect(result.PUBLIC_APP_URL).toBe("https://medassist.example.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject invalid PUBLIC_APP_URL", () => {
|
||||||
|
expect(() => EnvSchema.parse({ PUBLIC_APP_URL: "not-a-url" })).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
it("should accept valid OIDC_ISSUER_URL", () => {
|
it("should accept valid OIDC_ISSUER_URL", () => {
|
||||||
const result = EnvSchema.parse({ OIDC_ISSUER_URL: "https://auth.example.com" });
|
const result = EnvSchema.parse({ OIDC_ISSUER_URL: "https://auth.example.com" });
|
||||||
expect(result.OIDC_ISSUER_URL).toBe("https://auth.example.com");
|
expect(result.OIDC_ISSUER_URL).toBe("https://auth.example.com");
|
||||||
|
|||||||
Reference in New Issue
Block a user