forked from imbytecat/fullstack-starter
refactor: 移除设备与待办事项相关功能,重构许可证管理系统
- 删除设备信息表和待办事项表 - 添加许可证激活记录表的数据库模式快照,包含指纹唯一约束和时间戳字段。 - 添加新的迁移版本记录以支持最新数据库结构变更 - 移除 todo 相关操作的默认缓存失效配置,将 experimental_defaults 设置为空对象。 - 删除设备初始化逻辑,移除硬件指纹获取及数据库初始化相关功能。 - 将主页组件从待办事项列表替换为 License 管理系统欢迎界面,并添加设备指纹和 License 激活两个功能入口链接。 - 删除设备相关接口契约定义及验证规则 - 移除未使用的设备和待办事项合约导入,并更新合约导出对象。 - 删除待办事项接口契约文件中定义的输入输出验证 schema 及其相关路由契约 - 移除设备初始化逻辑,仅保留许可证激活初始化。 - 删除设备信息获取和许可证设置的API路由逻辑 - 移除未使用的路由模块导入并清理路由注册列表 - 删除待办事项相关的API路由定义及其实现逻辑 - 删除设备信息表的数据库模式定义。 - 移除设备信息表的导出,仅保留许可证激活表的导出。 - 删除待办事项数据表的定义及其相关字段配置
This commit is contained in:
2
apps/server/drizzle/0002_dizzy_kingpin.sql
Normal file
2
apps/server/drizzle/0002_dizzy_kingpin.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
DROP TABLE `device_info`;--> statement-breakpoint
|
||||||
|
DROP TABLE `todo`;
|
||||||
76
apps/server/drizzle/meta/0002_snapshot.json
Normal file
76
apps/server/drizzle/meta/0002_snapshot.json
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "2bdd10cc-8e14-4843-931a-37b1d00f21a6",
|
||||||
|
"prevId": "14bad572-1fc5-489d-90f2-7560a7cad1f4",
|
||||||
|
"tables": {
|
||||||
|
"license_activation": {
|
||||||
|
"name": "license_activation",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"fingerprint": {
|
||||||
|
"name": "fingerprint",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"license": {
|
||||||
|
"name": "license",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"license_activated_at": {
|
||||||
|
"name": "license_activated_at",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"license_activation_fingerprint_unique": {
|
||||||
|
"name": "license_activation_fingerprint_unique",
|
||||||
|
"columns": ["fingerprint"],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,13 @@
|
|||||||
"when": 1769409970060,
|
"when": 1769409970060,
|
||||||
"tag": "0001_watery_mongu",
|
"tag": "0001_watery_mongu",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1769412006512,
|
||||||
|
"tag": "0002_dizzy_kingpin",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,29 +2,5 @@ import { createTanstackQueryUtils } from '@orpc/tanstack-query'
|
|||||||
import { orpc as orpcClient } from './orpc.client'
|
import { orpc as orpcClient } from './orpc.client'
|
||||||
|
|
||||||
export const orpc = createTanstackQueryUtils(orpcClient, {
|
export const orpc = createTanstackQueryUtils(orpcClient, {
|
||||||
experimental_defaults: {
|
experimental_defaults: {},
|
||||||
todo: {
|
|
||||||
create: {
|
|
||||||
mutationOptions: {
|
|
||||||
onSuccess: (_, __, ___, ctx) => {
|
|
||||||
ctx.client.invalidateQueries({ queryKey: orpc.todo.list.key() })
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
mutationOptions: {
|
|
||||||
onSuccess: (_, __, ___, ctx) => {
|
|
||||||
ctx.client.invalidateQueries({ queryKey: orpc.todo.list.key() })
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
remove: {
|
|
||||||
mutationOptions: {
|
|
||||||
onSuccess: (_, __, ___, ctx) => {
|
|
||||||
ctx.client.invalidateQueries({ queryKey: orpc.todo.list.key() })
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
import { getHardwareFingerprint } from '@/lib/fingerprint'
|
|
||||||
import { getDB } from '@/server/db'
|
|
||||||
import { deviceInfoTable } from '@/server/db/schema'
|
|
||||||
|
|
||||||
let initPromise: Promise<void> | null = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 确保设备信息已在数据库中初始化
|
|
||||||
* 使用单例模式防止并发重复初始化
|
|
||||||
*/
|
|
||||||
export async function ensureDeviceInitialized(): Promise<void> {
|
|
||||||
if (initPromise) {
|
|
||||||
return initPromise
|
|
||||||
}
|
|
||||||
|
|
||||||
initPromise = (async () => {
|
|
||||||
try {
|
|
||||||
const db = getDB()
|
|
||||||
|
|
||||||
// 获取硬件指纹
|
|
||||||
let result: { fingerprint: string; quality: 'strong' | 'medium' | 'weak' }
|
|
||||||
try {
|
|
||||||
result = await getHardwareFingerprint({
|
|
||||||
cacheTtlMs: 10 * 60 * 1000,
|
|
||||||
includePrimaryDisk: true,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to get hardware fingerprint:', error)
|
|
||||||
// 回退逻辑
|
|
||||||
result = {
|
|
||||||
fingerprint: `unknown-${Date.now()}`,
|
|
||||||
quality: 'weak' as const,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 UPSERT 逻辑更新或插入设备信息
|
|
||||||
await db
|
|
||||||
.insert(deviceInfoTable)
|
|
||||||
.values({
|
|
||||||
fingerprint: result.fingerprint,
|
|
||||||
fingerprintQuality: result.quality,
|
|
||||||
license: null,
|
|
||||||
licenseActivatedAt: null,
|
|
||||||
})
|
|
||||||
.onConflictDoUpdate({
|
|
||||||
target: deviceInfoTable.fingerprint,
|
|
||||||
set: {
|
|
||||||
fingerprintQuality: result.quality,
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to initialize device info:', error)
|
|
||||||
// 重置 promise 以允许重试
|
|
||||||
initPromise = null
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
return initPromise
|
|
||||||
}
|
|
||||||
@@ -1,206 +1,44 @@
|
|||||||
import { useMutation, useSuspenseQuery } from '@tanstack/react-query'
|
import { createFileRoute, Link } from '@tanstack/react-router'
|
||||||
import { createFileRoute } from '@tanstack/react-router'
|
|
||||||
import type { ChangeEventHandler, FormEventHandler } from 'react'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { orpc } from '@/client/query-client'
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/')({
|
export const Route = createFileRoute('/')({
|
||||||
component: Todos,
|
component: Home,
|
||||||
loader: async ({ context }) => {
|
|
||||||
await context.queryClient.ensureQueryData(orpc.todo.list.queryOptions())
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function Todos() {
|
function Home() {
|
||||||
const [newTodoTitle, setNewTodoTitle] = useState('')
|
|
||||||
|
|
||||||
const listQuery = useSuspenseQuery(orpc.todo.list.queryOptions())
|
|
||||||
const createMutation = useMutation(orpc.todo.create.mutationOptions())
|
|
||||||
const updateMutation = useMutation(orpc.todo.update.mutationOptions())
|
|
||||||
const deleteMutation = useMutation(orpc.todo.remove.mutationOptions())
|
|
||||||
|
|
||||||
const handleCreateTodo: FormEventHandler<HTMLFormElement> = (e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
if (newTodoTitle.trim()) {
|
|
||||||
createMutation.mutate({ title: newTodoTitle.trim() })
|
|
||||||
setNewTodoTitle('')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleInputChange: ChangeEventHandler<HTMLInputElement> = (e) => {
|
|
||||||
setNewTodoTitle(e.target.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleToggleTodo = (id: string, currentCompleted: boolean) => {
|
|
||||||
updateMutation.mutate({
|
|
||||||
id,
|
|
||||||
data: { completed: !currentCompleted },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDeleteTodo = (id: string) => {
|
|
||||||
deleteMutation.mutate({ id })
|
|
||||||
}
|
|
||||||
|
|
||||||
const todos = listQuery.data
|
|
||||||
const completedCount = todos.filter((todo) => todo.completed).length
|
|
||||||
const totalCount = todos.length
|
|
||||||
const progress = totalCount > 0 ? (completedCount / totalCount) * 100 : 0
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-slate-50 py-12 px-4 sm:px-6 font-sans">
|
<div className="min-h-screen bg-slate-50 py-12 px-4 sm:px-6 font-sans">
|
||||||
<div className="max-w-2xl mx-auto space-y-8">
|
<div className="max-w-2xl mx-auto space-y-8 text-center">
|
||||||
{/* Header */}
|
<h1 className="text-4xl font-bold text-slate-900 tracking-tight">
|
||||||
<div className="flex items-end justify-between">
|
License 管理系统
|
||||||
<div>
|
</h1>
|
||||||
<h1 className="text-3xl font-bold text-slate-900 tracking-tight">
|
<p className="text-slate-500 text-lg">
|
||||||
我的待办
|
欢迎使用基于 TanStack Start + ORPC 的 License 管理系统
|
||||||
</h1>
|
</p>
|
||||||
<p className="text-slate-500 mt-1">保持专注,逐个击破</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
|
||||||
<div className="text-2xl font-semibold text-slate-900">
|
|
||||||
{completedCount}
|
|
||||||
<span className="text-slate-400 text-lg">/{totalCount}</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-xs font-medium text-slate-400 uppercase tracking-wider">
|
|
||||||
已完成
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Add Todo Form */}
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-8">
|
||||||
<form onSubmit={handleCreateTodo} className="relative group z-10">
|
<Link
|
||||||
<div className="relative transform transition-all duration-200 focus-within:-translate-y-1">
|
to="/fingerprint"
|
||||||
<input
|
className="p-6 bg-white rounded-2xl shadow-sm border border-slate-100 hover:shadow-md hover:border-indigo-500/50 transition-all text-left group"
|
||||||
type="text"
|
>
|
||||||
value={newTodoTitle}
|
<h2 className="text-xl font-semibold text-slate-900 group-hover:text-indigo-600 transition-colors">
|
||||||
onChange={handleInputChange}
|
设备指纹
|
||||||
placeholder="添加新任务..."
|
</h2>
|
||||||
className="w-full pl-6 pr-32 py-5 bg-white rounded-2xl shadow-[0_8px_30px_rgb(0,0,0,0.04)] border-0 ring-1 ring-slate-100 focus:ring-2 focus:ring-indigo-500/50 outline-none transition-all placeholder:text-slate-400 text-lg text-slate-700"
|
<p className="text-slate-500 mt-2 text-sm">
|
||||||
disabled={createMutation.isPending}
|
查询当前设备的硬件指纹信息,用于 License 绑定
|
||||||
/>
|
</p>
|
||||||
<button
|
</Link>
|
||||||
type="submit"
|
|
||||||
disabled={createMutation.isPending || !newTodoTitle.trim()}
|
|
||||||
className="absolute right-3 top-3 bottom-3 px-6 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl font-medium transition-all shadow-md shadow-indigo-200 disabled:opacity-50 disabled:shadow-none hover:shadow-lg hover:shadow-indigo-300 active:scale-95"
|
|
||||||
>
|
|
||||||
{createMutation.isPending ? '添加中' : '添加'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{/* Progress Bar (Only visible when there are tasks) */}
|
<Link
|
||||||
{totalCount > 0 && (
|
to="/license"
|
||||||
<div className="h-1.5 w-full bg-slate-200 rounded-full overflow-hidden">
|
className="p-6 bg-white rounded-2xl shadow-sm border border-slate-100 hover:shadow-md hover:border-indigo-500/50 transition-all text-left group"
|
||||||
<div
|
>
|
||||||
className="h-full bg-indigo-500 transition-all duration-500 ease-out rounded-full"
|
<h2 className="text-xl font-semibold text-slate-900 group-hover:text-indigo-600 transition-colors">
|
||||||
style={{ width: `${progress}%` }}
|
License 激活
|
||||||
/>
|
</h2>
|
||||||
</div>
|
<p className="text-slate-500 mt-2 text-sm">
|
||||||
)}
|
管理和激活当前设备的 License 授权状态
|
||||||
|
</p>
|
||||||
{/* Todo List */}
|
</Link>
|
||||||
<div className="space-y-3">
|
|
||||||
{todos.length === 0 ? (
|
|
||||||
<div className="py-20 text-center">
|
|
||||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-slate-100 mb-4">
|
|
||||||
<svg
|
|
||||||
className="w-8 h-8 text-slate-400"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<p className="text-slate-500 text-lg font-medium">没有待办事项</p>
|
|
||||||
<p className="text-slate-400 text-sm mt-1">
|
|
||||||
输入上方内容添加您的第一个任务
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
todos.map((todo) => (
|
|
||||||
<div
|
|
||||||
key={todo.id}
|
|
||||||
className={`group relative flex items-center p-4 bg-white rounded-xl border border-slate-100 shadow-sm transition-all duration-200 hover:shadow-md hover:border-slate-200 ${
|
|
||||||
todo.completed ? 'bg-slate-50/50' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleToggleTodo(todo.id, todo.completed)}
|
|
||||||
className={`flex-shrink-0 w-6 h-6 rounded-full border-2 transition-all duration-200 flex items-center justify-center mr-4 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 ${
|
|
||||||
todo.completed
|
|
||||||
? 'bg-indigo-500 border-indigo-500'
|
|
||||||
: 'border-slate-300 hover:border-indigo-500 bg-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{todo.completed && (
|
|
||||||
<svg
|
|
||||||
className="w-3.5 h-3.5 text-white"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={3}
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M5 13l4 4L19 7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p
|
|
||||||
className={`text-lg transition-all duration-200 truncate ${
|
|
||||||
todo.completed
|
|
||||||
? 'text-slate-400 line-through decoration-slate-300 decoration-2'
|
|
||||||
: 'text-slate-700'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{todo.title}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center opacity-0 group-hover:opacity-100 transition-opacity duration-200 absolute right-4 pl-4 bg-gradient-to-l from-white via-white to-transparent sm:static sm:bg-none">
|
|
||||||
<span className="text-xs text-slate-400 mr-3 hidden sm:inline-block">
|
|
||||||
{new Date(todo.createdAt).toLocaleDateString('zh-CN')}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleDeleteTodo(todo.id)}
|
|
||||||
className="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors focus:outline-none"
|
|
||||||
title="删除"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="w-5 h-5"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import { oc } from '@orpc/contract'
|
|
||||||
import { z } from 'zod'
|
|
||||||
|
|
||||||
const deviceInfoSchema = z.object({
|
|
||||||
fingerprint: z.string(),
|
|
||||||
quality: z.enum(['strong', 'medium', 'weak']),
|
|
||||||
license: z.string().nullable(),
|
|
||||||
licenseActivatedAt: z.number().nullable(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const getInfo = oc.input(z.void()).output(deviceInfoSchema)
|
|
||||||
|
|
||||||
export const setLicense = oc
|
|
||||||
.input(z.object({ license: z.string().min(1) }))
|
|
||||||
.output(z.object({ success: z.boolean() }))
|
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
import * as device from './device.contract'
|
|
||||||
import * as fingerprint from './fingerprint.contract'
|
import * as fingerprint from './fingerprint.contract'
|
||||||
import * as license from './license.contract'
|
import * as license from './license.contract'
|
||||||
import * as todo from './todo.contract'
|
|
||||||
|
|
||||||
export const contract = {
|
export const contract = {
|
||||||
device,
|
|
||||||
fingerprint,
|
fingerprint,
|
||||||
todo,
|
|
||||||
license,
|
license,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
import { oc } from '@orpc/contract'
|
|
||||||
import {
|
|
||||||
createInsertSchema,
|
|
||||||
createSelectSchema,
|
|
||||||
createUpdateSchema,
|
|
||||||
} from 'drizzle-zod'
|
|
||||||
import { z } from 'zod'
|
|
||||||
import { todoTable } from '@/server/db/schema'
|
|
||||||
|
|
||||||
const selectSchema = createSelectSchema(todoTable)
|
|
||||||
|
|
||||||
const insertSchema = createInsertSchema(todoTable).omit({
|
|
||||||
id: true,
|
|
||||||
createdAt: true,
|
|
||||||
updatedAt: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const updateSchema = createUpdateSchema(todoTable).omit({
|
|
||||||
id: true,
|
|
||||||
createdAt: true,
|
|
||||||
updatedAt: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const list = oc.input(z.void()).output(z.array(selectSchema))
|
|
||||||
|
|
||||||
export const create = oc.input(insertSchema).output(selectSchema)
|
|
||||||
|
|
||||||
export const update = oc
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
id: z.uuid(),
|
|
||||||
data: updateSchema,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.output(selectSchema)
|
|
||||||
|
|
||||||
export const remove = oc
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
id: z.uuid(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.output(z.void())
|
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
import { os } from '@orpc/server'
|
import { os } from '@orpc/server'
|
||||||
import { ensureDeviceInitialized } from '@/lib/device-init'
|
|
||||||
import { ensureLicenseActivationInitialized } from '@/lib/license-init'
|
import { ensureLicenseActivationInitialized } from '@/lib/license-init'
|
||||||
import { getDB } from '@/server/db'
|
import { getDB } from '@/server/db'
|
||||||
|
|
||||||
export const dbProvider = os.middleware(async ({ context, next }) => {
|
export const dbProvider = os.middleware(async ({ context, next }) => {
|
||||||
await ensureDeviceInitialized()
|
|
||||||
await ensureLicenseActivationInitialized()
|
await ensureLicenseActivationInitialized()
|
||||||
return next({
|
return next({
|
||||||
context: {
|
context: {
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
import { ORPCError } from '@orpc/server'
|
|
||||||
import { eq } from 'drizzle-orm'
|
|
||||||
import { ensureDeviceInitialized } from '@/lib/device-init'
|
|
||||||
import { deviceInfoTable } from '@/server/db/schema'
|
|
||||||
import { db } from '../middlewares'
|
|
||||||
import { os } from '../server'
|
|
||||||
|
|
||||||
export const getInfo = os.device.getInfo
|
|
||||||
.use(db)
|
|
||||||
.handler(async ({ context }) => {
|
|
||||||
// 再次确保初始化(竞态条件兜底)
|
|
||||||
await ensureDeviceInitialized()
|
|
||||||
|
|
||||||
const info = await context.db.query.deviceInfoTable.findFirst()
|
|
||||||
|
|
||||||
if (!info) {
|
|
||||||
// 理论上不应该发生,因为 ensureDeviceInitialized 已经调用
|
|
||||||
throw new ORPCError('NOT_FOUND', {
|
|
||||||
message: 'Device info not found after initialization',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
fingerprint: info.fingerprint,
|
|
||||||
quality: info.fingerprintQuality,
|
|
||||||
license: info.license,
|
|
||||||
licenseActivatedAt: info.licenseActivatedAt?.getTime() ?? null,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export const setLicense = os.device.setLicense
|
|
||||||
.use(db)
|
|
||||||
.handler(async ({ context, input }) => {
|
|
||||||
await ensureDeviceInitialized()
|
|
||||||
|
|
||||||
const info = await context.db.query.deviceInfoTable.findFirst()
|
|
||||||
|
|
||||||
if (!info) {
|
|
||||||
throw new ORPCError('NOT_FOUND', {
|
|
||||||
message: 'Device info not found',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
await context.db
|
|
||||||
.update(deviceInfoTable)
|
|
||||||
.set({
|
|
||||||
license: input.license,
|
|
||||||
licenseActivatedAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
})
|
|
||||||
.where(eq(deviceInfoTable.id, info.id))
|
|
||||||
|
|
||||||
return { success: true }
|
|
||||||
})
|
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
import { os } from '../server'
|
import { os } from '../server'
|
||||||
import * as device from './device.router'
|
|
||||||
import * as fingerprint from './fingerprint.router'
|
import * as fingerprint from './fingerprint.router'
|
||||||
import * as license from './license.router'
|
import * as license from './license.router'
|
||||||
import * as todo from './todo.router'
|
|
||||||
|
|
||||||
export const router = os.router({
|
export const router = os.router({
|
||||||
device,
|
|
||||||
fingerprint,
|
fingerprint,
|
||||||
todo,
|
|
||||||
license,
|
license,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
import { ORPCError } from '@orpc/server'
|
|
||||||
import { eq } from 'drizzle-orm'
|
|
||||||
import { todoTable } from '@/server/db/schema'
|
|
||||||
import { db } from '../middlewares'
|
|
||||||
import { os } from '../server'
|
|
||||||
|
|
||||||
export const list = os.todo.list.use(db).handler(async ({ context }) => {
|
|
||||||
const todos = await context.db.query.todoTable.findMany({
|
|
||||||
orderBy: (todos, { desc }) => [desc(todos.createdAt)],
|
|
||||||
})
|
|
||||||
return todos
|
|
||||||
})
|
|
||||||
|
|
||||||
export const create = os.todo.create
|
|
||||||
.use(db)
|
|
||||||
.handler(async ({ context, input }) => {
|
|
||||||
const [newTodo] = await context.db
|
|
||||||
.insert(todoTable)
|
|
||||||
.values(input)
|
|
||||||
.returning()
|
|
||||||
|
|
||||||
if (!newTodo) {
|
|
||||||
throw new ORPCError('NOT_FOUND')
|
|
||||||
}
|
|
||||||
|
|
||||||
return newTodo
|
|
||||||
})
|
|
||||||
|
|
||||||
export const update = os.todo.update
|
|
||||||
.use(db)
|
|
||||||
.handler(async ({ context, input }) => {
|
|
||||||
const [updatedTodo] = await context.db
|
|
||||||
.update(todoTable)
|
|
||||||
.set(input.data)
|
|
||||||
.where(eq(todoTable.id, input.id))
|
|
||||||
.returning()
|
|
||||||
|
|
||||||
if (!updatedTodo) {
|
|
||||||
throw new ORPCError('NOT_FOUND')
|
|
||||||
}
|
|
||||||
|
|
||||||
return updatedTodo
|
|
||||||
})
|
|
||||||
|
|
||||||
export const remove = os.todo.remove
|
|
||||||
.use(db)
|
|
||||||
.handler(async ({ context, input }) => {
|
|
||||||
await context.db.delete(todoTable).where(eq(todoTable.id, input.id))
|
|
||||||
})
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
|
||||||
import { generatedFields } from './utils/field'
|
|
||||||
|
|
||||||
export const deviceInfoTable = sqliteTable('device_info', {
|
|
||||||
...generatedFields,
|
|
||||||
fingerprint: text('fingerprint').notNull().unique(),
|
|
||||||
fingerprintQuality: text('fingerprint_quality', {
|
|
||||||
enum: ['strong', 'medium', 'weak'],
|
|
||||||
}).notNull(),
|
|
||||||
license: text('license'),
|
|
||||||
licenseActivatedAt: integer('license_activated_at', {
|
|
||||||
mode: 'timestamp_ms',
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
@@ -1,3 +1 @@
|
|||||||
export * from './device-info'
|
|
||||||
export * from './license-activation'
|
export * from './license-activation'
|
||||||
export * from './todo'
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
|
||||||
import { generatedFields } from './utils/field'
|
|
||||||
|
|
||||||
export const todoTable = sqliteTable('todo', {
|
|
||||||
...generatedFields,
|
|
||||||
title: text('title').notNull(),
|
|
||||||
completed: integer('completed', { mode: 'boolean' }).notNull().default(false),
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user