feat: integrate Drizzle ORM and SQLite for authentication

This commit is contained in:
2025-06-18 16:15:49 +08:00
parent 6652953b1a
commit bc81e4d6fe
13 changed files with 1024 additions and 37 deletions

12
drizzle.config.ts Normal file
View File

@ -0,0 +1,12 @@
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './server/db/schema.ts',
out: './server/db/migrations',
dialect: 'sqlite',
dbCredentials: {
url: 'file:./server/db/local.db',
},
verbose: true,
strict: true,
});

View File

@ -4,5 +4,8 @@ export default defineNuxtConfig({
devtools: { enabled: true },
modules: [
'@nuxtjs/tailwindcss'
]
],
runtimeConfig: {
adminPassword: '', // NUXT_ADMIN_PASSWORD
}
})

View File

@ -10,11 +10,16 @@
"postinstall": "nuxt prepare"
},
"dependencies": {
"@libsql/client": "^0.15.9",
"bcryptjs": "^3.0.2",
"drizzle-orm": "^0.44.2",
"nuxt": "^3.17.5",
"vue": "^3.5.16",
"vue-router": "^4.5.1"
},
"devDependencies": {
"@nuxtjs/tailwindcss": "7.0.0-beta.0"
"@nuxtjs/tailwindcss": "7.0.0-beta.0",
"@types/bcryptjs": "^3.0.0",
"drizzle-kit": "^0.31.1"
}
}

611
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,23 @@
import { defineEventHandler, readBody, setResponseStatus } from 'h3';
import { defineEventHandler, readBody, setResponseStatus, useRuntimeConfig } from 'h3';
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const { password } = body;
const config = useRuntimeConfig(event);
if (!password) {
setResponseStatus(event, 400);
return { message: '请填写密码' };
}
// TODO: Replace with a more secure admin authentication method
if (password === 'adminpassword') {
return {
message: '管理员登录成功!',
};
} else {
const adminPassword = config.adminPassword;
if (!adminPassword || password !== adminPassword) {
setResponseStatus(event, 401);
return { message: '密码错误' };
}
return {
message: '管理员登录成功!',
};
});

View File

@ -1,4 +1,7 @@
import { defineEventHandler, readBody, setResponseStatus } from 'h3';
import { db, customers } from '~/server/db';
import { eq } from 'drizzle-orm';
import bcrypt from 'bcryptjs';
export default defineEventHandler(async (event) => {
const body = await readBody(event);
@ -9,14 +12,31 @@ export default defineEventHandler(async (event) => {
return { message: '请填写手机号和密码' };
}
// TODO: Replace with database user lookup and password verification
if (contact === '1234567890' && password === 'password') {
try {
const user = await db.query.customers.findFirst({
where: eq(customers.contact, contact),
});
if (!user) {
setResponseStatus(event, 401);
return { message: '手机号或密码错误' };
}
const isPasswordValid = bcrypt.compareSync(password, user.password);
if (!isPasswordValid) {
setResponseStatus(event, 401);
return { message: '手机号或密码错误' };
}
return {
message: '登录成功!',
customerId: 'dummy-customer-id-123',
customerId: user.id,
};
} else {
setResponseStatus(event, 401);
return { message: '手机号或密码错误' };
} catch (error) {
console.error('Login error:', error);
setResponseStatus(event, 500);
return { message: '登录失败,请稍后重试' };
}
});

View File

@ -1,21 +1,43 @@
import { defineEventHandler, readBody, setResponseStatus } from 'h3';
import { db, customers } from '~/server/db';
import bcrypt from 'bcryptjs';
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const { name, gender, contact, idCard, password } = body;
if (!name || !gender || !contact || !idCard || !password) {
setResponseStatus(event, 400);
return {
message: '请填写完整信息',
};
return { message: '请填写完整信息' };
}
// TODO: Add database logic to save the user
console.log('Registering new user:', { name, gender, contact, idCard });
try {
const hashedPassword = bcrypt.hashSync(password, 10);
return {
message: '注册成功!',
};
await db.insert(customers).values({
name,
gender,
contact,
idCard,
password: hashedPassword,
});
return { message: '注册成功!' };
} catch (error: any) {
// Check for unique constraint violation
if (error.message?.includes('UNIQUE constraint failed')) {
setResponseStatus(event, 409); // Conflict
if (error.message.includes('customers.contact')) {
return { message: '该手机号已被注册' };
}
if (error.message.includes('customers.id_card')) {
return { message: '该身份证号已被注册' };
}
}
console.error('Registration error:', error);
setResponseStatus(event, 500);
return { message: '注册失败,请稍后重试' };
}
});

