mirror of
https://github.com/beilunyang/moemail.git
synced 2025-09-26 19:41:22 +08:00
feat: Implement role-based access control and enhance permissions system
This commit is contained in:
50
README.md
50
README.md
@@ -14,6 +14,7 @@
|
||||
<a href="#本地运行">本地运行</a> •
|
||||
<a href="#部署">部署</a> •
|
||||
<a href="#Cloudflare 邮件路由配置">Cloudflare 邮件路由配置</a> •
|
||||
<a href="#权限系统">权限系统</a> •
|
||||
<a href="#Webhook 集成">Webhook 集成</a> •
|
||||
<a href="#环境变量">环境变量</a> •
|
||||
<a href="#Github OAuth App 配置">Github OAuth App 配置</a> •
|
||||
@@ -31,7 +32,7 @@
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
## 特性
|
||||
|
||||
@@ -45,6 +46,7 @@
|
||||
- 💸 **免费自部署**:基于 Cloudflare 构建, 可实现免费自部署,无需任何费用
|
||||
- 🎉 **可爱的 UI**:简洁可爱萌萌哒 UI 界面
|
||||
- 🔔 **Webhook 通知**:支持通过 webhook 接收新邮件通知
|
||||
- 🛡️ **权限系统**:支持基于角色的权限控制系统
|
||||
|
||||
## 技术栈
|
||||
|
||||
@@ -63,7 +65,7 @@
|
||||
### 前置要求
|
||||
|
||||
- Node.js 18+
|
||||
- pnpm
|
||||
- Pnpm
|
||||
- Wrangler CLI
|
||||
- Cloudflare 账号
|
||||
|
||||
@@ -241,6 +243,45 @@ pnpm deploy:cleanup
|
||||
- 确保域名的 DNS 托管在 Cloudflare
|
||||
- Email Worker 必须已经部署成功
|
||||
|
||||
## 权限系统
|
||||
|
||||
本项目采用基于角色的权限控制系统(RBAC)。
|
||||
|
||||
### 角色等级
|
||||
|
||||
系统包含三个角色等级:
|
||||
|
||||
1. **皇帝(Emperor)**
|
||||
- 网站所有者
|
||||
- 拥有所有权限
|
||||
- 每个站点仅允许一位皇帝
|
||||
|
||||
2. **骑士(Knight)**
|
||||
- 高级用户
|
||||
- 可以使用临时邮箱功能
|
||||
- 可以配置 Webhook
|
||||
- 开放注册时默认角色
|
||||
|
||||
3. **平民(Civilian)**
|
||||
- 普通用户
|
||||
- 无任何权限
|
||||
- 非开放注册时默认角色
|
||||
|
||||
### 权限配置
|
||||
|
||||
通过环境变量 `OPEN_REGISTRATION` 控制注册策略:
|
||||
- `true`: 新用户默认为骑士
|
||||
- `false`: 新用户默认为平民
|
||||
|
||||
### 角色升级
|
||||
|
||||
1. **成为皇帝**
|
||||
- 第一个访问 `/api/roles/init-emperor` 接口的用户将成为皇帝
|
||||
- 站点已有皇帝后,无法再提升其他用户为皇帝
|
||||
|
||||
2. **成为骑士**
|
||||
- 皇帝在个人中心页面对平民进行册封
|
||||
|
||||
|
||||
## Webhook 集成
|
||||
|
||||
@@ -301,6 +342,9 @@ pnpx cloudflared tunnel --url http://localhost:3001
|
||||
- `AUTH_GITHUB_SECRET`: GitHub OAuth App Secret
|
||||
- `AUTH_SECRET`: NextAuth Secret,用来加密 session,请设置一个随机字符串
|
||||
|
||||
### 权限相关
|
||||
- `OPEN_REGISTRATION`: 是否开放注册,`true` 表示开放注册,`false` 表示关闭注册
|
||||
|
||||
### 邮箱配置
|
||||
- `NEXT_PUBLIC_EMAIL_DOMAIN`: 邮箱域名,支持多域名,用逗号分隔 (例如: moemail.app,bitibiti.com)
|
||||
|
||||
@@ -335,7 +379,7 @@ pnpx cloudflared tunnel --url http://localhost:3001
|
||||
## 支持
|
||||
|
||||
如果你喜欢这个项目,欢迎给它一个 Star ⭐️
|
||||
或者进行赞助
|
||||
或者进<EFBFBD><EFBFBD><EFBFBD>赞助
|
||||
<br />
|
||||
<br />
|
||||
<img src="https://pic.otaku.ren/20240212/AQADPrgxGwoIWFZ-.jpg" style="width: 400px;"/>
|
||||
|
54
app/api/roles/init-emperor/route.ts
Normal file
54
app/api/roles/init-emperor/route.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { auth } from "@/lib/auth";
|
||||
import { createDb } from "@/lib/db";
|
||||
import { roles, userRoles } from "@/lib/schema";
|
||||
import { ROLES } from "@/lib/permissions";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
||||
export async function GET() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return Response.json({ error: "未授权" }, { status: 401 });
|
||||
}
|
||||
|
||||
const db = createDb();
|
||||
|
||||
const emperorRole = await db.query.roles.findFirst({
|
||||
where: eq(roles.name, ROLES.EMPEROR),
|
||||
with: {
|
||||
userRoles: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (emperorRole && emperorRole.userRoles.length > 0) {
|
||||
return Response.json({ error: "已存在皇帝, 谋反将被处死" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
let roleId = emperorRole?.id;
|
||||
if (!roleId) {
|
||||
const [newRole] = await db.insert(roles)
|
||||
.values({
|
||||
name: ROLES.EMPEROR,
|
||||
description: "皇帝(网站所有者)",
|
||||
})
|
||||
.returning({ id: roles.id });
|
||||
roleId = newRole.id;
|
||||
}
|
||||
|
||||
await db.insert(userRoles)
|
||||
.values({
|
||||
userId: session.user.id,
|
||||
roleId,
|
||||
});
|
||||
|
||||
return Response.json({ message: "登基成功,你已成为皇帝" });
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize emperor:", error);
|
||||
return Response.json(
|
||||
{ error: "登基称帝失败" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
58
app/api/roles/promote/route.ts
Normal file
58
app/api/roles/promote/route.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { createDb } from "@/lib/db";
|
||||
import { roles, userRoles } from "@/lib/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { ROLES } from "@/lib/permissions";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { userId, roleName } = await request.json() as { userId: string, roleName: string };
|
||||
if (!userId || !roleName) {
|
||||
return Response.json(
|
||||
{ error: "缺少必要参数" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (roleName !== ROLES.KNIGHT) {
|
||||
return Response.json(
|
||||
{ error: "角色不合法" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const db = createDb();
|
||||
|
||||
let targetRole = await db.query.roles.findFirst({
|
||||
where: eq(roles.name, roleName),
|
||||
});
|
||||
|
||||
if (!targetRole) {
|
||||
const [newRole] = await db.insert(roles)
|
||||
.values({
|
||||
name: roleName,
|
||||
description: "高级用户",
|
||||
})
|
||||
.returning();
|
||||
targetRole = newRole;
|
||||
}
|
||||
|
||||
await db.delete(userRoles)
|
||||
.where(eq(userRoles.userId, userId));
|
||||
|
||||
await db.insert(userRoles)
|
||||
.values({
|
||||
userId,
|
||||
roleId: targetRole.id,
|
||||
});
|
||||
|
||||
return Response.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("Failed to promote user:", error);
|
||||
return Response.json(
|
||||
{ error: "升级用户失败" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
43
app/api/roles/users/route.ts
Normal file
43
app/api/roles/users/route.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { createDb } from "@/lib/db"
|
||||
import { userRoles, users } from "@/lib/schema"
|
||||
import { eq } from "drizzle-orm"
|
||||
|
||||
export const runtime = "edge"
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const url = new URL(request.url)
|
||||
const email = url.searchParams.get('email')
|
||||
|
||||
if (!email) {
|
||||
return Response.json(
|
||||
{ error: "邮箱地址不能为空" },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const db = createDb()
|
||||
|
||||
const user = await db.query.users.findFirst({
|
||||
where: eq(users.email, email),
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
return Response.json({ user: null })
|
||||
}
|
||||
|
||||
const userRole = await db.query.userRoles.findFirst({
|
||||
where: eq(userRoles.userId, user.id),
|
||||
with: {
|
||||
role: true
|
||||
}
|
||||
})
|
||||
|
||||
return Response.json({
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
role: userRole?.role.name
|
||||
}
|
||||
})
|
||||
}
|
@@ -90,8 +90,8 @@ export function CreateDialog({ onEmailCreated }: CreateDialogProps) {
|
||||
const fetchDomains = async () => {
|
||||
const response = await fetch("/api/emails/domains");
|
||||
const data = (await response.json()) as DomainResponse;
|
||||
setDomains(data.domains);
|
||||
setCurrentDomain(data.domains[0]);
|
||||
setDomains(data.domains || []);
|
||||
setCurrentDomain(data.domains[0] || "");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
27
app/components/no-permission-dialog.tsx
Normal file
27
app/components/no-permission-dialog.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useRouter } from "next/navigation"
|
||||
|
||||
export function NoPermissionDialog() {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-background/50 backdrop-blur-sm z-50">
|
||||
<div className="fixed left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%] w-[90%] max-w-md">
|
||||
<div className="bg-background border-2 border-primary/20 rounded-lg p-6 md:p-12 shadow-lg">
|
||||
<div className="text-center space-y-4">
|
||||
<h1 className="text-xl md:text-2xl font-bold">权限不足</h1>
|
||||
<p className="text-sm md:text-base text-muted-foreground">你没有权限访问此页面,请联系网站皇帝授权</p>
|
||||
<Button
|
||||
onClick={() => router.push("/")}
|
||||
className="mt-4 w-full md:w-auto"
|
||||
>
|
||||
返回首页
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -4,20 +4,31 @@ import { User } from "next-auth"
|
||||
import Image from "next/image"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { signOut } from "next-auth/react"
|
||||
import { Github, Mail, Settings } from "lucide-react"
|
||||
import { Github, Mail, Settings, Crown, Sword, User2 } from "lucide-react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { WebhookConfig } from "./webhook-config"
|
||||
import { PromotePanel } from "./promote-panel"
|
||||
import { useRolePermission } from "@/hooks/use-role-permission"
|
||||
import { PERMISSIONS } from "@/lib/permissions"
|
||||
|
||||
interface ProfileCardProps {
|
||||
user: User
|
||||
}
|
||||
|
||||
const roleConfigs = {
|
||||
emperor: { name: '皇帝', icon: Crown },
|
||||
knight: { name: '骑士', icon: Sword },
|
||||
civilian: { name: '平民', icon: User2 },
|
||||
} as const
|
||||
|
||||
export function ProfileCard({ user }: ProfileCardProps) {
|
||||
const router = useRouter()
|
||||
const { checkPermission } = useRolePermission()
|
||||
const canManageWebhook = checkPermission(PERMISSIONS.MANAGE_WEBHOOK)
|
||||
const canPromote = checkPermission(PERMISSIONS.PROMOTE_USER)
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto space-y-6">
|
||||
{/* 用户信息卡片 */}
|
||||
<div className="bg-background rounded-lg border-2 border-primary/20 p-6">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="relative">
|
||||
@@ -42,20 +53,40 @@ export function ProfileCard({ user }: ProfileCardProps) {
|
||||
<p className="text-sm text-muted-foreground truncate mt-1">
|
||||
{user.email}
|
||||
</p>
|
||||
{user.roles && (
|
||||
<div className="flex gap-2 mt-2">
|
||||
{user.roles.map(({ name }) => {
|
||||
const roleConfig = roleConfigs[name as keyof typeof roleConfigs]
|
||||
const Icon = roleConfig.icon
|
||||
return (
|
||||
<div
|
||||
key={name}
|
||||
className="flex items-center gap-1 text-xs bg-primary/10 text-primary px-2 py-0.5 rounded"
|
||||
title={roleConfig.name}
|
||||
>
|
||||
<Icon className="w-3 h-3" />
|
||||
{roleConfig.name}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Webhook 配置卡片 */}
|
||||
<div className="bg-background rounded-lg border-2 border-primary/20 p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<Settings className="w-5 h-5 text-primary" />
|
||||
<h2 className="text-lg font-semibold">Webhook 配置</h2>
|
||||
{canManageWebhook && (
|
||||
<div className="bg-background rounded-lg border-2 border-primary/20 p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<Settings className="w-5 h-5 text-primary" />
|
||||
<h2 className="text-lg font-semibold">Webhook 配置</h2>
|
||||
</div>
|
||||
<WebhookConfig />
|
||||
</div>
|
||||
<WebhookConfig />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{canPromote && <PromotePanel />}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 px-1">
|
||||
<Button
|
||||
onClick={() => router.push("/moe")}
|
||||
|
93
app/components/profile/promote-panel.tsx
Normal file
93
app/components/profile/promote-panel.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Sword, Search, Loader2 } from "lucide-react"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { useState } from "react"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
import { ROLES } from "@/lib/permissions"
|
||||
|
||||
|
||||
export function PromotePanel() {
|
||||
const [email, setEmail] = useState("")
|
||||
const [loading, setLoading] = useState(false)
|
||||
const { toast } = useToast()
|
||||
|
||||
const handlePromote = async () => {
|
||||
if (!email) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await fetch(`/api/roles/users?email=${encodeURIComponent(email)}`)
|
||||
const data = await res.json() as { user?: { id: string; name?: string; email: string }; error?: string }
|
||||
|
||||
if (!res.ok) throw new Error(data.error || '未知错误')
|
||||
if (!data.user) {
|
||||
toast({
|
||||
title: "未找到用户",
|
||||
description: "请确认邮箱地址是否正确",
|
||||
variant: "destructive"
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const promoteRes = await fetch('/api/roles/promote', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
userId: data.user.id,
|
||||
roleName: ROLES.KNIGHT
|
||||
})
|
||||
})
|
||||
|
||||
if (!promoteRes.ok) throw new Error('册封失败')
|
||||
|
||||
toast({
|
||||
title: "册封成功",
|
||||
description: `已将 ${data.user.email} 册封为骑士`
|
||||
})
|
||||
setEmail("")
|
||||
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "册封失败",
|
||||
description: error instanceof Error ? error.message : "请稍后重试",
|
||||
variant: "destructive"
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-background rounded-lg border-2 border-primary/20 p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<Sword className="w-5 h-5 text-primary" />
|
||||
<h2 className="text-lg font-semibold">册封骑士</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
placeholder="输入用户邮箱"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handlePromote}
|
||||
disabled={!email || loading}
|
||||
className="gap-2"
|
||||
>
|
||||
{loading ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Search className="w-4 h-4" />
|
||||
)}
|
||||
{loading ? "册封中..." : "册封"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
25
app/hooks/use-role-permission.ts
Normal file
25
app/hooks/use-role-permission.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
"use client"
|
||||
|
||||
import { useSession } from "next-auth/react"
|
||||
import { Permission, Role, hasPermission } from "@/lib/permissions"
|
||||
|
||||
export function useRolePermission() {
|
||||
const { data: session } = useSession()
|
||||
const roles = session?.user?.roles
|
||||
|
||||
const checkPermission = (permission: Permission) => {
|
||||
if (!roles) return false
|
||||
return hasPermission(roles.map(r => r.name) as Role[], permission)
|
||||
}
|
||||
|
||||
const hasRole = (role: Role) => {
|
||||
if (!roles) return false
|
||||
return roles.some(r => r.name === role)
|
||||
}
|
||||
|
||||
return {
|
||||
checkPermission,
|
||||
hasRole,
|
||||
roles,
|
||||
}
|
||||
}
|
132
app/lib/auth.ts
132
app/lib/auth.ts
@@ -1,27 +1,127 @@
|
||||
import NextAuth from "next-auth"
|
||||
import GitHub from "next-auth/providers/github"
|
||||
import { DrizzleAdapter } from "@auth/drizzle-adapter"
|
||||
import { createDb } from "./db"
|
||||
import { accounts, sessions, users } from "./schema"
|
||||
import { createDb, Db } from "./db"
|
||||
import { accounts, sessions, users, roles, userRoles } from "./schema"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { Permission, hasPermission, ROLES, Role } from "./permissions"
|
||||
|
||||
const ROLE_DESCRIPTIONS: Record<Role, string> = {
|
||||
[ROLES.EMPEROR]: "皇帝(网站所有者)",
|
||||
[ROLES.KNIGHT]: "骑士(高级用户)",
|
||||
[ROLES.CIVILIAN]: "平民(普通用户)",
|
||||
}
|
||||
|
||||
const getDefaultRole = (): Role =>
|
||||
process.env.OPEN_REGISTRATION === 'true' ? ROLES.KNIGHT : ROLES.CIVILIAN
|
||||
|
||||
async function findOrCreateRole(db: Db, roleName: Role) {
|
||||
let role = await db.query.roles.findFirst({
|
||||
where: eq(roles.name, roleName),
|
||||
})
|
||||
|
||||
if (!role) {
|
||||
const [newRole] = await db.insert(roles)
|
||||
.values({
|
||||
name: roleName,
|
||||
description: ROLE_DESCRIPTIONS[roleName],
|
||||
})
|
||||
.returning()
|
||||
role = newRole
|
||||
}
|
||||
|
||||
return role
|
||||
}
|
||||
|
||||
async function assignRoleToUser(db: Db, userId: string, roleId: string) {
|
||||
await db.insert(userRoles)
|
||||
.values({
|
||||
userId,
|
||||
roleId,
|
||||
})
|
||||
}
|
||||
|
||||
export async function checkPermission(permission: Permission) {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) return false
|
||||
|
||||
const db = createDb()
|
||||
const userRoleRecords = await db.query.userRoles.findMany({
|
||||
where: eq(userRoles.userId, session.user.id),
|
||||
with: { role: true },
|
||||
})
|
||||
|
||||
const userRoleNames = userRoleRecords.map(ur => ur.role.name)
|
||||
return hasPermission(userRoleNames as Role[], permission)
|
||||
}
|
||||
|
||||
export const {
|
||||
handlers: { GET, POST },
|
||||
auth,
|
||||
signIn,
|
||||
signOut
|
||||
} = NextAuth(() => {
|
||||
return {
|
||||
secret: process.env.AUTH_SECRET,
|
||||
adapter: DrizzleAdapter(createDb(), {
|
||||
usersTable: users,
|
||||
accountsTable: accounts,
|
||||
sessionsTable: sessions,
|
||||
}),
|
||||
providers: [
|
||||
GitHub({
|
||||
clientId: process.env.AUTH_GITHUB_ID,
|
||||
clientSecret: process.env.AUTH_GITHUB_SECRET,
|
||||
} = NextAuth(() => ({
|
||||
secret: process.env.AUTH_SECRET,
|
||||
adapter: DrizzleAdapter(createDb(), {
|
||||
usersTable: users,
|
||||
accountsTable: accounts,
|
||||
sessionsTable: sessions,
|
||||
}),
|
||||
providers: [
|
||||
GitHub({
|
||||
clientId: process.env.AUTH_GITHUB_ID,
|
||||
clientSecret: process.env.AUTH_GITHUB_SECRET,
|
||||
})
|
||||
],
|
||||
events: {
|
||||
async signIn({ user }) {
|
||||
if (!user.id) return
|
||||
|
||||
try {
|
||||
const db = createDb()
|
||||
const existingRole = await db.query.userRoles.findFirst({
|
||||
where: eq(userRoles.userId, user.id),
|
||||
})
|
||||
|
||||
if (existingRole) return
|
||||
|
||||
const defaultRole = await findOrCreateRole(db, getDefaultRole())
|
||||
await assignRoleToUser(db, user.id, defaultRole.id)
|
||||
} catch (error) {
|
||||
console.error('Error assigning role:', error)
|
||||
}
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
signIn: "/",
|
||||
error: "/",
|
||||
},
|
||||
callbacks: {
|
||||
async session({ session, user }) {
|
||||
if (!session?.user) return session
|
||||
|
||||
const db = createDb()
|
||||
let userRoleRecords = await db.query.userRoles.findMany({
|
||||
where: eq(userRoles.userId, user.id),
|
||||
with: { role: true },
|
||||
})
|
||||
],
|
||||
|
||||
if (!userRoleRecords.length) {
|
||||
const defaultRole = await findOrCreateRole(db, getDefaultRole())
|
||||
await assignRoleToUser(db, user.id, defaultRole.id)
|
||||
userRoleRecords = [{
|
||||
userId: user.id,
|
||||
roleId: defaultRole.id,
|
||||
createdAt: new Date(),
|
||||
role: defaultRole
|
||||
}]
|
||||
}
|
||||
|
||||
session.user.roles = userRoleRecords.map(ur => ({
|
||||
name: ur.role.name,
|
||||
}))
|
||||
|
||||
return session
|
||||
},
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
@@ -2,4 +2,6 @@ import { getRequestContext } from "@cloudflare/next-on-pages"
|
||||
import { drizzle } from "drizzle-orm/d1"
|
||||
import * as schema from "./schema"
|
||||
|
||||
export const createDb = () => drizzle(getRequestContext().env.DB, { schema })
|
||||
export const createDb = () => drizzle(getRequestContext().env.DB, { schema })
|
||||
|
||||
export type Db = ReturnType<typeof createDb>
|
||||
|
28
app/lib/permissions.ts
Normal file
28
app/lib/permissions.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export const ROLES = {
|
||||
EMPEROR: 'emperor',
|
||||
KNIGHT: 'knight',
|
||||
CIVILIAN: 'civilian',
|
||||
} as const;
|
||||
|
||||
export type Role = typeof ROLES[keyof typeof ROLES];
|
||||
|
||||
export const PERMISSIONS = {
|
||||
MANAGE_EMAIL: 'manage_email',
|
||||
MANAGE_WEBHOOK: 'manage_webhook',
|
||||
PROMOTE_USER: 'promote_user',
|
||||
} as const;
|
||||
|
||||
export type Permission = typeof PERMISSIONS[keyof typeof PERMISSIONS];
|
||||
|
||||
export const ROLE_PERMISSIONS: Record<Role, Permission[]> = {
|
||||
[ROLES.EMPEROR]: Object.values(PERMISSIONS),
|
||||
[ROLES.KNIGHT]: [
|
||||
PERMISSIONS.MANAGE_EMAIL,
|
||||
PERMISSIONS.MANAGE_WEBHOOK,
|
||||
],
|
||||
[ROLES.CIVILIAN]: [],
|
||||
} as const;
|
||||
|
||||
export function hasPermission(userRoles: Role[], permission: Permission): boolean {
|
||||
return userRoles.some(role => ROLE_PERMISSIONS[role]?.includes(permission));
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
import { integer, sqliteTable, text, primaryKey } from "drizzle-orm/sqlite-core"
|
||||
import type { AdapterAccountType } from "next-auth/adapters"
|
||||
|
||||
import { relations } from 'drizzle-orm';
|
||||
|
||||
// https://authjs.dev/getting-started/adapters/drizzle
|
||||
export const users = sqliteTable("user", {
|
||||
id: text("id")
|
||||
@@ -81,4 +82,35 @@ export const webhooks = sqliteTable('webhook', {
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
})
|
||||
})
|
||||
|
||||
export const roles = sqliteTable("role", {
|
||||
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||
name: text("name").notNull(),
|
||||
description: text("description"),
|
||||
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => new Date()),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp" }).$defaultFn(() => new Date()),
|
||||
});
|
||||
|
||||
export const rolesRelations = relations(roles, ({ many }) => ({
|
||||
userRoles: many(userRoles),
|
||||
}));
|
||||
|
||||
export const userRoles = sqliteTable("user_role", {
|
||||
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
roleId: text("role_id").notNull().references(() => roles.id, { onDelete: "cascade" }),
|
||||
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(() => new Date()),
|
||||
}, (table) => ({
|
||||
pk: primaryKey({ columns: [table.userId, table.roleId] }),
|
||||
}));
|
||||
|
||||
export const userRolesRelations = relations(userRoles, ({ one }) => ({
|
||||
user: one(users, {
|
||||
fields: [userRoles.userId],
|
||||
references: [users.id],
|
||||
}),
|
||||
role: one(roles, {
|
||||
fields: [userRoles.roleId],
|
||||
references: [roles.id],
|
||||
}),
|
||||
}));
|
24
app/moe/no-permission/page.tsx
Normal file
24
app/moe/no-permission/page.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Header } from "@/components/layout/header"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Crown } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
export default function NoPermissionPage() {
|
||||
return (
|
||||
<div className="bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 h-screen">
|
||||
<div className="container mx-auto h-full px-4 lg:px-8 max-w-[1600px]">
|
||||
<Header />
|
||||
<main className="h-full flex items-center justify-center">
|
||||
<div className="text-center space-y-4">
|
||||
<Crown className="w-12 h-12 text-primary mx-auto" />
|
||||
<h1 className="text-2xl font-bold">权限不足</h1>
|
||||
<p className="text-muted-foreground">你没有权限访问此页面,请联系皇帝</p>
|
||||
<Link href="/">
|
||||
<Button>返回首页</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -1,23 +1,29 @@
|
||||
import { Header } from "@/components/layout/header"
|
||||
import { ThreeColumnLayout } from "@/components/emails/three-column-layout"
|
||||
import { NoPermissionDialog } from "@/components/no-permission-dialog"
|
||||
import { auth } from "@/lib/auth"
|
||||
import { redirect } from "next/navigation"
|
||||
import { checkPermission } from "@/lib/auth"
|
||||
import { PERMISSIONS } from "@/lib/permissions"
|
||||
|
||||
export const runtime = "edge"
|
||||
|
||||
export default async function MoePage() {
|
||||
const session = await auth()
|
||||
|
||||
if (!session) {
|
||||
if (!session?.user) {
|
||||
redirect("/")
|
||||
}
|
||||
|
||||
const hasPermission = await checkPermission(PERMISSIONS.MANAGE_EMAIL)
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 h-screen">
|
||||
<div className="container mx-auto h-full px-4 lg:px-8 max-w-[1600px]">
|
||||
<Header />
|
||||
<main className="h-full">
|
||||
<ThreeColumnLayout />
|
||||
{!hasPermission && <NoPermissionDialog />}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
33
drizzle/0004_panoramic_hedge_knight.sql
Normal file
33
drizzle/0004_panoramic_hedge_knight.sql
Normal file
@@ -0,0 +1,33 @@
|
||||
CREATE TABLE `permission` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`description` text,
|
||||
`created_at` integer,
|
||||
`updated_at` integer
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `role_permission` (
|
||||
`role_id` text NOT NULL,
|
||||
`permission_id` text NOT NULL,
|
||||
`created_at` integer,
|
||||
PRIMARY KEY(`role_id`, `permission_id`),
|
||||
FOREIGN KEY (`role_id`) REFERENCES `role`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`permission_id`) REFERENCES `permission`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `role` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`description` text,
|
||||
`created_at` integer,
|
||||
`updated_at` integer
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `user_role` (
|
||||
`user_id` text NOT NULL,
|
||||
`role_id` text NOT NULL,
|
||||
`created_at` integer,
|
||||
PRIMARY KEY(`user_id`, `role_id`),
|
||||
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`role_id`) REFERENCES `role`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
2
drizzle/0005_tricky_mathemanic.sql
Normal file
2
drizzle/0005_tricky_mathemanic.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
DROP TABLE `permission`;--> statement-breakpoint
|
||||
DROP TABLE `role_permission`;
|
654
drizzle/meta/0004_snapshot.json
Normal file
654
drizzle/meta/0004_snapshot.json
Normal file
@@ -0,0 +1,654 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "f54d0fb3-1aba-4100-89b8-6aaa32d52eba",
|
||||
"prevId": "c65780fc-6545-438d-a3b6-fbdafa9b1276",
|
||||
"tables": {
|
||||
"account": {
|
||||
"name": "account",
|
||||
"columns": {
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"providerAccountId": {
|
||||
"name": "providerAccountId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"token_type": {
|
||||
"name": "token_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"session_state": {
|
||||
"name": "session_state",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"account_userId_user_id_fk": {
|
||||
"name": "account_userId_user_id_fk",
|
||||
"tableFrom": "account",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"userId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"account_provider_providerAccountId_pk": {
|
||||
"columns": [
|
||||
"provider",
|
||||
"providerAccountId"
|
||||
],
|
||||
"name": "account_provider_providerAccountId_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"address": {
|
||||
"name": "address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"email_address_unique": {
|
||||
"name": "email_address_unique",
|
||||
"columns": [
|
||||
"address"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"email_userId_user_id_fk": {
|
||||
"name": "email_userId_user_id_fk",
|
||||
"tableFrom": "email",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"userId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"message": {
|
||||
"name": "message",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"emailId": {
|
||||
"name": "emailId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"from_address": {
|
||||
"name": "from_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"subject": {
|
||||
"name": "subject",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"content": {
|
||||
"name": "content",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"html": {
|
||||
"name": "html",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"received_at": {
|
||||
"name": "received_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"message_emailId_email_id_fk": {
|
||||
"name": "message_emailId_email_id_fk",
|
||||
"tableFrom": "message",
|
||||
"tableTo": "email",
|
||||
"columnsFrom": [
|
||||
"emailId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"permission": {
|
||||
"name": "permission",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"role_permission": {
|
||||
"name": "role_permission",
|
||||
"columns": {
|
||||
"role_id": {
|
||||
"name": "role_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"permission_id": {
|
||||
"name": "permission_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"role_permission_role_id_role_id_fk": {
|
||||
"name": "role_permission_role_id_role_id_fk",
|
||||
"tableFrom": "role_permission",
|
||||
"tableTo": "role",
|
||||
"columnsFrom": [
|
||||
"role_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"role_permission_permission_id_permission_id_fk": {
|
||||
"name": "role_permission_permission_id_permission_id_fk",
|
||||
"tableFrom": "role_permission",
|
||||
"tableTo": "permission",
|
||||
"columnsFrom": [
|
||||
"permission_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"role_permission_role_id_permission_id_pk": {
|
||||
"columns": [
|
||||
"role_id",
|
||||
"permission_id"
|
||||
],
|
||||
"name": "role_permission_role_id_permission_id_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"session": {
|
||||
"name": "session",
|
||||
"columns": {
|
||||
"sessionToken": {
|
||||
"name": "sessionToken",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires": {
|
||||
"name": "expires",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"session_userId_user_id_fk": {
|
||||
"name": "session_userId_user_id_fk",
|
||||
"tableFrom": "session",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"userId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"user_role": {
|
||||
"name": "user_role",
|
||||
"columns": {
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"role_id": {
|
||||
"name": "role_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"user_role_user_id_user_id_fk": {
|
||||
"name": "user_role_user_id_user_id_fk",
|
||||
"tableFrom": "user_role",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"user_role_role_id_role_id_fk": {
|
||||
"name": "user_role_role_id_role_id_fk",
|
||||
"tableFrom": "user_role",
|
||||
"tableTo": "role",
|
||||
"columnsFrom": [
|
||||
"role_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"user_role_user_id_role_id_pk": {
|
||||
"columns": [
|
||||
"user_id",
|
||||
"role_id"
|
||||
],
|
||||
"name": "user_role_user_id_role_id_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"user": {
|
||||
"name": "user",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"emailVerified": {
|
||||
"name": "emailVerified",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_email_unique": {
|
||||
"name": "user_email_unique",
|
||||
"columns": [
|
||||
"email"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"webhook": {
|
||||
"name": "webhook",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"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": {},
|
||||
"foreignKeys": {
|
||||
"webhook_user_id_user_id_fk": {
|
||||
"name": "webhook_user_id_user_id_fk",
|
||||
"tableFrom": "webhook",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
543
drizzle/meta/0005_snapshot.json
Normal file
543
drizzle/meta/0005_snapshot.json
Normal file
@@ -0,0 +1,543 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "1cde0c8f-dffe-4a01-bc08-678dc5879a13",
|
||||
"prevId": "f54d0fb3-1aba-4100-89b8-6aaa32d52eba",
|
||||
"tables": {
|
||||
"account": {
|
||||
"name": "account",
|
||||
"columns": {
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"providerAccountId": {
|
||||
"name": "providerAccountId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"token_type": {
|
||||
"name": "token_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"session_state": {
|
||||
"name": "session_state",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"account_userId_user_id_fk": {
|
||||
"name": "account_userId_user_id_fk",
|
||||
"tableFrom": "account",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"userId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"account_provider_providerAccountId_pk": {
|
||||
"columns": [
|
||||
"provider",
|
||||
"providerAccountId"
|
||||
],
|
||||
"name": "account_provider_providerAccountId_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"address": {
|
||||
"name": "address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"email_address_unique": {
|
||||
"name": "email_address_unique",
|
||||
"columns": [
|
||||
"address"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"email_userId_user_id_fk": {
|
||||
"name": "email_userId_user_id_fk",
|
||||
"tableFrom": "email",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"userId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"message": {
|
||||
"name": "message",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"emailId": {
|
||||
"name": "emailId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"from_address": {
|
||||
"name": "from_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"subject": {
|
||||
"name": "subject",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"content": {
|
||||
"name": "content",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"html": {
|
||||
"name": "html",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"received_at": {
|
||||
"name": "received_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"message_emailId_email_id_fk": {
|
||||
"name": "message_emailId_email_id_fk",
|
||||
"tableFrom": "message",
|
||||
"tableTo": "email",
|
||||
"columnsFrom": [
|
||||
"emailId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"session": {
|
||||
"name": "session",
|
||||
"columns": {
|
||||
"sessionToken": {
|
||||
"name": "sessionToken",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires": {
|
||||
"name": "expires",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"session_userId_user_id_fk": {
|
||||
"name": "session_userId_user_id_fk",
|
||||
"tableFrom": "session",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"userId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"user_role": {
|
||||
"name": "user_role",
|
||||
"columns": {
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"role_id": {
|
||||
"name": "role_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"user_role_user_id_user_id_fk": {
|
||||
"name": "user_role_user_id_user_id_fk",
|
||||
"tableFrom": "user_role",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"user_role_role_id_role_id_fk": {
|
||||
"name": "user_role_role_id_role_id_fk",
|
||||
"tableFrom": "user_role",
|
||||
"tableTo": "role",
|
||||
"columnsFrom": [
|
||||
"role_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"user_role_user_id_role_id_pk": {
|
||||
"columns": [
|
||||
"user_id",
|
||||
"role_id"
|
||||
],
|
||||
"name": "user_role_user_id_role_id_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"user": {
|
||||
"name": "user",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"emailVerified": {
|
||||
"name": "emailVerified",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_email_unique": {
|
||||
"name": "user_email_unique",
|
||||
"columns": [
|
||||
"email"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"webhook": {
|
||||
"name": "webhook",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"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": {},
|
||||
"foreignKeys": {
|
||||
"webhook_user_id_user_id_fk": {
|
||||
"name": "webhook_user_id_user_id_fk",
|
||||
"tableFrom": "webhook",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
@@ -29,6 +29,20 @@
|
||||
"when": 1734454698466,
|
||||
"tag": "0003_dashing_dust",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 4,
|
||||
"version": "6",
|
||||
"when": 1735229458794,
|
||||
"tag": "0004_panoramic_hedge_knight",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 5,
|
||||
"version": "6",
|
||||
"when": 1735273566991,
|
||||
"tag": "0005_tricky_mathemanic",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
@@ -1,22 +1,47 @@
|
||||
import { auth } from "@/lib/auth"
|
||||
import { NextResponse } from "next/server"
|
||||
import { PERMISSIONS } from "@/lib/permissions"
|
||||
import { checkPermission } from "@/lib/auth"
|
||||
import { Permission } from "@/lib/permissions"
|
||||
|
||||
export async function middleware() {
|
||||
const API_PERMISSIONS: Record<string, Permission> = {
|
||||
'/api/emails': PERMISSIONS.MANAGE_EMAIL,
|
||||
'/api/webhook': PERMISSIONS.MANAGE_WEBHOOK,
|
||||
'/api/roles/promote': PERMISSIONS.PROMOTE_USER,
|
||||
}
|
||||
|
||||
export async function middleware(request: Request) {
|
||||
const session = await auth()
|
||||
const pathname = new URL(request.url).pathname
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json(
|
||||
{ error: "Unauthorized" },
|
||||
{ error: "未授权" },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
for (const [route, permission] of Object.entries(API_PERMISSIONS)) {
|
||||
if (pathname.startsWith(route)) {
|
||||
const hasAccess = await checkPermission(permission)
|
||||
|
||||
if (!hasAccess) {
|
||||
return NextResponse.json(
|
||||
{ error: "权限不足" },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
"/api/emails/:path*",
|
||||
"/api/webhook/:path*",
|
||||
'/api/emails/:path*',
|
||||
'/api/webhook/:path*',
|
||||
'/api/roles/:path*',
|
||||
]
|
||||
}
|
11
types.d.ts
vendored
11
types.d.ts
vendored
@@ -1,5 +1,6 @@
|
||||
/// <reference types="@cloudflare/workers-types" />
|
||||
|
||||
|
||||
declare global {
|
||||
interface CloudflareEnv {
|
||||
DB: D1Database;
|
||||
@@ -8,4 +9,14 @@ declare global {
|
||||
type Env = CloudflareEnv
|
||||
}
|
||||
|
||||
declare module "next-auth" {
|
||||
interface User {
|
||||
roles?: { name: string }[]
|
||||
}
|
||||
|
||||
interface Session {
|
||||
user: User
|
||||
}
|
||||
}
|
||||
|
||||
export type { Env }
|
Reference in New Issue
Block a user