feat: integrate Drizzle ORM and SQLite for authentication
This commit is contained in:
12
drizzle.config.ts
Normal file
12
drizzle.config.ts
Normal 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,
|
||||||
|
});
|
@ -4,5 +4,8 @@ export default defineNuxtConfig({
|
|||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
modules: [
|
modules: [
|
||||||
'@nuxtjs/tailwindcss'
|
'@nuxtjs/tailwindcss'
|
||||||
]
|
],
|
||||||
|
runtimeConfig: {
|
||||||
|
adminPassword: '', // NUXT_ADMIN_PASSWORD
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
@ -10,11 +10,16 @@
|
|||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@libsql/client": "^0.15.9",
|
||||||
|
"bcryptjs": "^3.0.2",
|
||||||
|
"drizzle-orm": "^0.44.2",
|
||||||
"nuxt": "^3.17.5",
|
"nuxt": "^3.17.5",
|
||||||
"vue": "^3.5.16",
|
"vue": "^3.5.16",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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
611
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,21 +1,23 @@
|
|||||||
import { defineEventHandler, readBody, setResponseStatus } from 'h3';
|
import { defineEventHandler, readBody, setResponseStatus, useRuntimeConfig } from 'h3';
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
const { password } = body;
|
const { password } = body;
|
||||||
|
const config = useRuntimeConfig(event);
|
||||||
|
|
||||||
if (!password) {
|
if (!password) {
|
||||||
setResponseStatus(event, 400);
|
setResponseStatus(event, 400);
|
||||||
return { message: '请填写密码' };
|
return { message: '请填写密码' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Replace with a more secure admin authentication method
|
const adminPassword = config.adminPassword;
|
||||||
if (password === 'adminpassword') {
|
|
||||||
return {
|
if (!adminPassword || password !== adminPassword) {
|
||||||
message: '管理员登录成功!',
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
setResponseStatus(event, 401);
|
setResponseStatus(event, 401);
|
||||||
return { message: '密码错误' };
|
return { message: '密码错误' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: '管理员登录成功!',
|
||||||
|
};
|
||||||
});
|
});
|
@ -1,4 +1,7 @@
|
|||||||
import { defineEventHandler, readBody, setResponseStatus } from 'h3';
|
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) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
@ -9,14 +12,31 @@ export default defineEventHandler(async (event) => {
|
|||||||
return { message: '请填写手机号和密码' };
|
return { message: '请填写手机号和密码' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Replace with database user lookup and password verification
|
try {
|
||||||
if (contact === '1234567890' && password === 'password') {
|
const user = await db.query.customers.findFirst({
|
||||||
return {
|
where: eq(customers.contact, contact),
|
||||||
message: '登录成功!',
|
});
|
||||||
customerId: 'dummy-customer-id-123',
|
|
||||||
};
|
if (!user) {
|
||||||
} else {
|
|
||||||
setResponseStatus(event, 401);
|
setResponseStatus(event, 401);
|
||||||
return { message: '手机号或密码错误' };
|
return { message: '手机号或密码错误' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isPasswordValid = bcrypt.compareSync(password, user.password);
|
||||||
|
|
||||||
|
if (!isPasswordValid) {
|
||||||
|
setResponseStatus(event, 401);
|
||||||
|
return { message: '手机号或密码错误' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: '登录成功!',
|
||||||
|
customerId: user.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
setResponseStatus(event, 500);
|
||||||
|
return { message: '登录失败,请稍后重试' };
|
||||||
|
}
|
||||||
});
|
});
|
@ -1,21 +1,43 @@
|
|||||||
import { defineEventHandler, readBody, setResponseStatus } from 'h3';
|
import { defineEventHandler, readBody, setResponseStatus } from 'h3';
|
||||||
|
import { db, customers } from '~/server/db';
|
||||||
|
import bcrypt from 'bcryptjs';
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const body = await readBody(event);
|
const body = await readBody(event);
|
||||||
|
|
||||||
const { name, gender, contact, idCard, password } = body;
|
const { name, gender, contact, idCard, password } = body;
|
||||||
|
|
||||||
if (!name || !gender || !contact || !idCard || !password) {
|
if (!name || !gender || !contact || !idCard || !password) {
|
||||||
setResponseStatus(event, 400);
|
setResponseStatus(event, 400);
|
||||||
return {
|
return { message: '请填写完整信息' };
|
||||||
message: '请填写完整信息',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add database logic to save the user
|
try {
|
||||||
console.log('Registering new user:', { name, gender, contact, idCard });
|
const hashedPassword = bcrypt.hashSync(password, 10);
|
||||||
|
|
||||||
return {
|
await db.insert(customers).values({
|
||||||
message: '注册成功!',
|
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
10
server/db/index.ts
Normal 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
BIN
server/db/local.db
Normal file
Binary file not shown.
35
server/db/migrations/0000_gifted_agent_brand.sql
Normal file
35
server/db/migrations/0000_gifted_agent_brand.sql
Normal 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
|
||||||
|
);
|
248
server/db/migrations/meta/0000_snapshot.json
Normal file
248
server/db/migrations/meta/0000_snapshot.json
Normal 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": {}
|
||||||
|
}
|
||||||
|
}
|
13
server/db/migrations/meta/_journal.json
Normal file
13
server/db/migrations/meta/_journal.json
Normal 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
32
server/db/schema.ts
Normal 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(),
|
||||||
|
});
|
Reference in New Issue
Block a user