10
server/db/index.ts Normal file
View File

@ -0,0 +1,10 @@
import { drizzle } from 'drizzle-orm/libsql';
import { createClient } from '@libsql/client';
import * as schema from './schema';
const client = createClient({
url: 'file:./server/db/local.db',
});
export const db = drizzle(client, { schema });
export * from './schema';

BIN
server/db/local.db Normal file

Binary file not shown.

View File

@ -0,0 +1,35 @@
CREATE TABLE `customers` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL,
`gender` text NOT NULL,
`contact` text NOT NULL,
`id_card` text NOT NULL,
`password` text NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `customers_contact_unique` ON `customers` (`contact`);--> statement-breakpoint
CREATE UNIQUE INDEX `customers_id_card_unique` ON `customers` (`id_card`);--> statement-breakpoint
CREATE TABLE `reservations` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`customer_id` integer NOT NULL,
`room_id` integer NOT NULL,
`check_in_time` integer NOT NULL,
`stay_days` integer NOT NULL,
FOREIGN KEY (`customer_id`) REFERENCES `customers`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`room_id`) REFERENCES `rooms`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `room_types` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`type_name` text NOT NULL,
`star_rating` integer NOT NULL
);
--> statement-breakpoint
CREATE TABLE `rooms` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`type_id` integer NOT NULL,
`price` real NOT NULL,
`feature` text,
`available_count` integer DEFAULT 0 NOT NULL,
FOREIGN KEY (`type_id`) REFERENCES `room_types`(`id`) ON UPDATE no action ON DELETE no action
);

View File

@ -0,0 +1,248 @@
{
"version": "6",
"dialect": "sqlite",
"id": "4e2b0752-859c-462f-81a8-d794f837ca5b",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"customers": {
"name": "customers",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"gender": {
"name": "gender",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"contact": {
"name": "contact",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"id_card": {
"name": "id_card",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"customers_contact_unique": {
"name": "customers_contact_unique",
"columns": [
"contact"
],
"isUnique": true
},
"customers_id_card_unique": {
"name": "customers_id_card_unique",
"columns": [
"id_card"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"reservations": {
"name": "reservations",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"customer_id": {
"name": "customer_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"room_id": {
"name": "room_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"check_in_time": {
"name": "check_in_time",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"stay_days": {
"name": "stay_days",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"reservations_customer_id_customers_id_fk": {
"name": "reservations_customer_id_customers_id_fk",
"tableFrom": "reservations",
"tableTo": "customers",
"columnsFrom": [
"customer_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"reservations_room_id_rooms_id_fk": {
"name": "reservations_room_id_rooms_id_fk",
"tableFrom": "reservations",
"tableTo": "rooms",
"columnsFrom": [
"room_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"room_types": {
"name": "room_types",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"type_name": {
"name": "type_name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"star_rating": {
"name": "star_rating",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"rooms": {
"name": "rooms",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"type_id": {
"name": "type_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"price": {
"name": "price",
"type": "real",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"feature": {
"name": "feature",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"available_count": {
"name": "available_count",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {},
"foreignKeys": {
"rooms_type_id_room_types_id_fk": {
"name": "rooms_type_id_room_types_id_fk",
"tableFrom": "rooms",
"tableTo": "room_types",
"columnsFrom": [
"type_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1750234000729,
"tag": "0000_gifted_agent_brand",
"breakpoints": true
}
]
}

32
server/db/schema.ts Normal file
View File

@ -0,0 +1,32 @@
import { sqliteTable, text, integer, real } from 'drizzle-orm/sqlite-core';
export const customers = sqliteTable('customers', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
gender: text('gender', { enum: ['male', 'female'] }).notNull(),
contact: text('contact').notNull().unique(),
idCard: text('id_card').notNull().unique(),
password: text('password').notNull(),
});
export const roomTypes = sqliteTable('room_types', {
id: integer('id').primaryKey({ autoIncrement: true }),
typeName: text('type_name').notNull(),
starRating: integer('star_rating').notNull(),
});
export const rooms = sqliteTable('rooms', {
id: integer('id').primaryKey({ autoIncrement: true }),
typeId: integer('type_id').notNull().references(() => roomTypes.id),
price: real('price').notNull(),
feature: text('feature'),
availableCount: integer('available_count').notNull().default(0),
});
export const reservations = sqliteTable('reservations', {
id: integer('id').primaryKey({ autoIncrement: true }),
customerId: integer('customer_id').notNull().references(() => customers.id),
roomId: integer('room_id').notNull().references(() => rooms.id),
checkInTime: integer('check_in_time', { mode: 'timestamp' }).notNull(),
stayDays: integer('stay_days').notNull(),
});