feat(auth): implement user authentication and profile management
- Added authentication context and provider to manage user state. - Created login and registration forms with validation and error handling. - Implemented user profile component for updating user information and changing passwords. - Introduced user settings in the database for notification preferences. - Updated translations for authentication-related strings in English and German. - Enhanced styles for authentication components and user profile. - Added middleware for optional and required authentication checks.
This commit is contained in:
+40
-46
@@ -11,32 +11,66 @@ async function main() {
|
||||
|
||||
const client = createClient({ url });
|
||||
|
||||
// Create tables directly
|
||||
// Create tables - fresh schema without roles, with per-user settings
|
||||
const sql = `
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id integer PRIMARY KEY AUTOINCREMENT,
|
||||
email text NOT NULL UNIQUE,
|
||||
password_hash text NOT NULL,
|
||||
role text NOT NULL DEFAULT 'user',
|
||||
username text NOT NULL UNIQUE,
|
||||
password_hash text,
|
||||
auth_provider text NOT NULL DEFAULT 'local',
|
||||
is_active integer NOT NULL DEFAULT 1,
|
||||
last_login_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 medications (
|
||||
id integer PRIMARY KEY AUTOINCREMENT,
|
||||
name text NOT NULL UNIQUE,
|
||||
user_id integer NOT NULL,
|
||||
name text NOT NULL,
|
||||
generic_name text,
|
||||
taken_by text,
|
||||
count integer NOT NULL DEFAULT 0,
|
||||
strips integer NOT NULL DEFAULT 0,
|
||||
pack_count integer NOT NULL DEFAULT 1,
|
||||
strips_per_pack integer NOT NULL DEFAULT 1,
|
||||
tabs_per_strip integer NOT NULL DEFAULT 1,
|
||||
loose_tablets integer NOT NULL DEFAULT 0,
|
||||
pill_weight_mg integer,
|
||||
usage_json text NOT NULL DEFAULT '[]',
|
||||
every_json text NOT NULL DEFAULT '[]',
|
||||
start_json text NOT NULL DEFAULT '[]',
|
||||
strip_size integer NOT NULL DEFAULT 1,
|
||||
image_url text,
|
||||
updated_at integer NOT NULL DEFAULT (strftime('%s','now'))
|
||||
expiry_date text,
|
||||
notes text,
|
||||
intake_reminders_enabled integer NOT NULL DEFAULT 0,
|
||||
updated_at integer NOT NULL DEFAULT (strftime('%s','now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_settings (
|
||||
id integer PRIMARY KEY AUTOINCREMENT,
|
||||
user_id integer NOT NULL UNIQUE,
|
||||
email_enabled integer NOT NULL DEFAULT 0,
|
||||
notification_email text,
|
||||
email_stock_reminders integer NOT NULL DEFAULT 1,
|
||||
email_intake_reminders integer NOT NULL DEFAULT 1,
|
||||
shoutrrr_enabled integer NOT NULL DEFAULT 0,
|
||||
shoutrrr_url text,
|
||||
shoutrrr_stock_reminders integer NOT NULL DEFAULT 1,
|
||||
shoutrrr_intake_reminders integer NOT NULL DEFAULT 1,
|
||||
reminder_days_before integer NOT NULL DEFAULT 7,
|
||||
repeat_daily_reminders integer NOT NULL DEFAULT 0,
|
||||
low_stock_days integer NOT NULL DEFAULT 30,
|
||||
normal_stock_days integer NOT NULL DEFAULT 90,
|
||||
high_stock_days integer NOT NULL DEFAULT 180,
|
||||
language text NOT NULL DEFAULT 'en',
|
||||
last_auto_email_sent text,
|
||||
last_notification_type text,
|
||||
last_notification_channel text,
|
||||
updated_at integer NOT NULL DEFAULT (strftime('%s','now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS refresh_tokens (
|
||||
@@ -49,20 +83,6 @@ async function main() {
|
||||
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
id integer PRIMARY KEY AUTOINCREMENT,
|
||||
smtp_host text,
|
||||
smtp_port integer,
|
||||
smtp_user text,
|
||||
smtp_pass_encrypted text,
|
||||
smtp_from text,
|
||||
smtp_secure integer NOT NULL DEFAULT 0,
|
||||
email_enabled integer NOT NULL DEFAULT 0,
|
||||
notification_email text,
|
||||
reminder_days_before integer NOT NULL DEFAULT 7,
|
||||
updated_at integer NOT NULL DEFAULT (strftime('%s','now'))
|
||||
);
|
||||
`;
|
||||
|
||||
// Execute each statement separately
|
||||
@@ -73,32 +93,6 @@ async function main() {
|
||||
await client.execute(stmt);
|
||||
}
|
||||
|
||||
// Run migrations for existing databases
|
||||
console.log("Running migrations for existing databases...");
|
||||
|
||||
const migrations = [
|
||||
{ column: "image_url", sql: "ALTER TABLE medications ADD COLUMN image_url TEXT" },
|
||||
{ column: "expiry_date", sql: "ALTER TABLE medications ADD COLUMN expiry_date TEXT" },
|
||||
{ column: "notes", sql: "ALTER TABLE medications ADD COLUMN notes TEXT" },
|
||||
{ column: "generic_name", sql: "ALTER TABLE medications ADD COLUMN generic_name TEXT" },
|
||||
{ column: "intake_reminders_enabled", sql: "ALTER TABLE medications ADD COLUMN intake_reminders_enabled INTEGER NOT NULL DEFAULT 0" },
|
||||
{ column: "pill_weight_mg", sql: "ALTER TABLE medications ADD COLUMN pill_weight_mg INTEGER" },
|
||||
{ column: "taken_by", sql: "ALTER TABLE medications ADD COLUMN taken_by TEXT" },
|
||||
];
|
||||
|
||||
for (const migration of migrations) {
|
||||
try {
|
||||
await client.execute(migration.sql);
|
||||
console.log(`Added ${migration.column} column`);
|
||||
} catch (e: any) {
|
||||
if (e.message?.includes("duplicate column") || e.message?.includes("already exists")) {
|
||||
console.log(`${migration.column} column already exists, skipping`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Database setup complete!");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
-- Add user_id to medications (for existing databases)
|
||||
-- First, add the column as nullable
|
||||
ALTER TABLE medications ADD COLUMN user_id INTEGER REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
-- Create user_settings table for per-user notification settings
|
||||
CREATE TABLE IF NOT EXISTS user_settings (
|
||||
id integer PRIMARY KEY AUTOINCREMENT,
|
||||
user_id integer NOT NULL UNIQUE,
|
||||
email_enabled integer NOT NULL DEFAULT 0,
|
||||
notification_email text,
|
||||
email_stock_reminders integer NOT NULL DEFAULT 1,
|
||||
email_intake_reminders integer NOT NULL DEFAULT 1,
|
||||
shoutrrr_enabled integer NOT NULL DEFAULT 0,
|
||||
shoutrrr_url text,
|
||||
shoutrrr_stock_reminders integer NOT NULL DEFAULT 1,
|
||||
shoutrrr_intake_reminders integer NOT NULL DEFAULT 1,
|
||||
reminder_days_before integer NOT NULL DEFAULT 7,
|
||||
repeat_daily_reminders integer NOT NULL DEFAULT 0,
|
||||
low_stock_days integer NOT NULL DEFAULT 30,
|
||||
normal_stock_days integer NOT NULL DEFAULT 90,
|
||||
high_stock_days integer NOT NULL DEFAULT 180,
|
||||
language text NOT NULL DEFAULT 'en',
|
||||
last_auto_email_sent text,
|
||||
last_notification_type text,
|
||||
last_notification_channel text,
|
||||
updated_at integer NOT NULL DEFAULT (strftime('%s','now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
@@ -9,6 +9,7 @@
|
||||
{ "idx": 6, "version": 1, "when": 1735200000, "tag": "0006_add_generic_name", "breakpoint": false },
|
||||
{ "idx": 7, "version": 1, "when": 1735300000, "tag": "0007_add_intake_reminders", "breakpoint": false },
|
||||
{ "idx": 8, "version": 1, "when": 1735400000, "tag": "0008_add_pill_weight", "breakpoint": false },
|
||||
{ "idx": 9, "version": 1, "when": 1735500000, "tag": "0009_add_taken_by", "breakpoint": false }
|
||||
{ "idx": 9, "version": 1, "when": 1735500000, "tag": "0009_add_taken_by", "breakpoint": false },
|
||||
{ "idx": 10, "version": 1, "when": 1735600000, "tag": "0010_add_user_settings", "breakpoint": false }
|
||||
]
|
||||
}
|
||||
|
||||
+50
-20
@@ -1,18 +1,27 @@
|
||||
import { sqliteTable, text, integer, real } from "drizzle-orm/sqlite-core";
|
||||
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
// =============================================================================
|
||||
// Users - Simple auth, no roles (every user is equal)
|
||||
// =============================================================================
|
||||
export const users = sqliteTable("users", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
email: text("email", { length: 255 }).notNull().unique(),
|
||||
passwordHash: text("password_hash", { length: 255 }).notNull(),
|
||||
role: text("role", { length: 50 }).notNull().default("user"),
|
||||
username: text("username", { length: 100 }).notNull().unique(),
|
||||
passwordHash: text("password_hash", { length: 255 }),
|
||||
authProvider: text("auth_provider", { length: 50 }).notNull().default("local"),
|
||||
isActive: integer("is_active", { mode: "boolean" }).notNull().default(true),
|
||||
lastLoginAt: integer("last_login_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`),
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Medications - Per user
|
||||
// =============================================================================
|
||||
export const medications = sqliteTable("medications", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
name: text("name", { length: 100 }).notNull().unique(),
|
||||
userId: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
name: text("name", { length: 100 }).notNull(),
|
||||
genericName: text("generic_name", { length: 100 }),
|
||||
takenBy: text("taken_by", { length: 100 }),
|
||||
count: integer("count").notNull().default(0),
|
||||
@@ -33,6 +42,42 @@ export const medications = sqliteTable("medications", {
|
||||
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// User Settings - Per user (email, push, thresholds, language)
|
||||
// =============================================================================
|
||||
export const userSettings = sqliteTable("user_settings", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
userId: integer("user_id").notNull().unique().references(() => users.id, { onDelete: "cascade" }),
|
||||
// Email notifications
|
||||
emailEnabled: integer("email_enabled", { mode: "boolean" }).notNull().default(false),
|
||||
notificationEmail: text("notification_email"),
|
||||
emailStockReminders: integer("email_stock_reminders", { mode: "boolean" }).notNull().default(true),
|
||||
emailIntakeReminders: integer("email_intake_reminders", { mode: "boolean" }).notNull().default(true),
|
||||
// Push notifications (shoutrrr/ntfy)
|
||||
shoutrrrEnabled: integer("shoutrrr_enabled", { mode: "boolean" }).notNull().default(false),
|
||||
shoutrrrUrl: text("shoutrrr_url"),
|
||||
shoutrrrStockReminders: integer("shoutrrr_stock_reminders", { mode: "boolean" }).notNull().default(true),
|
||||
shoutrrrIntakeReminders: integer("shoutrrr_intake_reminders", { mode: "boolean" }).notNull().default(true),
|
||||
// Reminder settings
|
||||
reminderDaysBefore: integer("reminder_days_before").notNull().default(7),
|
||||
repeatDailyReminders: integer("repeat_daily_reminders", { mode: "boolean" }).notNull().default(false),
|
||||
// Stock thresholds (days)
|
||||
lowStockDays: integer("low_stock_days").notNull().default(30),
|
||||
normalStockDays: integer("normal_stock_days").notNull().default(90),
|
||||
highStockDays: integer("high_stock_days").notNull().default(180),
|
||||
// UI preferences
|
||||
language: text("language", { length: 10 }).notNull().default("en"),
|
||||
// Last notification tracking
|
||||
lastAutoEmailSent: text("last_auto_email_sent"),
|
||||
lastNotificationType: text("last_notification_type"),
|
||||
lastNotificationChannel: text("last_notification_channel"),
|
||||
// Timestamps
|
||||
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Refresh Tokens - For JWT rotation
|
||||
// =============================================================================
|
||||
export const refreshTokens = sqliteTable("refresh_tokens", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
userId: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
@@ -42,18 +87,3 @@ export const refreshTokens = sqliteTable("refresh_tokens", {
|
||||
revoked: integer("revoked", { mode: "boolean" }).notNull().default(false),
|
||||
createdAt: integer("created_at", { mode: "timestamp" }).notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
});
|
||||
|
||||
export const settings = sqliteTable("settings", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
smtpHost: text("smtp_host"),
|
||||
smtpPort: integer("smtp_port"),
|
||||
smtpUser: text("smtp_user"),
|
||||
smtpPassEncrypted: text("smtp_pass_encrypted"),
|
||||
smtpFrom: text("smtp_from"),
|
||||
smtpSecure: integer("smtp_secure", { mode: "boolean" }).notNull().default(false),
|
||||
// Email notification settings
|
||||
emailEnabled: integer("email_enabled", { mode: "boolean" }).notNull().default(false),
|
||||
notificationEmail: text("notification_email"),
|
||||
reminderDaysBefore: integer("reminder_days_before").notNull().default(7),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user