feat: add auth and database foundation
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
# Drizzle
|
||||
DATABASE_URL=file:local.db
|
||||
|
||||
ORIGIN=""
|
||||
|
||||
# Better Auth
|
||||
# For production use 32 characters and generated with high entropy
|
||||
# https://www.better-auth.com/docs/installation
|
||||
BETTER_AUTH_SECRET=""
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
|
||||
if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is not set');
|
||||
|
||||
export default defineConfig({
|
||||
schema: './src/lib/server/db/schema.ts',
|
||||
dialect: 'sqlite',
|
||||
dbCredentials: { url: process.env.DATABASE_URL },
|
||||
verbose: true,
|
||||
strict: true
|
||||
});
|
||||
@@ -0,0 +1,188 @@
|
||||
CREATE TABLE `task` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`title` text NOT NULL,
|
||||
`priority` integer DEFAULT 1 NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `clients` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`type` text DEFAULT 'business' NOT NULL,
|
||||
`status` text DEFAULT 'active' NOT NULL,
|
||||
`email` text,
|
||||
`phone` text,
|
||||
`website` text,
|
||||
`notes` text,
|
||||
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`archived_at` integer
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `addresses` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`client_id` text NOT NULL,
|
||||
`label` text NOT NULL,
|
||||
`line_1` text NOT NULL,
|
||||
`line_2` text,
|
||||
`city` text NOT NULL,
|
||||
`region` text,
|
||||
`postcode` text NOT NULL,
|
||||
`country` text DEFAULT 'United Kingdom' NOT NULL,
|
||||
`is_primary` integer DEFAULT false NOT NULL,
|
||||
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`archived_at` integer,
|
||||
FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`) ON UPDATE no action ON DELETE restrict
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `addresses_client_id_idx` ON `addresses` (`client_id`);--> statement-breakpoint
|
||||
CREATE TABLE `contacts` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`client_id` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`role` text,
|
||||
`email` text,
|
||||
`phone` text,
|
||||
`is_primary` integer DEFAULT false NOT NULL,
|
||||
`notes` text,
|
||||
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`archived_at` integer,
|
||||
FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`) ON UPDATE no action ON DELETE restrict
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `contacts_client_id_idx` ON `contacts` (`client_id`);--> statement-breakpoint
|
||||
CREATE TABLE `rooms` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`type` text DEFAULT 'room' NOT NULL,
|
||||
`capacity` integer DEFAULT 1 NOT NULL,
|
||||
`location` text,
|
||||
`status` text DEFAULT 'available' NOT NULL,
|
||||
`hourly_rate_cents` integer DEFAULT 0 NOT NULL,
|
||||
`notes` text,
|
||||
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`archived_at` integer
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `services` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`category` text,
|
||||
`unit` text DEFAULT 'each' NOT NULL,
|
||||
`price_cents` integer DEFAULT 0 NOT NULL,
|
||||
`description` text,
|
||||
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`archived_at` integer
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `bookings` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`client_id` text NOT NULL,
|
||||
`room_id` text NOT NULL,
|
||||
`service_id` text,
|
||||
`starts_at` text NOT NULL,
|
||||
`ends_at` text NOT NULL,
|
||||
`status` text DEFAULT 'booked' NOT NULL,
|
||||
`notes` text,
|
||||
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`archived_at` integer,
|
||||
FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`) ON UPDATE no action ON DELETE restrict,
|
||||
FOREIGN KEY (`room_id`) REFERENCES `rooms`(`id`) ON UPDATE no action ON DELETE restrict,
|
||||
FOREIGN KEY (`service_id`) REFERENCES `services`(`id`) ON UPDATE no action ON DELETE set null
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `bookings_client_id_idx` ON `bookings` (`client_id`);--> statement-breakpoint
|
||||
CREATE INDEX `bookings_room_id_idx` ON `bookings` (`room_id`);--> statement-breakpoint
|
||||
CREATE INDEX `bookings_service_id_idx` ON `bookings` (`service_id`);--> statement-breakpoint
|
||||
CREATE TABLE `invoices` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`client_id` text NOT NULL,
|
||||
`invoice_number` text NOT NULL,
|
||||
`issue_date` text NOT NULL,
|
||||
`due_date` text NOT NULL,
|
||||
`status` text DEFAULT 'draft' NOT NULL,
|
||||
`subtotal_cents` integer DEFAULT 0 NOT NULL,
|
||||
`tax_cents` integer DEFAULT 0 NOT NULL,
|
||||
`total_cents` integer DEFAULT 0 NOT NULL,
|
||||
`notes` text,
|
||||
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`archived_at` integer,
|
||||
FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`) ON UPDATE no action ON DELETE restrict
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `invoices_invoice_number_unique` ON `invoices` (`invoice_number`);--> statement-breakpoint
|
||||
CREATE INDEX `invoices_client_id_idx` ON `invoices` (`client_id`);--> statement-breakpoint
|
||||
CREATE TABLE `contracts` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`client_id` text NOT NULL,
|
||||
`title` text NOT NULL,
|
||||
`start_date` text NOT NULL,
|
||||
`end_date` text,
|
||||
`status` text DEFAULT 'draft' NOT NULL,
|
||||
`value_cents` integer DEFAULT 0 NOT NULL,
|
||||
`notes` text,
|
||||
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`archived_at` integer,
|
||||
FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`) ON UPDATE no action ON DELETE restrict
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `contracts_client_id_idx` ON `contracts` (`client_id`);--> statement-breakpoint
|
||||
CREATE TABLE `account` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`account_id` text NOT NULL,
|
||||
`provider_id` text NOT NULL,
|
||||
`user_id` text NOT NULL,
|
||||
`access_token` text,
|
||||
`refresh_token` text,
|
||||
`id_token` text,
|
||||
`access_token_expires_at` integer,
|
||||
`refresh_token_expires_at` integer,
|
||||
`scope` text,
|
||||
`password` text,
|
||||
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`updated_at` integer NOT NULL,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `account_userId_idx` ON `account` (`user_id`);--> statement-breakpoint
|
||||
CREATE TABLE `session` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`expires_at` integer NOT NULL,
|
||||
`token` text NOT NULL,
|
||||
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`updated_at` integer NOT NULL,
|
||||
`ip_address` text,
|
||||
`user_agent` text,
|
||||
`user_id` text NOT NULL,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `session_token_unique` ON `session` (`token`);--> statement-breakpoint
|
||||
CREATE INDEX `session_userId_idx` ON `session` (`user_id`);--> statement-breakpoint
|
||||
CREATE TABLE `user` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`email` text NOT NULL,
|
||||
`email_verified` integer DEFAULT false NOT NULL,
|
||||
`image` text,
|
||||
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);--> statement-breakpoint
|
||||
CREATE TABLE `verification` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`identifier` text NOT NULL,
|
||||
`value` text NOT NULL,
|
||||
`expires_at` integer NOT NULL,
|
||||
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `verification_identifier_idx` ON `verification` (`identifier`);
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `clients` DROP COLUMN `email`;--> statement-breakpoint
|
||||
ALTER TABLE `clients` DROP COLUMN `phone`;
|
||||
@@ -0,0 +1,24 @@
|
||||
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||
CREATE TABLE `__new_rooms` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`type` text DEFAULT 'meeting_room' NOT NULL,
|
||||
`sq_ft` integer,
|
||||
`internal_price_per_hour_cents` integer,
|
||||
`external_price_per_hour_cents` integer,
|
||||
`internal_price_per_day_cents` integer,
|
||||
`external_price_per_day_cents` integer,
|
||||
`internal_price_per_half_day_cents` integer,
|
||||
`external_price_per_half_day_cents` integer,
|
||||
`max_attendees` integer,
|
||||
`workstations` integer,
|
||||
`price_per_month_cents` integer,
|
||||
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`archived_at` integer
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `__new_rooms`("id", "name", "type", "sq_ft", "internal_price_per_hour_cents", "external_price_per_hour_cents", "internal_price_per_day_cents", "external_price_per_day_cents", "internal_price_per_half_day_cents", "external_price_per_half_day_cents", "max_attendees", "workstations", "price_per_month_cents", "created_at", "updated_at", "archived_at") SELECT "id", "name", CASE WHEN "type" = 'office' THEN 'private_office' ELSE 'meeting_room' END, NULL, "hourly_rate_cents", "hourly_rate_cents", NULL, NULL, NULL, NULL, "capacity", NULL, NULL, "created_at", "updated_at", "archived_at" FROM `rooms`;--> statement-breakpoint
|
||||
DROP TABLE `rooms`;--> statement-breakpoint
|
||||
ALTER TABLE `__new_rooms` RENAME TO `rooms`;--> statement-breakpoint
|
||||
PRAGMA foreign_keys=ON;
|
||||
@@ -0,0 +1,80 @@
|
||||
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||
CREATE TABLE `__new_rooms` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`type` text DEFAULT 'meeting_room' NOT NULL,
|
||||
`sq_ft` integer,
|
||||
`internal_price_per_hour_gbp` real,
|
||||
`external_price_per_hour_gbp` real,
|
||||
`internal_price_per_day_gbp` real,
|
||||
`external_price_per_day_gbp` real,
|
||||
`internal_price_per_half_day_gbp` real,
|
||||
`external_price_per_half_day_gbp` real,
|
||||
`max_attendees` integer,
|
||||
`workstations` integer,
|
||||
`price_per_month_gbp` real,
|
||||
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`archived_at` integer
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `__new_rooms`("id", "name", "type", "sq_ft", "internal_price_per_hour_gbp", "external_price_per_hour_gbp", "internal_price_per_day_gbp", "external_price_per_day_gbp", "internal_price_per_half_day_gbp", "external_price_per_half_day_gbp", "max_attendees", "workstations", "price_per_month_gbp", "created_at", "updated_at", "archived_at") SELECT "id", "name", "type", "sq_ft", "internal_price_per_hour_cents" / 100.0, "external_price_per_hour_cents" / 100.0, "internal_price_per_day_cents" / 100.0, "external_price_per_day_cents" / 100.0, "internal_price_per_half_day_cents" / 100.0, "external_price_per_half_day_cents" / 100.0, "max_attendees", "workstations", "price_per_month_cents" / 100.0, "created_at", "updated_at", "archived_at" FROM `rooms`;--> statement-breakpoint
|
||||
DROP TABLE `rooms`;--> statement-breakpoint
|
||||
ALTER TABLE `__new_rooms` RENAME TO `rooms`;--> statement-breakpoint
|
||||
PRAGMA foreign_keys=ON;--> statement-breakpoint
|
||||
CREATE TABLE `__new_services` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`category` text,
|
||||
`unit` text DEFAULT 'each' NOT NULL,
|
||||
`price_gbp` real DEFAULT 0 NOT NULL,
|
||||
`description` text,
|
||||
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`archived_at` integer
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `__new_services`("id", "name", "category", "unit", "price_gbp", "description", "created_at", "updated_at", "archived_at") SELECT "id", "name", "category", "unit", "price_cents" / 100.0, "description", "created_at", "updated_at", "archived_at" FROM `services`;--> statement-breakpoint
|
||||
DROP TABLE `services`;--> statement-breakpoint
|
||||
ALTER TABLE `__new_services` RENAME TO `services`;--> statement-breakpoint
|
||||
CREATE TABLE `__new_invoices` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`client_id` text NOT NULL,
|
||||
`invoice_number` text NOT NULL,
|
||||
`issue_date` text NOT NULL,
|
||||
`due_date` text NOT NULL,
|
||||
`status` text DEFAULT 'draft' NOT NULL,
|
||||
`subtotal_gbp` real DEFAULT 0 NOT NULL,
|
||||
`tax_gbp` real DEFAULT 0 NOT NULL,
|
||||
`total_gbp` real DEFAULT 0 NOT NULL,
|
||||
`notes` text,
|
||||
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`archived_at` integer,
|
||||
FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`) ON UPDATE no action ON DELETE restrict
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `__new_invoices`("id", "client_id", "invoice_number", "issue_date", "due_date", "status", "subtotal_gbp", "tax_gbp", "total_gbp", "notes", "created_at", "updated_at", "archived_at") SELECT "id", "client_id", "invoice_number", "issue_date", "due_date", "status", "subtotal_cents" / 100.0, "tax_cents" / 100.0, "total_cents" / 100.0, "notes", "created_at", "updated_at", "archived_at" FROM `invoices`;--> statement-breakpoint
|
||||
DROP TABLE `invoices`;--> statement-breakpoint
|
||||
ALTER TABLE `__new_invoices` RENAME TO `invoices`;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `invoices_invoice_number_unique` ON `invoices` (`invoice_number`);--> statement-breakpoint
|
||||
CREATE INDEX `invoices_client_id_idx` ON `invoices` (`client_id`);--> statement-breakpoint
|
||||
CREATE TABLE `__new_contracts` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`client_id` text NOT NULL,
|
||||
`title` text NOT NULL,
|
||||
`start_date` text NOT NULL,
|
||||
`end_date` text,
|
||||
`status` text DEFAULT 'draft' NOT NULL,
|
||||
`value_gbp` real DEFAULT 0 NOT NULL,
|
||||
`notes` text,
|
||||
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`archived_at` integer,
|
||||
FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`) ON UPDATE no action ON DELETE restrict
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `__new_contracts`("id", "client_id", "title", "start_date", "end_date", "status", "value_gbp", "notes", "created_at", "updated_at", "archived_at") SELECT "id", "client_id", "title", "start_date", "end_date", "status", "value_cents" / 100.0, "notes", "created_at", "updated_at", "archived_at" FROM `contracts`;--> statement-breakpoint
|
||||
DROP TABLE `contracts`;--> statement-breakpoint
|
||||
ALTER TABLE `__new_contracts` RENAME TO `contracts`;--> statement-breakpoint
|
||||
CREATE INDEX `contracts_client_id_idx` ON `contracts` (`client_id`);
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `rooms` DROP COLUMN `internal_price_per_half_day_gbp`;--> statement-breakpoint
|
||||
ALTER TABLE `rooms` DROP COLUMN `external_price_per_half_day_gbp`;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1779571091471,
|
||||
"tag": "0000_wild_guardian",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "6",
|
||||
"when": 1780516359556,
|
||||
"tag": "0001_amusing_maximus",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "6",
|
||||
"when": 1780608782775,
|
||||
"tag": "0002_volatile_gressill",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "6",
|
||||
"when": 1780609384877,
|
||||
"tag": "0003_tan_george_stacy",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 4,
|
||||
"version": "6",
|
||||
"when": 1780609661307,
|
||||
"tag": "0004_orange_ultimatum",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { createClient } from '@libsql/client';
|
||||
import { hashPassword } from 'better-auth/crypto';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
function readDotEnv() {
|
||||
try {
|
||||
const entries = readFileSync('.env', 'utf8')
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line && !line.startsWith('#'))
|
||||
.map((line) => {
|
||||
const index = line.indexOf('=');
|
||||
const key = line.slice(0, index);
|
||||
const value = line.slice(index + 1).replace(/^"|"$/g, '');
|
||||
return [key, value];
|
||||
});
|
||||
|
||||
for (const [key, value] of entries) {
|
||||
process.env[key] ??= value;
|
||||
}
|
||||
} catch {
|
||||
// .env is optional in deployed or scripted environments.
|
||||
}
|
||||
}
|
||||
|
||||
readDotEnv();
|
||||
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
const email = process.env.ADMIN_EMAIL?.trim().toLowerCase();
|
||||
const password = process.env.ADMIN_PASSWORD;
|
||||
const name = process.env.ADMIN_NAME?.trim() || 'Administrator';
|
||||
const overwrite = process.env.ADMIN_OVERWRITE === '1';
|
||||
|
||||
if (!databaseUrl) {
|
||||
console.error('DATABASE_URL is required.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!email || !password) {
|
||||
console.error('ADMIN_EMAIL and ADMIN_PASSWORD are required.');
|
||||
console.error('Example: ADMIN_EMAIL=admin@example.com ADMIN_PASSWORD=change-me pnpm admin:create');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = createClient({ url: databaseUrl });
|
||||
const now = Date.now();
|
||||
const existing = await client.execute({
|
||||
sql: 'select id from user where email = ? limit 1',
|
||||
args: [email]
|
||||
});
|
||||
|
||||
if (existing.rows.length > 0 && !overwrite) {
|
||||
console.error(`A user with email ${email} already exists. Set ADMIN_OVERWRITE=1 to update the password.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const userId = existing.rows[0]?.id?.toString() ?? randomUUID();
|
||||
const passwordHash = await hashPassword(password);
|
||||
|
||||
if (existing.rows.length === 0) {
|
||||
await client.execute({
|
||||
sql: 'insert into user (id, name, email, email_verified, image, created_at, updated_at) values (?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [userId, name, email, 1, null, now, now]
|
||||
});
|
||||
} else {
|
||||
await client.execute({
|
||||
sql: 'update user set name = ?, email_verified = ?, updated_at = ? where id = ?',
|
||||
args: [name, 1, now, userId]
|
||||
});
|
||||
}
|
||||
|
||||
const account = await client.execute({
|
||||
sql: 'select id from account where user_id = ? and provider_id = ? limit 1',
|
||||
args: [userId, 'credential']
|
||||
});
|
||||
|
||||
if (account.rows.length === 0) {
|
||||
await client.execute({
|
||||
sql: 'insert into account (id, account_id, provider_id, user_id, password, created_at, updated_at) values (?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [randomUUID(), userId, 'credential', userId, passwordHash, now, now]
|
||||
});
|
||||
} else {
|
||||
await client.execute({
|
||||
sql: 'update account set password = ?, updated_at = ? where id = ?',
|
||||
args: [passwordHash, now, account.rows[0].id]
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Admin user ready: ${email}`);
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
import { building } from '$app/environment';
|
||||
import { auth } from '$lib/server/auth';
|
||||
import { svelteKitHandler } from 'better-auth/svelte-kit';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
const handleBetterAuth: Handle = async ({ event, resolve }) => {
|
||||
const session = await auth.api.getSession({ headers: event.request.headers });
|
||||
|
||||
if (session) {
|
||||
event.locals.session = session.session;
|
||||
event.locals.user = session.user;
|
||||
}
|
||||
|
||||
if (event.url.pathname.startsWith('/dashboard') && !event.locals.user) {
|
||||
const redirectTo = event.url.pathname + event.url.search;
|
||||
redirect(303, `/login?redirectTo=${encodeURIComponent(redirectTo)}`);
|
||||
}
|
||||
|
||||
return svelteKitHandler({ event, resolve, auth, building });
|
||||
};
|
||||
|
||||
export const handle: Handle = handleBetterAuth;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const passwordMessage = 'Password must be at least 8 characters.';
|
||||
const optionalPassword = z.string().refine((value) => value.length === 0 || value.length >= 8, {
|
||||
message: passwordMessage
|
||||
});
|
||||
|
||||
export const createAdminSchema = z.object({
|
||||
name: z.string().trim().min(1, 'Enter a name.'),
|
||||
email: z.email('Enter a valid email address.').transform((value) => value.toLowerCase()),
|
||||
password: z.string().min(8, passwordMessage)
|
||||
});
|
||||
|
||||
export const editAdminSchema = z.object({
|
||||
id: z.string().min(1, 'Admin id is required.'),
|
||||
name: z.string().trim().min(1, 'Enter a name.'),
|
||||
email: z.email('Enter a valid email address.').transform((value) => value.toLowerCase()),
|
||||
password: optionalPassword
|
||||
});
|
||||
|
||||
export const deleteAdminSchema = z.object({
|
||||
id: z.string().min(1, 'Admin id is required.')
|
||||
});
|
||||
|
||||
export type CreateAdminSchema = typeof createAdminSchema;
|
||||
export type EditAdminSchema = typeof editAdminSchema;
|
||||
export type DeleteAdminSchema = typeof deleteAdminSchema;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const loginSchema = z.object({
|
||||
email: z.email('Enter a valid email address.'),
|
||||
password: z.string().min(1, 'Enter your password.')
|
||||
});
|
||||
|
||||
export type LoginSchema = typeof loginSchema;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { betterAuth } from 'better-auth/minimal';
|
||||
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
|
||||
import { sveltekitCookies } from 'better-auth/svelte-kit';
|
||||
import { env } from '$env/dynamic/private';
|
||||
import { getRequestEvent } from '$app/server';
|
||||
import { db } from '$lib/server/db';
|
||||
|
||||
export const auth = betterAuth({
|
||||
baseURL: env.ORIGIN,
|
||||
secret: env.BETTER_AUTH_SECRET,
|
||||
database: drizzleAdapter(db, { provider: 'sqlite' }),
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
disableSignUp: true
|
||||
},
|
||||
plugins: [
|
||||
sveltekitCookies(getRequestEvent) // make sure this is the last plugin in the array
|
||||
]
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
import { clients } from './clients.schema';
|
||||
import { timestamps } from './shared.schema';
|
||||
|
||||
export const addresses = sqliteTable(
|
||||
'addresses',
|
||||
{
|
||||
id: text('id')
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
clientId: text('client_id')
|
||||
.notNull()
|
||||
.references(() => clients.id, { onDelete: 'restrict' }),
|
||||
label: text('label').notNull(),
|
||||
line1: text('line_1').notNull(),
|
||||
line2: text('line_2'),
|
||||
city: text('city').notNull(),
|
||||
region: text('region'),
|
||||
postcode: text('postcode').notNull(),
|
||||
country: text('country').notNull().default('United Kingdom'),
|
||||
isPrimary: integer('is_primary', { mode: 'boolean' }).notNull().default(false),
|
||||
...timestamps
|
||||
},
|
||||
(table) => [index('addresses_client_id_idx').on(table.clientId)]
|
||||
);
|
||||
@@ -0,0 +1,105 @@
|
||||
import { relations, sql } from 'drizzle-orm';
|
||||
import { sqliteTable, text, integer, index } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
export const user = sqliteTable('user', {
|
||||
id: text('id').primaryKey(),
|
||||
name: text('name').notNull(),
|
||||
email: text('email').notNull().unique(),
|
||||
emailVerified: integer('email_verified', { mode: 'boolean' }).default(false).notNull(),
|
||||
image: text('image'),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||
.notNull(),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
|
||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull()
|
||||
});
|
||||
|
||||
export const session = sqliteTable(
|
||||
'session',
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
expiresAt: integer('expires_at', { mode: 'timestamp_ms' }).notNull(),
|
||||
token: text('token').notNull().unique(),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||
.notNull(),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
ipAddress: text('ip_address'),
|
||||
userAgent: text('user_agent'),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: 'cascade' })
|
||||
},
|
||||
(table) => [index('session_userId_idx').on(table.userId)]
|
||||
);
|
||||
|
||||
export const account = sqliteTable(
|
||||
'account',
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
accountId: text('account_id').notNull(),
|
||||
providerId: text('provider_id').notNull(),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: 'cascade' }),
|
||||
accessToken: text('access_token'),
|
||||
refreshToken: text('refresh_token'),
|
||||
idToken: text('id_token'),
|
||||
accessTokenExpiresAt: integer('access_token_expires_at', {
|
||||
mode: 'timestamp_ms'
|
||||
}),
|
||||
refreshTokenExpiresAt: integer('refresh_token_expires_at', {
|
||||
mode: 'timestamp_ms'
|
||||
}),
|
||||
scope: text('scope'),
|
||||
password: text('password'),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||
.notNull(),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull()
|
||||
},
|
||||
(table) => [index('account_userId_idx').on(table.userId)]
|
||||
);
|
||||
|
||||
export const verification = sqliteTable(
|
||||
'verification',
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
identifier: text('identifier').notNull(),
|
||||
value: text('value').notNull(),
|
||||
expiresAt: integer('expires_at', { mode: 'timestamp_ms' }).notNull(),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||
.notNull(),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
|
||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull()
|
||||
},
|
||||
(table) => [index('verification_identifier_idx').on(table.identifier)]
|
||||
);
|
||||
|
||||
export const userRelations = relations(user, ({ many }) => ({
|
||||
sessions: many(session),
|
||||
accounts: many(account)
|
||||
}));
|
||||
|
||||
export const sessionRelations = relations(session, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [session.userId],
|
||||
references: [user.id]
|
||||
})
|
||||
}));
|
||||
|
||||
export const accountRelations = relations(account, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [account.userId],
|
||||
references: [user.id]
|
||||
})
|
||||
}));
|
||||
@@ -0,0 +1,31 @@
|
||||
import { index, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
import { clients } from './clients.schema';
|
||||
import { rooms } from './rooms.schema';
|
||||
import { services } from './services.schema';
|
||||
import { timestamps } from './shared.schema';
|
||||
|
||||
export const bookings = sqliteTable(
|
||||
'bookings',
|
||||
{
|
||||
id: text('id')
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
clientId: text('client_id')
|
||||
.notNull()
|
||||
.references(() => clients.id, { onDelete: 'restrict' }),
|
||||
roomId: text('room_id')
|
||||
.notNull()
|
||||
.references(() => rooms.id, { onDelete: 'restrict' }),
|
||||
serviceId: text('service_id').references(() => services.id, { onDelete: 'set null' }),
|
||||
startsAt: text('starts_at').notNull(),
|
||||
endsAt: text('ends_at').notNull(),
|
||||
status: text('status').notNull().default('booked'),
|
||||
notes: text('notes'),
|
||||
...timestamps
|
||||
},
|
||||
(table) => [
|
||||
index('bookings_client_id_idx').on(table.clientId),
|
||||
index('bookings_room_id_idx').on(table.roomId),
|
||||
index('bookings_service_id_idx').on(table.serviceId)
|
||||
]
|
||||
);
|
||||
@@ -0,0 +1,14 @@
|
||||
import { text, sqliteTable } from 'drizzle-orm/sqlite-core';
|
||||
import { timestamps } from './shared.schema';
|
||||
|
||||
export const clients = sqliteTable('clients', {
|
||||
id: text('id')
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
name: text('name').notNull(),
|
||||
type: text('type').notNull().default('business'),
|
||||
status: text('status').notNull().default('active'),
|
||||
website: text('website'),
|
||||
notes: text('notes'),
|
||||
...timestamps
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
import { clients } from './clients.schema';
|
||||
import { timestamps } from './shared.schema';
|
||||
|
||||
export const contacts = sqliteTable(
|
||||
'contacts',
|
||||
{
|
||||
id: text('id')
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
clientId: text('client_id')
|
||||
.notNull()
|
||||
.references(() => clients.id, { onDelete: 'restrict' }),
|
||||
name: text('name').notNull(),
|
||||
role: text('role'),
|
||||
email: text('email'),
|
||||
phone: text('phone'),
|
||||
isPrimary: integer('is_primary', { mode: 'boolean' }).notNull().default(false),
|
||||
notes: text('notes'),
|
||||
...timestamps
|
||||
},
|
||||
(table) => [index('contacts_client_id_idx').on(table.clientId)]
|
||||
);
|
||||
@@ -0,0 +1,23 @@
|
||||
import { index, real, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
import { clients } from './clients.schema';
|
||||
import { timestamps } from './shared.schema';
|
||||
|
||||
export const contracts = sqliteTable(
|
||||
'contracts',
|
||||
{
|
||||
id: text('id')
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
clientId: text('client_id')
|
||||
.notNull()
|
||||
.references(() => clients.id, { onDelete: 'restrict' }),
|
||||
title: text('title').notNull(),
|
||||
startDate: text('start_date').notNull(),
|
||||
endDate: text('end_date'),
|
||||
status: text('status').notNull().default('draft'),
|
||||
valueGbp: real('value_gbp').notNull().default(0),
|
||||
notes: text('notes'),
|
||||
...timestamps
|
||||
},
|
||||
(table) => [index('contracts_client_id_idx').on(table.clientId)]
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
import { drizzle } from 'drizzle-orm/libsql';
|
||||
import { createClient } from '@libsql/client';
|
||||
import * as schema from './schema';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
if (!env.DATABASE_URL) throw new Error('DATABASE_URL is not set');
|
||||
|
||||
const client = createClient({ url: env.DATABASE_URL });
|
||||
|
||||
export const db = drizzle(client, { schema });
|
||||
@@ -0,0 +1,25 @@
|
||||
import { index, real, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
import { clients } from './clients.schema';
|
||||
import { timestamps } from './shared.schema';
|
||||
|
||||
export const invoices = sqliteTable(
|
||||
'invoices',
|
||||
{
|
||||
id: text('id')
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
clientId: text('client_id')
|
||||
.notNull()
|
||||
.references(() => clients.id, { onDelete: 'restrict' }),
|
||||
invoiceNumber: text('invoice_number').notNull().unique(),
|
||||
issueDate: text('issue_date').notNull(),
|
||||
dueDate: text('due_date').notNull(),
|
||||
status: text('status').notNull().default('draft'),
|
||||
subtotalGbp: real('subtotal_gbp').notNull().default(0),
|
||||
taxGbp: real('tax_gbp').notNull().default(0),
|
||||
totalGbp: real('total_gbp').notNull().default(0),
|
||||
notes: text('notes'),
|
||||
...timestamps
|
||||
},
|
||||
(table) => [index('invoices_client_id_idx').on(table.clientId)]
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
import { integer, real, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
import { timestamps } from './shared.schema';
|
||||
|
||||
export const rooms = sqliteTable('rooms', {
|
||||
id: text('id')
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
name: text('name').notNull(),
|
||||
type: text('type').notNull().default('meeting_room'),
|
||||
sqFt: integer('sq_ft'),
|
||||
internalPricePerHourGbp: real('internal_price_per_hour_gbp'),
|
||||
externalPricePerHourGbp: real('external_price_per_hour_gbp'),
|
||||
internalPricePerDayGbp: real('internal_price_per_day_gbp'),
|
||||
externalPricePerDayGbp: real('external_price_per_day_gbp'),
|
||||
maxAttendees: integer('max_attendees'),
|
||||
workstations: integer('workstations'),
|
||||
pricePerMonthGbp: real('price_per_month_gbp'),
|
||||
...timestamps
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
export const task = sqliteTable('task', {
|
||||
id: text('id')
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
title: text('title').notNull(),
|
||||
priority: integer('priority').notNull().default(1)
|
||||
});
|
||||
|
||||
export * from './clients.schema';
|
||||
export * from './addresses.schema';
|
||||
export * from './contacts.schema';
|
||||
export * from './rooms.schema';
|
||||
export * from './services.schema';
|
||||
export * from './bookings.schema';
|
||||
export * from './invoices.schema';
|
||||
export * from './contracts.schema';
|
||||
export * from './auth.schema';
|
||||
@@ -0,0 +1,14 @@
|
||||
import { real, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
import { timestamps } from './shared.schema';
|
||||
|
||||
export const services = sqliteTable('services', {
|
||||
id: text('id')
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
name: text('name').notNull(),
|
||||
category: text('category'),
|
||||
unit: text('unit').notNull().default('each'),
|
||||
priceGbp: real('price_gbp').notNull().default(0),
|
||||
description: text('description'),
|
||||
...timestamps
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { integer } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
export const timestamps = {
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||
.notNull(),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
|
||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||
.$onUpdate(() => new Date())
|
||||
.notNull(),
|
||||
archivedAt: integer('archived_at', { mode: 'timestamp_ms' })
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
import './layout.css';
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
import { Toaster } from '$lib/components/ui/sonner/index.js';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
|
||||
{@render children()}
|
||||
<Toaster position="top-center" richColors />
|
||||
@@ -0,0 +1,6 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = () => {
|
||||
redirect(303, '/dashboard');
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
<h1>Welcome to SvelteKit</h1>
|
||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
|
||||
@@ -0,0 +1,64 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { APIError } from 'better-auth/api';
|
||||
import { message, superValidate } from 'sveltekit-superforms/server';
|
||||
import { zod4 } from 'sveltekit-superforms/adapters';
|
||||
import { auth } from '$lib/server/auth';
|
||||
import { loginSchema } from '$lib/schemas/auth';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
function getSafeRedirect(url: URL) {
|
||||
const redirectTo = url.searchParams.get('redirectTo');
|
||||
|
||||
if (!redirectTo || !redirectTo.startsWith('/') || redirectTo.startsWith('//')) {
|
||||
return '/dashboard';
|
||||
}
|
||||
|
||||
return redirectTo;
|
||||
}
|
||||
|
||||
export const load: PageServerLoad = async ({ locals, url }) => {
|
||||
if (locals.user) {
|
||||
redirect(303, getSafeRedirect(url));
|
||||
}
|
||||
|
||||
return {
|
||||
form: await superValidate(
|
||||
{
|
||||
email: url.searchParams.get('email') ?? '',
|
||||
password: ''
|
||||
},
|
||||
zod4(loginSchema),
|
||||
{ id: 'login' }
|
||||
)
|
||||
};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async (event) => {
|
||||
const form = await superValidate(event, zod4(loginSchema), { id: 'login' });
|
||||
|
||||
if (!form.valid) {
|
||||
return message(form, 'Check the highlighted fields.', { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
await auth.api.signInEmail({
|
||||
body: {
|
||||
email: form.data.email,
|
||||
password: form.data.password,
|
||||
callbackURL: getSafeRedirect(event.url)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof APIError) {
|
||||
return message(form, error.message || 'The email or password you entered is incorrect.', {
|
||||
status: 400
|
||||
});
|
||||
}
|
||||
|
||||
return message(form, 'Something went wrong while signing you in.', { status: 500 });
|
||||
}
|
||||
|
||||
redirect(303, getSafeRedirect(event.url));
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import LoginForm from '$lib/components/login-form.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex min-h-svh flex-col items-center justify-center gap-6 bg-background p-6 md:p-10">
|
||||
<div class="w-full max-w-sm">
|
||||
<LoginForm form={data.form} />
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user