feat: 重构用户角色系统为管理员标识
- 将用户角色字段从role改为is_admin布尔值 - 更新相关API权限检查逻辑 - 修改数据库schema和迁移文件 - 调整前端用户显示逻辑 - 添加API响应工具函数 - 优化权限检查中间件 - 重构英雄组件为原子组件
This commit is contained in:
@@ -0,0 +1,17 @@
|
|||||||
|
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||||
|
CREATE TABLE `__new_users` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`email` text NOT NULL,
|
||||||
|
`password_hash` text,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`is_admin` integer DEFAULT false NOT NULL,
|
||||||
|
`avatar` text,
|
||||||
|
`created_at` integer NOT NULL,
|
||||||
|
`updated_at` integer NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO `__new_users`("id", "email", "password_hash", "name", "is_admin", "avatar", "created_at", "updated_at") SELECT "id", "email", "password_hash", "name", "is_admin", "avatar", "created_at", "updated_at" FROM `users`;--> statement-breakpoint
|
||||||
|
DROP TABLE `users`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `__new_users` RENAME TO `users`;--> statement-breakpoint
|
||||||
|
PRAGMA foreign_keys=ON;--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);
|
||||||
@@ -0,0 +1,502 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "923c66d2-b19b-4d84-b88c-dc75d07fefd6",
|
||||||
|
"prevId": "98ef90e0-460c-4b25-9197-bf2f4900d3f9",
|
||||||
|
"tables": {
|
||||||
|
"audit_logs": {
|
||||||
|
"name": "audit_logs",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"name": "action",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"resource_type": {
|
||||||
|
"name": "resource_type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"resource_id": {
|
||||||
|
"name": "resource_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"details": {
|
||||||
|
"name": "details",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"ip_address": {
|
||||||
|
"name": "ip_address",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"user_agent": {
|
||||||
|
"name": "user_agent",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"audit_logs_user_id_users_id_fk": {
|
||||||
|
"name": "audit_logs_user_id_users_id_fk",
|
||||||
|
"tableFrom": "audit_logs",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"name": "slug",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"excerpt": {
|
||||||
|
"name": "excerpt",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"cover_image": {
|
||||||
|
"name": "cover_image",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"category": {
|
||||||
|
"name": "category",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"name": "tags",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'draft'"
|
||||||
|
},
|
||||||
|
"published_at": {
|
||||||
|
"name": "published_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"author_id": {
|
||||||
|
"name": "author_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"sort_order": {
|
||||||
|
"name": "sort_order",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"name": "metadata",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"content_slug_unique": {
|
||||||
|
"name": "content_slug_unique",
|
||||||
|
"columns": [
|
||||||
|
"slug"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"content_author_id_users_id_fk": {
|
||||||
|
"name": "content_author_id_users_id_fk",
|
||||||
|
"tableFrom": "content",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"author_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"content_versions": {
|
||||||
|
"name": "content_versions",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"content_id": {
|
||||||
|
"name": "content_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"name": "version",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"changes": {
|
||||||
|
"name": "changes",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"changed_by": {
|
||||||
|
"name": "changed_by",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"changed_at": {
|
||||||
|
"name": "changed_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"content_versions_content_id_content_id_fk": {
|
||||||
|
"name": "content_versions_content_id_content_id_fk",
|
||||||
|
"tableFrom": "content_versions",
|
||||||
|
"tableTo": "content",
|
||||||
|
"columnsFrom": [
|
||||||
|
"content_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"content_versions_changed_by_users_id_fk": {
|
||||||
|
"name": "content_versions_changed_by_users_id_fk",
|
||||||
|
"tableFrom": "content_versions",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"changed_by"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"site_config": {
|
||||||
|
"name": "site_config",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"name": "key",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"category": {
|
||||||
|
"name": "category",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_by": {
|
||||||
|
"name": "updated_by",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"site_config_key_unique": {
|
||||||
|
"name": "site_config_key_unique",
|
||||||
|
"columns": [
|
||||||
|
"key"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"site_config_updated_by_users_id_fk": {
|
||||||
|
"name": "site_config_updated_by_users_id_fk",
|
||||||
|
"tableFrom": "site_config",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"updated_by"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"name": "users",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"password_hash": {
|
||||||
|
"name": "password_hash",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"is_admin": {
|
||||||
|
"name": "is_admin",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"avatar": {
|
||||||
|
"name": "avatar",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"users_email_unique": {
|
||||||
|
"name": "users_email_unique",
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {
|
||||||
|
"\"users\".\"role\"": "\"users\".\"is_admin\""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,13 @@
|
|||||||
"when": 1772974841798,
|
"when": 1772974841798,
|
||||||
"tag": "0000_white_justice",
|
"tag": "0000_white_justice",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1773202935722,
|
||||||
|
"tag": "0001_clammy_toro",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import '@testing-library/jest-dom';
|
||||||
|
|
||||||
jest.mock('next-auth', () => {
|
jest.mock('next-auth', () => {
|
||||||
return {
|
return {
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
|
|||||||
@@ -129,8 +129,7 @@ export default function AdminLayout({
|
|||||||
{session?.user?.name}
|
{session?.user?.name}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 truncate">
|
<p className="text-xs text-gray-500 truncate">
|
||||||
{session?.user?.role === 'admin' ? '管理员' :
|
{session?.user?.isAdmin ? '管理员' : '用户'}
|
||||||
session?.user?.role === 'editor' ? '编辑' : '查看者'}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,23 +1,17 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest } from 'next/server';
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { siteConfig } from '@/db/schema';
|
import { siteConfig } from '@/db/schema';
|
||||||
import { auth } from '@/lib/auth';
|
import { checkIsAdmin, getAdminUserId } from '@/lib/auth/check-permission';
|
||||||
import { hasPermission } from '@/lib/auth/permissions';
|
import { forbidden, success, notFound, validationError, badRequest, handleApiError } from '@/lib/api-response';
|
||||||
import { eq, and } from 'drizzle-orm';
|
import { eq, and } from 'drizzle-orm';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const session = await auth();
|
const { isAdmin } = await checkIsAdmin();
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!isAdmin) {
|
||||||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
return forbidden();
|
||||||
}
|
|
||||||
|
|
||||||
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
|
|
||||||
|
|
||||||
if (!hasPermission(userRole, 'config', 'read')) {
|
|
||||||
return NextResponse.json({ error: '无权限' }, { status: 403 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
@@ -32,10 +26,10 @@ export async function GET(request: NextRequest) {
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (config.length === 0) {
|
if (config.length === 0) {
|
||||||
return NextResponse.json({ error: '配置不存在' }, { status: 404 });
|
return notFound('配置不存在');
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json(config[0]);
|
return success(config[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const conditions = [];
|
const conditions = [];
|
||||||
@@ -61,35 +55,29 @@ export async function GET(request: NextRequest) {
|
|||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, typeof configs>);
|
}, {} as Record<string, typeof configs>);
|
||||||
|
|
||||||
return NextResponse.json({
|
return success({
|
||||||
configs: groupedConfigs,
|
configs: groupedConfigs,
|
||||||
flat: configs,
|
flat: configs,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取配置失败:', error);
|
return handleApiError(error);
|
||||||
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const session = await auth();
|
const { isAdmin } = await checkIsAdmin();
|
||||||
|
const userId = await getAdminUserId();
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!isAdmin || !userId) {
|
||||||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
return forbidden();
|
||||||
}
|
|
||||||
|
|
||||||
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
|
|
||||||
|
|
||||||
if (!hasPermission(userRole, 'config', 'update')) {
|
|
||||||
return NextResponse.json({ error: '无权限' }, { status: 403 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { key, value, category, description } = body;
|
const { key, value, category, description } = body;
|
||||||
|
|
||||||
if (!key || !value || !category) {
|
if (!key || !value || !category) {
|
||||||
return NextResponse.json({ error: '缺少必要字段' }, { status: 400 });
|
return validationError('缺少必要字段', { required: ['key', 'value', 'category'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
const existing = await db
|
const existing = await db
|
||||||
@@ -107,12 +95,12 @@ export async function POST(request: NextRequest) {
|
|||||||
value,
|
value,
|
||||||
description: description || existing[0]!.description,
|
description: description || existing[0]!.description,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
updatedBy: session.user.id,
|
updatedBy: userId,
|
||||||
})
|
})
|
||||||
.where(eq(siteConfig.key, key))
|
.where(eq(siteConfig.key, key))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
return NextResponse.json(updated[0]);
|
return success(updated[0], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newConfig = await db
|
const newConfig = await db
|
||||||
@@ -124,36 +112,30 @@ export async function POST(request: NextRequest) {
|
|||||||
category,
|
category,
|
||||||
description: description || null,
|
description: description || null,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
updatedBy: session.user.id,
|
updatedBy: userId,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
return NextResponse.json(newConfig[0], { status: 201 });
|
return success(newConfig[0], 201);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('创建/更新配置失败:', error);
|
return handleApiError(error);
|
||||||
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function PUT(request: NextRequest) {
|
export async function PUT(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const session = await auth();
|
const { isAdmin } = await checkIsAdmin();
|
||||||
|
const userId = await getAdminUserId();
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!isAdmin || !userId) {
|
||||||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
return forbidden();
|
||||||
}
|
|
||||||
|
|
||||||
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
|
|
||||||
|
|
||||||
if (!hasPermission(userRole, 'config', 'update')) {
|
|
||||||
return NextResponse.json({ error: '无权限' }, { status: 403 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { configs } = body as { configs: Array<{ key: string; value: unknown; description?: string }> };
|
const { configs } = body as { configs: Array<{ key: string; value: unknown; description?: string }> };
|
||||||
|
|
||||||
if (!Array.isArray(configs)) {
|
if (!Array.isArray(configs)) {
|
||||||
return NextResponse.json({ error: '无效的数据格式' }, { status: 400 });
|
return badRequest('无效的数据格式');
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -173,7 +155,7 @@ export async function PUT(request: NextRequest) {
|
|||||||
value: config.value,
|
value: config.value,
|
||||||
description: config.description || existing[0]!.description,
|
description: config.description || existing[0]!.description,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
updatedBy: session.user.id,
|
updatedBy: userId,
|
||||||
})
|
})
|
||||||
.where(eq(siteConfig.key, config.key))
|
.where(eq(siteConfig.key, config.key))
|
||||||
.returning();
|
.returning();
|
||||||
@@ -188,16 +170,50 @@ export async function PUT(request: NextRequest) {
|
|||||||
category: 'general',
|
category: 'general',
|
||||||
description: config.description || null,
|
description: config.description || null,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
updatedBy: session.user.id,
|
updatedBy: userId,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
results.push(created[0]);
|
results.push(created[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ success: true, updated: results.length });
|
return success(results);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('批量更新配置失败:', error);
|
return handleApiError(error);
|
||||||
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { isAdmin } = await checkIsAdmin();
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
return forbidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const key = searchParams.get('key');
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
return badRequest('缺少 key 参数');
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await db
|
||||||
|
.select()
|
||||||
|
.from(siteConfig)
|
||||||
|
.where(eq(siteConfig.key, key))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (existing.length === 0) {
|
||||||
|
return notFound('配置不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.delete(siteConfig)
|
||||||
|
.where(eq(siteConfig.key, key));
|
||||||
|
|
||||||
|
return success({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
return handleApiError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest } from 'next/server';
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { content, contentVersions } from '@/db/schema';
|
import { content, contentVersions } from '@/db/schema';
|
||||||
import { auth } from '@/lib/auth';
|
import { checkIsAdmin, getAdminUserId } from '@/lib/auth/check-permission';
|
||||||
import { hasPermission } from '@/lib/auth/permissions';
|
|
||||||
import { createAuditLog } from '@/lib/audit';
|
import { createAuditLog } from '@/lib/audit';
|
||||||
|
import { forbidden, notFound, success, handleApiError } from '@/lib/api-response';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
@@ -12,16 +12,10 @@ export async function GET(
|
|||||||
{ params }: { params: Promise<{ id: string }> }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const session = await auth();
|
const { isAdmin } = await checkIsAdmin();
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!isAdmin) {
|
||||||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
return forbidden();
|
||||||
}
|
|
||||||
|
|
||||||
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
|
|
||||||
|
|
||||||
if (!hasPermission(userRole, 'content', 'read')) {
|
|
||||||
return NextResponse.json({ error: '无权限' }, { status: 403 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
@@ -33,7 +27,7 @@ export async function GET(
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (item.length === 0) {
|
if (item.length === 0) {
|
||||||
return NextResponse.json({ error: '内容不存在' }, { status: 404 });
|
return notFound('内容不存在');
|
||||||
}
|
}
|
||||||
|
|
||||||
const versions = await db
|
const versions = await db
|
||||||
@@ -42,13 +36,12 @@ export async function GET(
|
|||||||
.where(eq(contentVersions.contentId, id))
|
.where(eq(contentVersions.contentId, id))
|
||||||
.orderBy(contentVersions.version);
|
.orderBy(contentVersions.version);
|
||||||
|
|
||||||
return NextResponse.json({
|
return success({
|
||||||
...item[0],
|
...item[0],
|
||||||
versions,
|
versions,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取内容详情失败:', error);
|
return handleApiError(error);
|
||||||
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,16 +50,11 @@ export async function PUT(
|
|||||||
{ params }: { params: Promise<{ id: string }> }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const session = await auth();
|
const { isAdmin } = await checkIsAdmin();
|
||||||
|
const userId = await getAdminUserId();
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!isAdmin || !userId) {
|
||||||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
return forbidden();
|
||||||
}
|
|
||||||
|
|
||||||
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
|
|
||||||
|
|
||||||
if (!hasPermission(userRole, 'content', 'update')) {
|
|
||||||
return NextResponse.json({ error: '无权限' }, { status: 403 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
@@ -79,7 +67,7 @@ export async function PUT(
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (existingContent.length === 0) {
|
if (existingContent.length === 0) {
|
||||||
return NextResponse.json({ error: '内容不存在' }, { status: 404 });
|
return notFound('内容不存在');
|
||||||
}
|
}
|
||||||
|
|
||||||
const current = existingContent[0]!;
|
const current = existingContent[0]!;
|
||||||
@@ -107,7 +95,7 @@ export async function PUT(
|
|||||||
},
|
},
|
||||||
to: body,
|
to: body,
|
||||||
},
|
},
|
||||||
changedBy: session.user.id,
|
changedBy: userId,
|
||||||
changedAt: now,
|
changedAt: now,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -138,7 +126,7 @@ export async function PUT(
|
|||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
await createAuditLog({
|
await createAuditLog({
|
||||||
userId: session.user.id,
|
userId,
|
||||||
action: 'update',
|
action: 'update',
|
||||||
resourceType: 'content',
|
resourceType: 'content',
|
||||||
resourceId: id,
|
resourceId: id,
|
||||||
@@ -147,10 +135,9 @@ export async function PUT(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json(updated[0]);
|
return success(updated[0]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('更新内容失败:', error);
|
return handleApiError(error);
|
||||||
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,16 +146,11 @@ export async function DELETE(
|
|||||||
{ params }: { params: Promise<{ id: string }> }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const session = await auth();
|
const { isAdmin } = await checkIsAdmin();
|
||||||
|
const userId = await getAdminUserId();
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!isAdmin || !userId) {
|
||||||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
return forbidden();
|
||||||
}
|
|
||||||
|
|
||||||
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
|
|
||||||
|
|
||||||
if (!hasPermission(userRole, 'content', 'delete')) {
|
|
||||||
return NextResponse.json({ error: '无权限' }, { status: 403 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
@@ -180,14 +162,14 @@ export async function DELETE(
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (existingContent.length === 0) {
|
if (existingContent.length === 0) {
|
||||||
return NextResponse.json({ error: '内容不存在' }, { status: 404 });
|
return notFound('内容不存在');
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.delete(contentVersions).where(eq(contentVersions.contentId, id));
|
await db.delete(contentVersions).where(eq(contentVersions.contentId, id));
|
||||||
await db.delete(content).where(eq(content.id, id));
|
await db.delete(content).where(eq(content.id, id));
|
||||||
|
|
||||||
await createAuditLog({
|
await createAuditLog({
|
||||||
userId: session.user.id,
|
userId,
|
||||||
action: 'delete',
|
action: 'delete',
|
||||||
resourceType: 'content',
|
resourceType: 'content',
|
||||||
resourceId: id,
|
resourceId: id,
|
||||||
@@ -196,9 +178,8 @@ export async function DELETE(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json({ success: true });
|
return success({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('删除内容失败:', error);
|
return handleApiError(error);
|
||||||
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,18 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest } from 'next/server';
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { content } from '@/db/schema';
|
import { content } from '@/db/schema';
|
||||||
import { auth } from '@/lib/auth';
|
import { checkIsAdmin, getAdminUserId } from '@/lib/auth/check-permission';
|
||||||
import { hasPermission } from '@/lib/auth/permissions';
|
|
||||||
import { createAuditLog } from '@/lib/audit';
|
import { createAuditLog } from '@/lib/audit';
|
||||||
|
import { forbidden, badRequest, success, handleApiError, validationError } from '@/lib/api-response';
|
||||||
import { eq, desc, and, like, sql } from 'drizzle-orm';
|
import { eq, desc, and, like, sql } from 'drizzle-orm';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const session = await auth();
|
const { isAdmin } = await checkIsAdmin();
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!isAdmin) {
|
||||||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
return forbidden();
|
||||||
}
|
|
||||||
|
|
||||||
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
|
|
||||||
|
|
||||||
if (!hasPermission(userRole, 'content', 'read')) {
|
|
||||||
return NextResponse.json({ error: '无权限' }, { status: 403 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
@@ -61,7 +55,7 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
const total = countResult[0]?.count || 0;
|
const total = countResult[0]?.count || 0;
|
||||||
|
|
||||||
return NextResponse.json({
|
return success({
|
||||||
items,
|
items,
|
||||||
pagination: {
|
pagination: {
|
||||||
page,
|
page,
|
||||||
@@ -71,30 +65,24 @@ export async function GET(request: NextRequest) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取内容列表失败:', error);
|
return handleApiError(error);
|
||||||
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const session = await auth();
|
const { isAdmin } = await checkIsAdmin();
|
||||||
|
const userId = await getAdminUserId();
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!isAdmin || !userId) {
|
||||||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
return forbidden();
|
||||||
}
|
|
||||||
|
|
||||||
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
|
|
||||||
|
|
||||||
if (!hasPermission(userRole, 'content', 'create')) {
|
|
||||||
return NextResponse.json({ error: '无权限' }, { status: 403 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { type, title, slug, excerpt, contentBody, coverImage, category, tags, status: contentStatus, metadata } = body;
|
const { type, title, slug, excerpt, contentBody, coverImage, category, tags, status: contentStatus, metadata } = body;
|
||||||
|
|
||||||
if (!type || !title || !slug) {
|
if (!type || !title || !slug) {
|
||||||
return NextResponse.json({ error: '缺少必要字段' }, { status: 400 });
|
return validationError('缺少必要字段', { required: ['type', 'title', 'slug'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingContent = await db
|
const existingContent = await db
|
||||||
@@ -104,7 +92,7 @@ export async function POST(request: NextRequest) {
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (existingContent.length > 0) {
|
if (existingContent.length > 0) {
|
||||||
return NextResponse.json({ error: 'Slug 已存在' }, { status: 400 });
|
return badRequest('Slug 已存在');
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -122,7 +110,7 @@ export async function POST(request: NextRequest) {
|
|||||||
tags: tags || [],
|
tags: tags || [],
|
||||||
status: contentStatus || 'draft',
|
status: contentStatus || 'draft',
|
||||||
publishedAt: contentStatus === 'published' ? now : null,
|
publishedAt: contentStatus === 'published' ? now : null,
|
||||||
authorId: session.user.id,
|
authorId: userId,
|
||||||
metadata: metadata || null,
|
metadata: metadata || null,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
@@ -130,7 +118,7 @@ export async function POST(request: NextRequest) {
|
|||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
await createAuditLog({
|
await createAuditLog({
|
||||||
userId: session.user.id,
|
userId,
|
||||||
action: 'create',
|
action: 'create',
|
||||||
resourceType: 'content',
|
resourceType: 'content',
|
||||||
resourceId: newContent[0]!.id,
|
resourceId: newContent[0]!.id,
|
||||||
@@ -141,9 +129,8 @@ export async function POST(request: NextRequest) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json(newContent[0], { status: 201 });
|
return success(newContent[0], 201);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('创建内容失败:', error);
|
return handleApiError(error);
|
||||||
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,16 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest } from 'next/server';
|
||||||
import { auth } from '@/lib/auth';
|
import { checkIsAdmin, getAdminUserId } from '@/lib/auth/check-permission';
|
||||||
import { hasPermission } from '@/lib/auth/permissions';
|
|
||||||
import { createAuditLog } from '@/lib/audit';
|
import { createAuditLog } from '@/lib/audit';
|
||||||
import { uploadFile, deleteFile } from '@/lib/upload';
|
import { uploadFile, deleteFile } from '@/lib/upload';
|
||||||
|
import { forbidden, badRequest, notFound, success, handleApiError } from '@/lib/api-response';
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const session = await auth();
|
const { isAdmin } = await checkIsAdmin();
|
||||||
|
const userId = await getAdminUserId();
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!isAdmin || !userId) {
|
||||||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
return forbidden();
|
||||||
}
|
|
||||||
|
|
||||||
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
|
|
||||||
|
|
||||||
if (!hasPermission(userRole, 'content', 'create')) {
|
|
||||||
return NextResponse.json({ error: '无权限' }, { status: 403 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
@@ -23,16 +18,16 @@ export async function POST(request: NextRequest) {
|
|||||||
const type = (formData.get('type') as 'image' | 'document') || 'image';
|
const type = (formData.get('type') as 'image' | 'document') || 'image';
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return NextResponse.json({ error: '未找到文件' }, { status: 400 });
|
return badRequest('未找到文件');
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await uploadFile(file, {
|
const result = await uploadFile(file, {
|
||||||
type,
|
type,
|
||||||
userId: session.user.id,
|
userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
await createAuditLog({
|
await createAuditLog({
|
||||||
userId: session.user.id,
|
userId,
|
||||||
action: 'upload',
|
action: 'upload',
|
||||||
resourceType: 'file',
|
resourceType: 'file',
|
||||||
resourceId: result.id,
|
resourceId: result.id,
|
||||||
@@ -44,7 +39,7 @@ export async function POST(request: NextRequest) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json({
|
return success({
|
||||||
success: true,
|
success: true,
|
||||||
file: result,
|
file: result,
|
||||||
});
|
});
|
||||||
@@ -52,43 +47,38 @@ export async function POST(request: NextRequest) {
|
|||||||
console.error('文件上传失败:', error);
|
console.error('文件上传失败:', error);
|
||||||
|
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
return NextResponse.json({ error: error.message }, { status: 400 });
|
return badRequest(error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
|
return handleApiError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function DELETE(request: NextRequest) {
|
export async function DELETE(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const session = await auth();
|
const { isAdmin } = await checkIsAdmin();
|
||||||
|
const userId = await getAdminUserId();
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!isAdmin || !userId) {
|
||||||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
return forbidden();
|
||||||
}
|
|
||||||
|
|
||||||
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
|
|
||||||
|
|
||||||
if (!hasPermission(userRole, 'content', 'delete')) {
|
|
||||||
return NextResponse.json({ error: '无权限' }, { status: 403 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const fileUrl = searchParams.get('url');
|
const fileUrl = searchParams.get('url');
|
||||||
|
|
||||||
if (!fileUrl) {
|
if (!fileUrl) {
|
||||||
return NextResponse.json({ error: '缺少文件 URL' }, { status: 400 });
|
return badRequest('缺少文件 URL');
|
||||||
}
|
}
|
||||||
|
|
||||||
const success = await deleteFile(fileUrl);
|
const result = await deleteFile(fileUrl);
|
||||||
|
|
||||||
if (!success) {
|
if (!result) {
|
||||||
return NextResponse.json({ error: '文件不存在或删除失败' }, { status: 404 });
|
return notFound('文件不存在或删除失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ success: true });
|
return success({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('文件删除失败:', error);
|
console.error('文件删除失败:', error);
|
||||||
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
|
return handleApiError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest } from 'next/server';
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { users } from '@/db/schema';
|
import { users } from '@/db/schema';
|
||||||
import { auth } from '@/lib/auth';
|
import { checkIsAdmin } from '@/lib/auth/check-permission';
|
||||||
import { hasPermission } from '@/lib/auth/permissions';
|
import { forbidden, notFound, success, handleApiError } from '@/lib/api-response';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
|
|
||||||
@@ -11,26 +11,24 @@ export async function GET(
|
|||||||
{ params }: { params: Promise<{ id: string }> }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const session = await auth();
|
const { isAdmin, userId } = await checkIsAdmin();
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!isAdmin) {
|
||||||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
return forbidden();
|
||||||
}
|
|
||||||
|
|
||||||
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
|
|
||||||
|
|
||||||
if (!hasPermission(userRole, 'users', 'read')) {
|
|
||||||
return NextResponse.json({ error: '无权限' }, { status: 403 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
|
||||||
|
if (id !== userId) {
|
||||||
|
return forbidden('只能查看自己的信息');
|
||||||
|
}
|
||||||
|
|
||||||
const user = await db
|
const user = await db
|
||||||
.select({
|
.select({
|
||||||
id: users.id,
|
id: users.id,
|
||||||
email: users.email,
|
email: users.email,
|
||||||
name: users.name,
|
name: users.name,
|
||||||
role: users.role,
|
isAdmin: users.isAdmin,
|
||||||
createdAt: users.createdAt,
|
createdAt: users.createdAt,
|
||||||
updatedAt: users.updatedAt,
|
updatedAt: users.updatedAt,
|
||||||
})
|
})
|
||||||
@@ -39,13 +37,12 @@ export async function GET(
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (user.length === 0) {
|
if (user.length === 0) {
|
||||||
return NextResponse.json({ error: '用户不存在' }, { status: 404 });
|
return notFound('用户不存在');
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ user: user[0] });
|
return success({ user: user[0] });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取用户详情失败:', error);
|
return handleApiError(error);
|
||||||
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,21 +51,20 @@ export async function PUT(
|
|||||||
{ params }: { params: Promise<{ id: string }> }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const session = await auth();
|
const { isAdmin, userId } = await checkIsAdmin();
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!isAdmin) {
|
||||||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
return forbidden();
|
||||||
}
|
|
||||||
|
|
||||||
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
|
|
||||||
|
|
||||||
if (!hasPermission(userRole, 'users', 'update')) {
|
|
||||||
return NextResponse.json({ error: '无权限' }, { status: 403 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
|
||||||
|
if (id !== userId) {
|
||||||
|
return forbidden('只能修改自己的信息');
|
||||||
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { email, name, password, role } = body;
|
const { email, name, password } = body;
|
||||||
|
|
||||||
const existingUser = await db
|
const existingUser = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -77,16 +73,15 @@ export async function PUT(
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (existingUser.length === 0) {
|
if (existingUser.length === 0) {
|
||||||
return NextResponse.json({ error: '用户不存在' }, { status: 404 });
|
return notFound('用户不存在');
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateData: Record<string, any> = {
|
const updateData: Record<string, unknown> = {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (email) updateData.email = email;
|
if (email) updateData.email = email;
|
||||||
if (name) updateData.name = name;
|
if (name) updateData.name = name;
|
||||||
if (role) updateData.role = role;
|
|
||||||
if (password) {
|
if (password) {
|
||||||
updateData.passwordHash = await bcrypt.hash(password, 10);
|
updateData.passwordHash = await bcrypt.hash(password, 10);
|
||||||
}
|
}
|
||||||
@@ -97,18 +92,16 @@ export async function PUT(
|
|||||||
.where(eq(users.id, id))
|
.where(eq(users.id, id))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
return NextResponse.json({
|
return success({
|
||||||
user: {
|
user: {
|
||||||
id: updated[0]!.id,
|
id: updated[0]!.id,
|
||||||
email: updated[0]!.email,
|
email: updated[0]!.email,
|
||||||
name: updated[0]!.name,
|
name: updated[0]!.name,
|
||||||
role: updated[0]!.role,
|
isAdmin: updated[0]!.isAdmin,
|
||||||
createdAt: updated[0]!.createdAt,
|
},
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('更新用户失败:', error);
|
return handleApiError(error);
|
||||||
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,39 +110,24 @@ export async function DELETE(
|
|||||||
{ params }: { params: Promise<{ id: string }> }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const session = await auth();
|
const { isAdmin, userId } = await checkIsAdmin();
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!isAdmin) {
|
||||||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
return forbidden();
|
||||||
}
|
|
||||||
|
|
||||||
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
|
|
||||||
|
|
||||||
if (!hasPermission(userRole, 'users', 'delete')) {
|
|
||||||
return NextResponse.json({ error: '无权限' }, { status: 403 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
|
||||||
if (session.user.id === id) {
|
if (id !== userId) {
|
||||||
return NextResponse.json({ error: '不能删除自己的账号' }, { status: 400 });
|
return forbidden('不能删除其他用户');
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingUser = await db
|
await db
|
||||||
.select()
|
.delete(users)
|
||||||
.from(users)
|
.where(eq(users.id, id));
|
||||||
.where(eq(users.id, id))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (existingUser.length === 0) {
|
return success({ success: true });
|
||||||
return NextResponse.json({ error: '用户不存在' }, { status: 404 });
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.delete(users).where(eq(users.id, id));
|
|
||||||
|
|
||||||
return NextResponse.json({ success: true });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('删除用户失败:', error);
|
return handleApiError(error);
|
||||||
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,16 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest } from 'next/server';
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { users } from '@/db/schema';
|
import { users } from '@/db/schema';
|
||||||
import { auth } from '@/lib/auth';
|
import { checkIsAdmin } from '@/lib/auth/check-permission';
|
||||||
import { hasPermission } from '@/lib/auth/permissions';
|
import { forbidden, success, handleApiError } from '@/lib/api-response';
|
||||||
import { eq } from 'drizzle-orm';
|
import { desc } from 'drizzle-orm';
|
||||||
import { nanoid } from 'nanoid';
|
|
||||||
import bcrypt from 'bcryptjs';
|
|
||||||
|
|
||||||
export async function GET(_request: NextRequest) {
|
export async function GET(_request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const session = await auth();
|
const { isAdmin } = await checkIsAdmin();
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!isAdmin) {
|
||||||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
return forbidden();
|
||||||
}
|
|
||||||
|
|
||||||
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
|
|
||||||
|
|
||||||
if (!hasPermission(userRole, 'users', 'read')) {
|
|
||||||
return NextResponse.json({ error: '无权限' }, { status: 403 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const allUsers = await db
|
const allUsers = await db
|
||||||
@@ -26,77 +18,16 @@ export async function GET(_request: NextRequest) {
|
|||||||
id: users.id,
|
id: users.id,
|
||||||
email: users.email,
|
email: users.email,
|
||||||
name: users.name,
|
name: users.name,
|
||||||
role: users.role,
|
isAdmin: users.isAdmin,
|
||||||
createdAt: users.createdAt,
|
createdAt: users.createdAt,
|
||||||
updatedAt: users.updatedAt,
|
updatedAt: users.updatedAt,
|
||||||
})
|
})
|
||||||
.from(users)
|
.from(users)
|
||||||
.orderBy(users.createdAt);
|
.orderBy(desc(users.createdAt));
|
||||||
|
|
||||||
return NextResponse.json({ users: allUsers });
|
return success({ users: allUsers });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取用户列表失败:', error);
|
console.error('获取用户列表失败:', error);
|
||||||
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
|
return handleApiError(error);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const session = await auth();
|
|
||||||
|
|
||||||
if (!session?.user) {
|
|
||||||
return NextResponse.json({ error: '未授权' }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const userRole = session.user.role as 'admin' | 'editor' | 'viewer';
|
|
||||||
|
|
||||||
if (!hasPermission(userRole, 'users', 'create')) {
|
|
||||||
return NextResponse.json({ error: '无权限' }, { status: 403 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = await request.json();
|
|
||||||
const { email, name, password, role } = body;
|
|
||||||
|
|
||||||
if (!email || !name || !password || !role) {
|
|
||||||
return NextResponse.json({ error: '缺少必填字段' }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingUser = await db
|
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.email, email))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (existingUser.length > 0) {
|
|
||||||
return NextResponse.json({ error: '邮箱已被使用' }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const hashedPassword = await bcrypt.hash(password, 10);
|
|
||||||
|
|
||||||
const newUser = await db
|
|
||||||
.insert(users)
|
|
||||||
.values({
|
|
||||||
id: nanoid(),
|
|
||||||
email,
|
|
||||||
name,
|
|
||||||
passwordHash: hashedPassword,
|
|
||||||
role,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
user: {
|
|
||||||
id: newUser[0]!.id,
|
|
||||||
email: newUser[0]!.email,
|
|
||||||
name: newUser[0]!.name,
|
|
||||||
role: newUser[0]!.role,
|
|
||||||
createdAt: newUser[0]!.createdAt,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('创建用户失败:', error);
|
|
||||||
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
export { DataParticleFlow } from './data-particle-flow';
|
||||||
|
export { SubtleDots } from './subtle-dots';
|
||||||
|
export { SubtleParticles } from './subtle-particles';
|
||||||
|
export { ParticleGalaxy } from './particle-galaxy';
|
||||||
|
export { MouseInteractiveParticles } from './mouse-interactive-particles';
|
||||||
|
export { GradientFlow } from './gradient-flow';
|
||||||
|
export { GradientAnimation } from './gradient-animation';
|
||||||
|
export { GradientOrbs } from './gradient-orbs';
|
||||||
|
export { GradientGrid } from './gradient-grid';
|
||||||
|
export { TechGridFlow } from './tech-grid-flow';
|
||||||
|
export { MeshGradient } from './mesh-gradient';
|
||||||
|
export { InkTechFusion } from './ink-tech-fusion';
|
||||||
|
export { GridLines } from './grid-lines';
|
||||||
|
export { GlowEffect } from './glow-effect';
|
||||||
|
export { GeometricShapes } from './geometric-shapes';
|
||||||
|
export { GeometricAbstract } from './geometric-abstract';
|
||||||
|
export { FluidWaveBackground } from './fluid-wave-background';
|
||||||
|
export { AdvancedFloatingEffects } from './advanced-floating-effects';
|
||||||
|
export { ParallaxEffect } from './parallax-effect';
|
||||||
|
export { SealAnimationEnhanced } from './seal-animation-enhanced';
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { RippleButton, SealButton } from '@/components/ui/ripple-button';
|
||||||
|
import { MagneticButton, BlurReveal, CounterWithEffect } from '@/lib/animations';
|
||||||
|
import { COMPANY_INFO, STATS } from '@/lib/constants';
|
||||||
|
import { ArrowRight, Shield, Zap, Award } from 'lucide-react';
|
||||||
|
|
||||||
|
interface HeroContentProps {
|
||||||
|
isVisible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const features = [
|
||||||
|
{ icon: Shield, text: '安全可靠' },
|
||||||
|
{ icon: Zap, text: '高效便捷' },
|
||||||
|
{ icon: Award, text: '专业服务' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function scrollTo(id: string) {
|
||||||
|
const element = document.getElementById(id);
|
||||||
|
if (element) {
|
||||||
|
element.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyDown(event: React.KeyboardEvent<HTMLButtonElement>, id: string) {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
event.preventDefault();
|
||||||
|
scrollTo(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HeroContent({ isVisible }: HeroContentProps) {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||||
|
animate={isVisible ? { opacity: 1, y: 0, scale: 1 } : {}}
|
||||||
|
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
|
||||||
|
className="mb-8"
|
||||||
|
>
|
||||||
|
<span className="inline-flex items-center gap-2 px-5 py-2.5 rounded-full border border-[#1C1C1C]/20 bg-[#F5F5F5] text-[#1C1C1C] text-sm font-medium">
|
||||||
|
智连未来,成长伙伴
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HeroTitle({ isVisible }: HeroContentProps) {
|
||||||
|
return (
|
||||||
|
<motion.h1
|
||||||
|
id="hero-heading"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={isVisible ? { opacity: 1, y: 0 } : {}}
|
||||||
|
transition={{ duration: 0.6, delay: 0.1 }}
|
||||||
|
className="text-5xl sm:text-6xl lg:text-7xl tracking-tight mb-6 font-calligraphy"
|
||||||
|
style={{
|
||||||
|
fontWeight: 'normal',
|
||||||
|
WebkitFontSmoothing: 'antialiased',
|
||||||
|
MozOsxFontSmoothing: 'grayscale',
|
||||||
|
textRendering: 'optimizeLegibility'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{COMPANY_INFO.shortName}
|
||||||
|
</motion.h1>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HeroDescription(_props: HeroContentProps) {
|
||||||
|
return (
|
||||||
|
<div className="mb-10">
|
||||||
|
<BlurReveal delay={0.3}>
|
||||||
|
<p className="text-xl sm:text-2xl text-[#4A5568] mb-4">
|
||||||
|
<span className="font-semibold bg-gradient-to-r from-[#C41E3A] via-[#E04A68] to-[#C41E3A] bg-clip-text text-transparent">
|
||||||
|
企业数字化转型服务商
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</BlurReveal>
|
||||||
|
<BlurReveal delay={0.4}>
|
||||||
|
<p className="text-lg text-[#718096] max-w-2xl mx-auto leading-relaxed">
|
||||||
|
以智慧连接数字趋势,以伙伴身份陪您成长——您的数字化转型同行者
|
||||||
|
</p>
|
||||||
|
</BlurReveal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HeroButtons({ isVisible }: HeroContentProps) {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={isVisible ? { opacity: 1, y: 0 } : {}}
|
||||||
|
transition={{ duration: 0.6, delay: 0.3 }}
|
||||||
|
className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-8"
|
||||||
|
>
|
||||||
|
<MagneticButton strength={0.4}>
|
||||||
|
<Link href="/contact">
|
||||||
|
<SealButton size="lg" className="min-w-45">
|
||||||
|
立即咨询
|
||||||
|
<ArrowRight className="w-4 h-4 ml-2" />
|
||||||
|
</SealButton>
|
||||||
|
</Link>
|
||||||
|
</MagneticButton>
|
||||||
|
<MagneticButton strength={0.4}>
|
||||||
|
<RippleButton
|
||||||
|
size="lg"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => scrollTo('about')}
|
||||||
|
onKeyDown={(e) => handleKeyDown(e, 'about')}
|
||||||
|
className="min-w-45"
|
||||||
|
>
|
||||||
|
了解更多
|
||||||
|
</RippleButton>
|
||||||
|
</MagneticButton>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HeroFeatures({ isVisible }: HeroContentProps) {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={isVisible ? { opacity: 1, y: 0 } : {}}
|
||||||
|
transition={{ duration: 0.6, delay: 0.35 }}
|
||||||
|
className="flex flex-wrap gap-4 justify-center mb-16"
|
||||||
|
>
|
||||||
|
{features.map((feature, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
|
animate={isVisible ? { opacity: 1, scale: 1 } : {}}
|
||||||
|
transition={{ duration: 0.4, delay: 0.4 + index * 0.1 }}
|
||||||
|
whileHover={{ scale: 1.05, y: -2 }}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 rounded-full bg-[#FAFAFA] border border-[#E5E5E5] transition-all duration-300 hover:border-[#1C1C1C] hover:shadow-md cursor-default"
|
||||||
|
>
|
||||||
|
<feature.icon className="w-4 h-4 text-[#C41E3A]" />
|
||||||
|
<span className="text-sm text-[#3D3D3D]">{feature.text}</span>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HeroStats() {
|
||||||
|
const [statsVisible, setStatsVisible] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const statsEl = document.getElementById('stats-section');
|
||||||
|
if (!statsEl) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry?.isIntersecting) {
|
||||||
|
setStatsVisible(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.5 }
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe(statsEl);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
id="stats-section"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={statsVisible ? { opacity: 1, y: 0 } : {}}
|
||||||
|
transition={{ duration: 0.6, delay: 0.4 }}
|
||||||
|
className="pt-16 border-t border-[#E2E8F0]"
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 md:gap-12">
|
||||||
|
{STATS.map((stat, index) => (
|
||||||
|
<HeroStatItem
|
||||||
|
key={stat.label}
|
||||||
|
stat={stat}
|
||||||
|
index={index}
|
||||||
|
shouldAnimate={statsVisible}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function HeroStatItem({ stat, index, shouldAnimate }: {
|
||||||
|
stat: { value: string; label: string };
|
||||||
|
index: number;
|
||||||
|
shouldAnimate: boolean;
|
||||||
|
}) {
|
||||||
|
const numericValue = parseInt(stat.value.replace(/\D/g, ''));
|
||||||
|
const suffix = stat.value.replace(/[\d]/g, '');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="group cursor-default text-center"
|
||||||
|
initial={{ opacity: 0, y: 20, scale: 0.9 }}
|
||||||
|
animate={shouldAnimate ? { opacity: 1, y: 0, scale: 1 } : {}}
|
||||||
|
transition={{ duration: 0.5, delay: index * 0.1, type: 'spring', stiffness: 100 }}
|
||||||
|
whileHover={{ scale: 1.05, y: -5 }}
|
||||||
|
>
|
||||||
|
<div className="text-4xl sm:text-5xl font-bold text-[#C41E3A] mb-3">
|
||||||
|
{shouldAnimate ? (
|
||||||
|
<CounterWithEffect
|
||||||
|
end={numericValue}
|
||||||
|
suffix={suffix}
|
||||||
|
effect="bounce"
|
||||||
|
duration={2000}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="text-[#CBD5E0]">0{suffix}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-[#718096] group-hover:text-[#4A5568] transition-colors">
|
||||||
|
{stat.label}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,13 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import Link from 'next/link';
|
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import { motion } from 'framer-motion';
|
import { HeroContent, HeroTitle, HeroDescription, HeroButtons, HeroFeatures, HeroStats } from './hero-section-atoms';
|
||||||
import { RippleButton, SealButton } from '@/components/ui/ripple-button';
|
|
||||||
import { GradientText, MagneticButton, BlurReveal, CounterWithEffect } from '@/lib/animations';
|
|
||||||
import { COMPANY_INFO, STATS } from '@/lib/constants';
|
|
||||||
import { ArrowRight, Shield, Zap, Award } from 'lucide-react';
|
|
||||||
|
|
||||||
const InkBackground = dynamic(
|
const InkBackground = dynamic(
|
||||||
() => import('@/components/ui/ink-decoration').then(mod => ({ default: mod.InkBackground })),
|
() => import('@/components/ui/ink-decoration').then(mod => ({ default: mod.InkBackground })),
|
||||||
@@ -24,16 +19,9 @@ const SubtleDots = dynamic(
|
|||||||
{ ssr: false }
|
{ ssr: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
const features = [
|
|
||||||
{ icon: Shield, text: '安全可靠' },
|
|
||||||
{ icon: Zap, text: '高效便捷' },
|
|
||||||
{ icon: Award, text: '专业服务' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function HeroSection() {
|
export function HeroSection() {
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const sectionRef = useRef<HTMLElement>(null);
|
const sectionRef = useRef<HTMLElement>(null);
|
||||||
const [statsVisible, setStatsVisible] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
@@ -52,37 +40,6 @@ export function HeroSection() {
|
|||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const statsEl = document.getElementById('stats-section');
|
|
||||||
if (!statsEl) {return;}
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
([entry]) => {
|
|
||||||
if (entry?.isIntersecting) {
|
|
||||||
setStatsVisible(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ threshold: 0.5 }
|
|
||||||
);
|
|
||||||
|
|
||||||
observer.observe(statsEl);
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleScrollTo = (id: string) => {
|
|
||||||
const element = document.getElementById(id);
|
|
||||||
if (element) {
|
|
||||||
element.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>, id: string) => {
|
|
||||||
if (event.key === 'Enter' || event.key === ' ') {
|
|
||||||
event.preventDefault();
|
|
||||||
handleScrollTo(id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
id="home"
|
id="home"
|
||||||
@@ -102,153 +59,14 @@ export function HeroSection() {
|
|||||||
|
|
||||||
<div className="container-wide py-24 md:py-32 lg:py-40 relative z-10">
|
<div className="container-wide py-24 md:py-32 lg:py-40 relative z-10">
|
||||||
<div className="max-w-4xl mx-auto text-center">
|
<div className="max-w-4xl mx-auto text-center">
|
||||||
<motion.div
|
<HeroContent isVisible={isVisible} />
|
||||||
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
<HeroTitle isVisible={isVisible} />
|
||||||
animate={isVisible ? { opacity: 1, y: 0, scale: 1 } : {}}
|
<HeroDescription isVisible={isVisible} />
|
||||||
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
|
<HeroButtons isVisible={isVisible} />
|
||||||
className="mb-8"
|
<HeroFeatures isVisible={isVisible} />
|
||||||
>
|
<HeroStats />
|
||||||
<span className="inline-flex items-center gap-2 px-5 py-2.5 rounded-full border border-[#1C1C1C]/20 bg-[#F5F5F5] text-[#1C1C1C] text-sm font-medium">
|
|
||||||
智连未来,成长伙伴
|
|
||||||
</span>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.h1
|
|
||||||
id="hero-heading"
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={isVisible ? { opacity: 1, y: 0 } : {}}
|
|
||||||
transition={{ duration: 0.6, delay: 0.1 }}
|
|
||||||
className="text-5xl sm:text-6xl lg:text-7xl tracking-tight mb-6 font-calligraphy"
|
|
||||||
style={{
|
|
||||||
fontWeight: 'normal',
|
|
||||||
WebkitFontSmoothing: 'antialiased',
|
|
||||||
MozOsxFontSmoothing: 'grayscale',
|
|
||||||
textRendering: 'optimizeLegibility'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{COMPANY_INFO.shortName}
|
|
||||||
</motion.h1>
|
|
||||||
|
|
||||||
<BlurReveal delay={0.3}>
|
|
||||||
<p className="text-xl sm:text-2xl text-[#4A5568] mb-4">
|
|
||||||
<GradientText colors={['#C41E3A', '#E04A68', '#C41E3A']} duration={4}>
|
|
||||||
企业数字化转型服务商
|
|
||||||
</GradientText>
|
|
||||||
</p>
|
|
||||||
</BlurReveal>
|
|
||||||
|
|
||||||
<BlurReveal delay={0.4}>
|
|
||||||
<p className="text-lg text-[#718096] mb-10 max-w-2xl mx-auto leading-relaxed">
|
|
||||||
以智慧连接数字趋势,以伙伴身份陪您成长——您的数字化转型同行者
|
|
||||||
</p>
|
|
||||||
</BlurReveal>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={isVisible ? { opacity: 1, y: 0 } : {}}
|
|
||||||
transition={{ duration: 0.6, delay: 0.3 }}
|
|
||||||
className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-8"
|
|
||||||
>
|
|
||||||
<MagneticButton strength={0.4}>
|
|
||||||
<Link href="/contact">
|
|
||||||
<SealButton
|
|
||||||
size="lg"
|
|
||||||
className="min-w-45"
|
|
||||||
>
|
|
||||||
立即咨询
|
|
||||||
<ArrowRight className="w-4 h-4 ml-2" />
|
|
||||||
</SealButton>
|
|
||||||
</Link>
|
|
||||||
</MagneticButton>
|
|
||||||
<MagneticButton strength={0.4}>
|
|
||||||
<RippleButton
|
|
||||||
size="lg"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => handleScrollTo('about')}
|
|
||||||
onKeyDown={(e) => handleKeyDown(e, 'about')}
|
|
||||||
className="min-w-45"
|
|
||||||
>
|
|
||||||
了解更多
|
|
||||||
</RippleButton>
|
|
||||||
</MagneticButton>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={isVisible ? { opacity: 1, y: 0 } : {}}
|
|
||||||
transition={{ duration: 0.6, delay: 0.35 }}
|
|
||||||
className="flex flex-wrap gap-4 justify-center mb-16"
|
|
||||||
>
|
|
||||||
{features.map((feature, index) => (
|
|
||||||
<motion.div
|
|
||||||
key={index}
|
|
||||||
initial={{ opacity: 0, scale: 0.9 }}
|
|
||||||
animate={isVisible ? { opacity: 1, scale: 1 } : {}}
|
|
||||||
transition={{ duration: 0.4, delay: 0.4 + index * 0.1 }}
|
|
||||||
whileHover={{ scale: 1.05, y: -2 }}
|
|
||||||
className="flex items-center gap-2 px-4 py-2 rounded-full bg-[#FAFAFA] border border-[#E5E5E5] transition-all duration-300 hover:border-[#1C1C1C] hover:shadow-md cursor-default"
|
|
||||||
>
|
|
||||||
<feature.icon className="w-4 h-4 text-[#C41E3A]" />
|
|
||||||
<span className="text-sm text-[#3D3D3D]">{feature.text}</span>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
id="stats-section"
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={isVisible ? { opacity: 1, y: 0 } : {}}
|
|
||||||
transition={{ duration: 0.6, delay: 0.4 }}
|
|
||||||
className="pt-16 border-t border-[#E2E8F0]"
|
|
||||||
>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 md:gap-12">
|
|
||||||
{STATS.map((stat, index) => (
|
|
||||||
<StatItem
|
|
||||||
key={stat.label}
|
|
||||||
stat={stat}
|
|
||||||
index={index}
|
|
||||||
shouldAnimate={statsVisible}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatItem({ stat, index, shouldAnimate }: {
|
|
||||||
stat: { value: string; label: string };
|
|
||||||
index: number;
|
|
||||||
shouldAnimate: boolean;
|
|
||||||
}) {
|
|
||||||
const numericValue = parseInt(stat.value.replace(/\D/g, ''));
|
|
||||||
const suffix = stat.value.replace(/[\d]/g, '');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
className="group cursor-default text-center"
|
|
||||||
initial={{ opacity: 0, y: 20, scale: 0.9 }}
|
|
||||||
animate={shouldAnimate ? { opacity: 1, y: 0, scale: 1 } : {}}
|
|
||||||
transition={{ duration: 0.5, delay: index * 0.1, type: 'spring', stiffness: 100 }}
|
|
||||||
whileHover={{ scale: 1.05, y: -5 }}
|
|
||||||
>
|
|
||||||
<div className="text-4xl sm:text-5xl font-bold text-[#C41E3A] mb-3">
|
|
||||||
{shouldAnimate ? (
|
|
||||||
<CounterWithEffect
|
|
||||||
end={numericValue}
|
|
||||||
suffix={suffix}
|
|
||||||
effect="bounce"
|
|
||||||
duration={2000}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<span className="text-[#CBD5E0]">0{suffix}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-[#718096] group-hover:text-[#4A5568] transition-colors">
|
|
||||||
{stat.label}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ export const users = sqliteTable('users', {
|
|||||||
email: text('email').notNull().unique(),
|
email: text('email').notNull().unique(),
|
||||||
passwordHash: text('password_hash'),
|
passwordHash: text('password_hash'),
|
||||||
name: text('name').notNull(),
|
name: text('name').notNull(),
|
||||||
role: text('role', { enum: ['admin', 'editor', 'viewer'] }).notNull().default('editor'),
|
isAdmin: integer('is_admin', { mode: 'boolean' }).notNull().default(false),
|
||||||
avatar: text('avatar'),
|
avatar: text('avatar'),
|
||||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
||||||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ async function seed() {
|
|||||||
email: 'admin@novalon.cn',
|
email: 'admin@novalon.cn',
|
||||||
passwordHash: hashedPassword,
|
passwordHash: hashedPassword,
|
||||||
name: '系统管理员',
|
name: '系统管理员',
|
||||||
role: 'admin',
|
isAdmin: true,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
export type ErrorCode =
|
||||||
|
| 'UNAUTHORIZED'
|
||||||
|
| 'FORBIDDEN'
|
||||||
|
| 'NOT_FOUND'
|
||||||
|
| 'VALIDATION_ERROR'
|
||||||
|
| 'INTERNAL_ERROR'
|
||||||
|
| 'BAD_REQUEST';
|
||||||
|
|
||||||
|
export interface ApiError {
|
||||||
|
error: string;
|
||||||
|
code: ErrorCode;
|
||||||
|
details?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ERROR_MESSAGES: Record<ErrorCode, string> = {
|
||||||
|
UNAUTHORIZED: '未授权,请先登录',
|
||||||
|
FORBIDDEN: '无权限执行此操作',
|
||||||
|
NOT_FOUND: '请求的资源不存在',
|
||||||
|
VALIDATION_ERROR: '数据验证失败',
|
||||||
|
INTERNAL_ERROR: '服务器内部错误',
|
||||||
|
BAD_REQUEST: '请求参数错误',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function unauthorized(message?: string): NextResponse<ApiError> {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: message || ERROR_MESSAGES.UNAUTHORIZED, code: 'UNAUTHORIZED' },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function forbidden(message?: string): NextResponse<ApiError> {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: message || ERROR_MESSAGES.FORBIDDEN, code: 'FORBIDDEN' },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function notFound(message?: string): NextResponse<ApiError> {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: message || ERROR_MESSAGES.NOT_FOUND, code: 'NOT_FOUND' },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validationError(message: string, details?: Record<string, unknown>): NextResponse<ApiError> {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: message, code: 'VALIDATION_ERROR', details },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function badRequest(message: string): NextResponse<ApiError> {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: message, code: 'BAD_REQUEST' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function internalError(message = '服务器错误'): NextResponse<ApiError> {
|
||||||
|
console.error(message);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: ERROR_MESSAGES.INTERNAL_ERROR, code: 'INTERNAL_ERROR' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function success<T>(data: T, status = 200): NextResponse<T> {
|
||||||
|
return NextResponse.json(data, { status });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleApiError(error: unknown): NextResponse<ApiError> {
|
||||||
|
console.error('API Error:', error);
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
if (error.message.includes('未授权')) {
|
||||||
|
return unauthorized(error.message);
|
||||||
|
}
|
||||||
|
if (error.message.includes('无权限')) {
|
||||||
|
return forbidden(error.message);
|
||||||
|
}
|
||||||
|
if (error.message.includes('不存在')) {
|
||||||
|
return notFound(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return internalError();
|
||||||
|
}
|
||||||
+3
-3
@@ -42,7 +42,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
|||||||
id: user.id,
|
id: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
role: user.role,
|
isAdmin: user.isAdmin,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -51,14 +51,14 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
|||||||
async jwt({ token, user }) {
|
async jwt({ token, user }) {
|
||||||
if (user) {
|
if (user) {
|
||||||
token.id = user.id;
|
token.id = user.id;
|
||||||
token.role = user.role;
|
token.isAdmin = user.isAdmin;
|
||||||
}
|
}
|
||||||
return token;
|
return token;
|
||||||
},
|
},
|
||||||
async session({ session, token }) {
|
async session({ session, token }) {
|
||||||
if (session.user) {
|
if (session.user) {
|
||||||
session.user.id = token.id as string;
|
session.user.id = token.id as string;
|
||||||
session.user.role = token.role as string;
|
session.user.isAdmin = token.isAdmin as boolean;
|
||||||
}
|
}
|
||||||
return session;
|
return session;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,38 +1,37 @@
|
|||||||
import { auth } from '../auth';
|
import { auth } from '../auth';
|
||||||
import { hasPermission, Role, Resource, Action } from './permissions';
|
import { isAdminUser } from './permissions';
|
||||||
|
|
||||||
export async function checkPermission(
|
export async function checkIsAdmin(): Promise<{ isAdmin: boolean; userId?: string }> {
|
||||||
resource: Resource,
|
|
||||||
action: Action
|
|
||||||
): Promise<{ allowed: boolean; userId?: string; role?: Role }> {
|
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
|
|
||||||
if (!session || !session.user) {
|
if (!session || !session.user) {
|
||||||
return { allowed: false };
|
return { isAdmin: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const userRole = session.user.role as Role;
|
const isAdmin = isAdminUser(session.user.isAdmin as boolean | undefined);
|
||||||
const allowed = hasPermission(userRole, resource, action);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
allowed,
|
isAdmin,
|
||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
role: userRole,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function requirePermission(
|
export async function requireAdmin(): Promise<{ userId: string }> {
|
||||||
resource: Resource,
|
const result = await checkIsAdmin();
|
||||||
action: Action
|
|
||||||
): Promise<{ userId: string; role: Role }> {
|
|
||||||
const result = await checkPermission(resource, action);
|
|
||||||
|
|
||||||
if (!result.allowed) {
|
if (!result.isAdmin) {
|
||||||
throw new Error('无权限执行此操作');
|
throw new Error('无权限执行此操作');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userId: result.userId!,
|
userId: result.userId!,
|
||||||
role: result.role!,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAdminUserId(): Promise<string | null> {
|
||||||
|
const session = await auth();
|
||||||
|
if (!session?.user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return session.user.id;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,38 +1,3 @@
|
|||||||
export const PERMISSIONS = {
|
export function isAdminUser(isAdmin: boolean | undefined): boolean {
|
||||||
admin: {
|
return isAdmin === true;
|
||||||
content: ['create', 'read', 'update', 'delete', 'publish'],
|
|
||||||
config: ['read', 'update'],
|
|
||||||
users: ['create', 'read', 'update', 'delete'],
|
|
||||||
logs: ['read'],
|
|
||||||
},
|
|
||||||
editor: {
|
|
||||||
content: ['create', 'read', 'update', 'publish'],
|
|
||||||
config: ['read'],
|
|
||||||
users: [],
|
|
||||||
logs: ['read'],
|
|
||||||
},
|
|
||||||
viewer: {
|
|
||||||
content: ['read'],
|
|
||||||
config: ['read'],
|
|
||||||
users: [],
|
|
||||||
logs: [],
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export type Role = keyof typeof PERMISSIONS;
|
|
||||||
export type Resource = keyof typeof PERMISSIONS.admin;
|
|
||||||
export type Action = 'create' | 'read' | 'update' | 'delete' | 'publish';
|
|
||||||
|
|
||||||
export function hasPermission(
|
|
||||||
role: Role,
|
|
||||||
resource: Resource,
|
|
||||||
action: Action
|
|
||||||
): boolean {
|
|
||||||
const permissions = PERMISSIONS[role];
|
|
||||||
if (!permissions) return false;
|
|
||||||
|
|
||||||
const resourcePermissions = permissions[resource];
|
|
||||||
if (!resourcePermissions) return false;
|
|
||||||
|
|
||||||
return resourcePermissions.includes(action as never);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
import '@testing-library/jest-dom';
|
||||||
|
|
||||||
|
declare module 'expect' {
|
||||||
|
interface Matchers<R, T> extends jest.Matchers<R, T> {}
|
||||||
|
}
|
||||||
Vendored
+3
-3
@@ -4,18 +4,18 @@ declare module 'next-auth' {
|
|||||||
interface Session {
|
interface Session {
|
||||||
user: {
|
user: {
|
||||||
id: string;
|
id: string;
|
||||||
role: string;
|
isAdmin: boolean;
|
||||||
} & DefaultSession['user'];
|
} & DefaultSession['user'];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
role: string;
|
isAdmin: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'next-auth/jwt' {
|
declare module 'next-auth/jwt' {
|
||||||
interface JWT {
|
interface JWT {
|
||||||
id: string;
|
id: string;
|
||||||
role: string;
|
isAdmin: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user