feat: add auth and database foundation
This commit is contained in:
@@ -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' })
|
||||
};
|
||||
Reference in New Issue
Block a user