mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-13 10:26:38 +08:00
AccessToken 默认非管理员权限
This commit is contained in:
+7
-1
@@ -3641,7 +3641,7 @@ const docTemplate = `{
|
|||||||
"SessionCookie": []
|
"SessionCookie": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "为当前用户新建一个 API 访问令牌,仅在此接口返回一次明文令牌值,请妥善保存。",
|
"description": "为当前用户新建一个 API 访问令牌,仅在此接口返回一次明文令牌值,请妥善保存。可通过 is_admin 字段赋予令牌管理员权限(仅管理员用户可设置)。",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -4556,6 +4556,9 @@ const docTemplate = `{
|
|||||||
"id": {
|
"id": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"is_admin": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"last_used_at": {
|
"last_used_at": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -5496,6 +5499,9 @@ const docTemplate = `{
|
|||||||
"user.createTokenRequest": {
|
"user.createTokenRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"is_admin": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-1
@@ -3634,7 +3634,7 @@
|
|||||||
"SessionCookie": []
|
"SessionCookie": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "为当前用户新建一个 API 访问令牌,仅在此接口返回一次明文令牌值,请妥善保存。",
|
"description": "为当前用户新建一个 API 访问令牌,仅在此接口返回一次明文令牌值,请妥善保存。可通过 is_admin 字段赋予令牌管理员权限(仅管理员用户可设置)。",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -4549,6 +4549,9 @@
|
|||||||
"id": {
|
"id": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"is_admin": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"last_used_at": {
|
"last_used_at": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -5489,6 +5492,9 @@
|
|||||||
"user.createTokenRequest": {
|
"user.createTokenRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"is_admin": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -220,6 +220,8 @@ definitions:
|
|||||||
type: string
|
type: string
|
||||||
id:
|
id:
|
||||||
type: integer
|
type: integer
|
||||||
|
is_admin:
|
||||||
|
type: boolean
|
||||||
last_used_at:
|
last_used_at:
|
||||||
type: string
|
type: string
|
||||||
masked_token:
|
masked_token:
|
||||||
@@ -861,6 +863,8 @@ definitions:
|
|||||||
type: object
|
type: object
|
||||||
user.createTokenRequest:
|
user.createTokenRequest:
|
||||||
properties:
|
properties:
|
||||||
|
is_admin:
|
||||||
|
type: boolean
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
@@ -3185,7 +3189,7 @@ paths:
|
|||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
description: 为当前用户新建一个 API 访问令牌,仅在此接口返回一次明文令牌值,请妥善保存。
|
description: 为当前用户新建一个 API 访问令牌,仅在此接口返回一次明文令牌值,请妥善保存。可通过 is_admin 字段赋予令牌管理员权限(仅管理员用户可设置)。
|
||||||
parameters:
|
parameters:
|
||||||
- description: 令牌名称
|
- description: 令牌名称
|
||||||
in: body
|
in: body
|
||||||
|
|||||||
@@ -190,8 +190,9 @@ export const apiSections: PolicySection[] = [
|
|||||||
|
|
||||||
<h3 id="3-2-create-token" className="text-base md:text-lg font-semibold text-foreground mt-6 mb-2">3.2 新建访问令牌</h3>
|
<h3 id="3-2-create-token" className="text-base md:text-lg font-semibold text-foreground mt-6 mb-2">3.2 新建访问令牌</h3>
|
||||||
<p><strong>接口:</strong>POST <code className="bg-muted px-1.5 py-0.5 rounded text-xs font-mono">/api/v1/user/access-tokens</code></p>
|
<p><strong>接口:</strong>POST <code className="bg-muted px-1.5 py-0.5 rounded text-xs font-mono">/api/v1/user/access-tokens</code></p>
|
||||||
<p><strong>参数:</strong>JSON Body <code className="bg-muted px-1.5 rounded text-xs font-mono">{`{"name": "token名称"}`}</code></p>
|
<p><strong>参数:</strong>JSON Body <code className="bg-muted px-1.5 rounded text-xs font-mono">{`{"name": "token名称", "is_admin": false}`}</code></p>
|
||||||
<p><strong>说明:</strong>生成一个全新访问令牌。返回体中包含一次性明文 Token,切勿遗失。</p>
|
<p><strong>说明:</strong>生成一个全新访问令牌。返回体中包含一次性明文 Token,切勿遗失。</p>
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground"><code className="bg-muted px-1 rounded">is_admin</code>(可选,默认 <code className="bg-muted px-1 rounded">false</code>):是否赋予令牌管理员权限,仅管理员用户可设置。非管理员令牌无法访问 <code className="bg-muted px-1 rounded">/admin/**</code> 端点。</p>
|
||||||
<p className="mt-2">成功返回样例:</p>
|
<p className="mt-2">成功返回样例:</p>
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
code={`{
|
code={`{
|
||||||
@@ -203,6 +204,7 @@ export const apiSections: PolicySection[] = [
|
|||||||
"user_id": 1,
|
"user_id": 1,
|
||||||
"name": "my-dev-key",
|
"name": "my-dev-key",
|
||||||
"masked_token": "at_628d...29c9",
|
"masked_token": "at_628d...29c9",
|
||||||
|
"is_admin": false,
|
||||||
"last_used_at": null,
|
"last_used_at": null,
|
||||||
"created_at": "2026-06-07T21:30:00+08:00"
|
"created_at": "2026-06-07T21:30:00+08:00"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import * as React from "react"
|
|||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query"
|
import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query"
|
||||||
import {motion} from "motion/react"
|
import {motion} from "motion/react"
|
||||||
import {AlertTriangle, Check, Copy, Info, Key, Loader2, Plus, RefreshCw, Trash2} from "lucide-react"
|
import {AlertTriangle, Check, Copy, Info, Key, Loader2, Plus, RefreshCw, Shield, Trash2} from "lucide-react"
|
||||||
|
|
||||||
import {Button} from "@/components/ui/button"
|
import {Button} from "@/components/ui/button"
|
||||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card"
|
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card"
|
||||||
import {Input} from "@/components/ui/input"
|
import {Input} from "@/components/ui/input"
|
||||||
import {Label} from "@/components/ui/label"
|
import {Label} from "@/components/ui/label"
|
||||||
|
import {Badge} from "@/components/ui/badge"
|
||||||
|
import {Switch} from "@/components/ui/switch"
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -28,13 +30,16 @@ import {
|
|||||||
} from "@/components/ui/breadcrumb"
|
} from "@/components/ui/breadcrumb"
|
||||||
import {UserService} from "@/lib/services"
|
import {UserService} from "@/lib/services"
|
||||||
import type {CreateTokenResponse} from "@/lib/services/user"
|
import type {CreateTokenResponse} from "@/lib/services/user"
|
||||||
|
import {useAuth} from "@/components/providers/auth-provider"
|
||||||
import {toast} from "sonner"
|
import {toast} from "sonner"
|
||||||
|
|
||||||
export function AccessTokenMain() {
|
export function AccessTokenMain() {
|
||||||
|
const { user } = useAuth()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const [createDialogOpen, setCreateDialogOpen] = React.useState(false)
|
const [createDialogOpen, setCreateDialogOpen] = React.useState(false)
|
||||||
const [viewDialogOpen, setViewDialogOpen] = React.useState(false)
|
const [viewDialogOpen, setViewDialogOpen] = React.useState(false)
|
||||||
const [tokenName, setTokenName] = React.useState("")
|
const [tokenName, setTokenName] = React.useState("")
|
||||||
|
const [tokenIsAdmin, setTokenIsAdmin] = React.useState(false)
|
||||||
const [copiedId, setCopiedId] = React.useState<number | null>(null)
|
const [copiedId, setCopiedId] = React.useState<number | null>(null)
|
||||||
const [newCreatedToken, setNewCreatedToken] = React.useState<CreateTokenResponse | null>(null)
|
const [newCreatedToken, setNewCreatedToken] = React.useState<CreateTokenResponse | null>(null)
|
||||||
|
|
||||||
@@ -46,10 +51,11 @@ export function AccessTokenMain() {
|
|||||||
|
|
||||||
// 创建 Token
|
// 创建 Token
|
||||||
const createTokenMutation = useMutation({
|
const createTokenMutation = useMutation({
|
||||||
mutationFn: (name: string) => UserService.createAccessToken(name),
|
mutationFn: ({ name, isAdmin }: { name: string; isAdmin: boolean }) => UserService.createAccessToken(name, isAdmin),
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
setNewCreatedToken(data)
|
setNewCreatedToken(data)
|
||||||
setTokenName("")
|
setTokenName("")
|
||||||
|
setTokenIsAdmin(false)
|
||||||
setCreateDialogOpen(false)
|
setCreateDialogOpen(false)
|
||||||
setViewDialogOpen(true)
|
setViewDialogOpen(true)
|
||||||
void queryClient.invalidateQueries({ queryKey: ["user", "access-tokens"] })
|
void queryClient.invalidateQueries({ queryKey: ["user", "access-tokens"] })
|
||||||
@@ -92,7 +98,7 @@ export function AccessTokenMain() {
|
|||||||
toast.error("请输入令牌名称")
|
toast.error("请输入令牌名称")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
createTokenMutation.mutate(tokenName.trim())
|
createTokenMutation.mutate({ name: tokenName.trim(), isAdmin: tokenIsAdmin })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteToken = (id: number, name: string) => {
|
const handleDeleteToken = (id: number, name: string) => {
|
||||||
@@ -200,6 +206,12 @@ export function AccessTokenMain() {
|
|||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-semibold text-sm text-foreground">{token.name}</span>
|
<span className="font-semibold text-sm text-foreground">{token.name}</span>
|
||||||
|
{token.is_admin && (
|
||||||
|
<Badge variant="outline" className="text-[10px] px-1.5 py-0 h-4 border-rose-500/40 text-rose-500 bg-rose-500/5 font-semibold">
|
||||||
|
<Shield className="size-2.5 mr-0.5" />
|
||||||
|
管理员
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1 text-xs text-muted-foreground">
|
<div className="flex flex-col gap-1 text-xs text-muted-foreground">
|
||||||
<div className="font-mono bg-muted/30 px-2 py-0.5 rounded border border-border/50 w-fit select-all">
|
<div className="font-mono bg-muted/30 px-2 py-0.5 rounded border border-border/50 w-fit select-all">
|
||||||
@@ -293,6 +305,25 @@ export function AccessTokenMain() {
|
|||||||
className="rounded-xl border border-dashed focus:border-indigo-500 focus:ring-0 focus-visible:ring-0"
|
className="rounded-xl border border-dashed focus:border-indigo-500 focus:ring-0 focus-visible:ring-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{user?.is_admin && (
|
||||||
|
<div className="flex items-center justify-between rounded-xl border border-dashed p-3 bg-muted/5">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="token-admin" className="text-xs font-semibold flex items-center gap-1.5">
|
||||||
|
<Shield className="size-3.5 text-rose-500" />
|
||||||
|
管理员权限
|
||||||
|
</Label>
|
||||||
|
<p className="text-[11px] text-muted-foreground leading-normal">
|
||||||
|
开启后此令牌可访问 /admin/** 管理端点,默认关闭
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="token-admin"
|
||||||
|
checked={tokenIsAdmin}
|
||||||
|
onCheckedChange={setTokenIsAdmin}
|
||||||
|
disabled={createTokenMutation.isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter className="gap-2">
|
<DialogFooter className="gap-2">
|
||||||
<Button
|
<Button
|
||||||
@@ -336,9 +367,6 @@ export function AccessTokenMain() {
|
|||||||
<Check className="size-5 text-emerald-500 border border-emerald-500 rounded-full p-0.5" />
|
<Check className="size-5 text-emerald-500 border border-emerald-500 rounded-full p-0.5" />
|
||||||
令牌密钥已就绪
|
令牌密钥已就绪
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="text-xs text-muted-foreground">
|
|
||||||
这是您唯一一次能够查看此访问令牌明文密钥的机会。请立即将其复制并安全地保存。
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{newCreatedToken && (
|
{newCreatedToken && (
|
||||||
@@ -369,7 +397,7 @@ export function AccessTokenMain() {
|
|||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<span className="font-bold">重要提示:</span>
|
<span className="font-bold">重要提示:</span>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
为了系统安全性,数据库中仅存储令牌的 Hash 摘要值,系统本身无法为您找回此明文密钥。离开此窗口后,您将再也无法查看到它的明文值。
|
这是您唯一一次能够查看此访问令牌明文密钥的机会。请立即将其复制并安全地保存。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -383,7 +411,7 @@ export function AccessTokenMain() {
|
|||||||
setNewCreatedToken(null)
|
setNewCreatedToken(null)
|
||||||
setViewDialogOpen(false)
|
setViewDialogOpen(false)
|
||||||
}}
|
}}
|
||||||
className="bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl text-xs h-9 w-full"
|
className="rounded-xl w-full"
|
||||||
>
|
>
|
||||||
我已经复制并妥善保存
|
我已经复制并妥善保存
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export interface AccessToken {
|
|||||||
user_id: number;
|
user_id: number;
|
||||||
name: string;
|
name: string;
|
||||||
masked_token: string;
|
masked_token: string;
|
||||||
|
is_admin: boolean;
|
||||||
last_used_at?: string;
|
last_used_at?: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
@@ -32,9 +33,10 @@ export class UserService extends BaseService {
|
|||||||
/**
|
/**
|
||||||
* 创建一个新的 AccessToken
|
* 创建一个新的 AccessToken
|
||||||
* @param name - 令牌名称
|
* @param name - 令牌名称
|
||||||
|
* @param isAdmin - 是否赋予管理员权限(默认 false)
|
||||||
*/
|
*/
|
||||||
static async createAccessToken(name: string): Promise<CreateTokenResponse> {
|
static async createAccessToken(name: string, isAdmin = false): Promise<CreateTokenResponse> {
|
||||||
return this.post<CreateTokenResponse>('/access-tokens', { name });
|
return this.post<CreateTokenResponse>('/access-tokens', { name, is_admin: isAdmin });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ package admin
|
|||||||
// 管理后台错误消息常量
|
// 管理后台错误消息常量
|
||||||
const (
|
const (
|
||||||
AdminRequired = "未经授权访问"
|
AdminRequired = "未经授权访问"
|
||||||
|
TokenAdminRequired = "该访问令牌没有管理员权限,无法访问管理端点" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||||
InvalidAuthSourceID = "认证源 ID 无效"
|
InvalidAuthSourceID = "认证源 ID 无效"
|
||||||
InvalidCursorParam = "无效的 cursor 参数"
|
InvalidCursorParam = "无效的 cursor 参数"
|
||||||
InvalidTaskExecutionID = "无效的任务执行记录 ID"
|
InvalidTaskExecutionID = "无效的任务执行记录 ID"
|
||||||
|
|||||||
@@ -25,6 +25,15 @@ func LoginAdminRequired() gin.HandlerFunc {
|
|||||||
|
|
||||||
user, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
user, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||||
|
|
||||||
|
// 如果是通过 Access Token 鉴权,需要检查令牌本身是否具有管理员权限
|
||||||
|
if tokenAuth, _ := util.GetFromContext[bool](c, oauth.TokenAuthKey); tokenAuth {
|
||||||
|
tokenAdmin, _ := util.GetFromContext[bool](c, oauth.TokenAdminKey)
|
||||||
|
if !tokenAdmin {
|
||||||
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error_msg": TokenAdminRequired, "data": nil})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !user.IsAdmin {
|
if !user.IsAdmin {
|
||||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error_msg": AdminRequired, "data": nil})
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error_msg": AdminRequired, "data": nil})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ const (
|
|||||||
UserNameKey = "username"
|
UserNameKey = "username"
|
||||||
UserIDKey = "user_id"
|
UserIDKey = "user_id"
|
||||||
UserObjKey = "user_obj"
|
UserObjKey = "user_obj"
|
||||||
|
TokenAuthKey = "token_auth" // 标记当前请求是否通过 Access Token 鉴权
|
||||||
|
TokenAdminKey = "token_admin" // Access Token 本身是否具有管理员权限
|
||||||
PendingOAuthSourceIDKey = "pending_oauth_source_id"
|
PendingOAuthSourceIDKey = "pending_oauth_source_id"
|
||||||
PendingOAuthExternalIDKey = "pending_oauth_external_id"
|
PendingOAuthExternalIDKey = "pending_oauth_external_id"
|
||||||
PendingOAuthExternalUsernameKey = "pending_oauth_external_username"
|
PendingOAuthExternalUsernameKey = "pending_oauth_external_username"
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ func LoginRequired() gin.HandlerFunc {
|
|||||||
|
|
||||||
var user model.User
|
var user model.User
|
||||||
var authenticated bool
|
var authenticated bool
|
||||||
|
var tokenAuth bool
|
||||||
|
var tokenAdmin bool
|
||||||
|
|
||||||
if tokenStr != "" {
|
if tokenStr != "" {
|
||||||
tokenHash := model.HashToken(tokenStr)
|
tokenHash := model.HashToken(tokenStr)
|
||||||
@@ -52,6 +54,8 @@ func LoginRequired() gin.HandlerFunc {
|
|||||||
if err := db.DB(ctx).Where("token_hash = ?", tokenHash).First(&tokenRecord).Error; err == nil {
|
if err := db.DB(ctx).Where("token_hash = ?", tokenHash).First(&tokenRecord).Error; err == nil {
|
||||||
if err := db.DB(ctx).Where("id = ? AND is_active = ?", tokenRecord.UserID, true).First(&user).Error; err == nil {
|
if err := db.DB(ctx).Where("id = ? AND is_active = ?", tokenRecord.UserID, true).First(&user).Error; err == nil {
|
||||||
authenticated = true
|
authenticated = true
|
||||||
|
tokenAuth = true
|
||||||
|
tokenAdmin = tokenRecord.IsAdmin
|
||||||
// update token last used time
|
// update token last used time
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
db.DB(ctx).Model(&tokenRecord).Update("last_used_at", &now)
|
db.DB(ctx).Model(&tokenRecord).Update("last_used_at", &now)
|
||||||
@@ -80,6 +84,8 @@ func LoginRequired() gin.HandlerFunc {
|
|||||||
|
|
||||||
// set user info
|
// set user info
|
||||||
util.SetToContext(c, UserObjKey, &user)
|
util.SetToContext(c, UserObjKey, &user)
|
||||||
|
util.SetToContext(c, TokenAuthKey, tokenAuth)
|
||||||
|
util.SetToContext(c, TokenAdminKey, tokenAdmin)
|
||||||
|
|
||||||
// next
|
// next
|
||||||
c.Next()
|
c.Next()
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type createTokenRequest struct {
|
type createTokenRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
IsAdmin bool `json:"is_admin"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type tokenResponse struct {
|
type tokenResponse struct {
|
||||||
@@ -51,7 +52,7 @@ func ListAccessTokens(c *gin.Context) {
|
|||||||
|
|
||||||
// CreateAccessToken 创建一个新的 AccessToken
|
// CreateAccessToken 创建一个新的 AccessToken
|
||||||
// @Summary 创建一个新的 AccessToken
|
// @Summary 创建一个新的 AccessToken
|
||||||
// @Description 为当前用户新建一个 API 访问令牌,仅在此接口返回一次明文令牌值,请妥善保存。
|
// @Description 为当前用户新建一个 API 访问令牌,仅在此接口返回一次明文令牌值,请妥善保存。可通过 is_admin 字段赋予令牌管理员权限(仅管理员用户可设置)。
|
||||||
// @Tags user
|
// @Tags user
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
@@ -76,6 +77,12 @@ func CreateAccessToken(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 只有管理员才能创建具有管理员权限的令牌
|
||||||
|
if req.IsAdmin && !currUser.IsAdmin {
|
||||||
|
c.JSON(http.StatusOK, util.Err(errAdminTokenRequiresAdmin))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 检查最大限制(基于 ConfigKeyMaxAPIKeysPerUser 配置,默认值为 5)
|
// 检查最大限制(基于 ConfigKeyMaxAPIKeysPerUser 配置,默认值为 5)
|
||||||
maxLimit := 5
|
maxLimit := 5
|
||||||
if val, err := model.GetIntByKey(ctx, model.ConfigKeyMaxAPIKeysPerUser); err == nil {
|
if val, err := model.GetIntByKey(ctx, model.ConfigKeyMaxAPIKeysPerUser); err == nil {
|
||||||
@@ -108,6 +115,7 @@ func CreateAccessToken(c *gin.Context) {
|
|||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
TokenHash: tokenHash,
|
TokenHash: tokenHash,
|
||||||
MaskedToken: maskedToken,
|
MaskedToken: maskedToken,
|
||||||
|
IsAdmin: req.IsAdmin,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := db.DB(ctx).Create(&tokenRecord).Error; err != nil {
|
if err := db.DB(ctx).Create(&tokenRecord).Error; err != nil {
|
||||||
|
|||||||
@@ -31,11 +31,12 @@ const (
|
|||||||
errRenderEmailTemplateFailed = "渲染验证邮件模板失败:%w"
|
errRenderEmailTemplateFailed = "渲染验证邮件模板失败:%w"
|
||||||
errGenerateEmailCodeFailed = "生成验证码失败,请重试"
|
errGenerateEmailCodeFailed = "生成验证码失败,请重试"
|
||||||
errDispatchEmailTaskFailed = "投递验证邮件发送任务失败,请重试"
|
errDispatchEmailTaskFailed = "投递验证邮件发送任务失败,请重试"
|
||||||
errTokenNameRequired = "令牌名称不能为空" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
errTokenNameRequired = "令牌名称不能为空" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||||
errAccessTokenLimitReached = "已达到访问令牌最大创建数量限制" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
errAccessTokenLimitReached = "已达到访问令牌最大创建数量限制" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||||
errGenerateTokenFailed = "生成令牌失败" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
errGenerateTokenFailed = "生成令牌失败" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||||
errInvalidTokenID = "无效的令牌ID" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
errInvalidTokenID = "无效的令牌ID" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||||
errTokenNotFoundOrForbidden = "令牌不存在或无权操作" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
errTokenNotFoundOrForbidden = "令牌不存在或无权操作" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||||
|
errAdminTokenRequiresAdmin = "只有管理员才能创建具有管理员权限的令牌" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
|
||||||
errTaskPayloadRequired = "任务参数不能为空"
|
errTaskPayloadRequired = "任务参数不能为空"
|
||||||
errInvalidJSONFormat = "无效的 JSON 格式: %w"
|
errInvalidJSONFormat = "无效的 JSON 格式: %w"
|
||||||
errEmailTaskFieldsRequired = "to、subject、body 不能为空"
|
errEmailTaskFieldsRequired = "to、subject、body 不能为空"
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- +goose Up
|
||||||
|
ALTER TABLE access_tokens ADD COLUMN is_admin BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
ALTER TABLE access_tokens DROP COLUMN IF EXISTS is_admin;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- +goose Up
|
||||||
|
ALTER TABLE access_tokens ADD COLUMN is_admin BOOLEAN NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
ALTER TABLE access_tokens DROP COLUMN is_admin;
|
||||||
@@ -25,6 +25,7 @@ type AccessToken struct {
|
|||||||
Name string `json:"name" gorm:"size:128;not null"`
|
Name string `json:"name" gorm:"size:128;not null"`
|
||||||
TokenHash string `json:"-" gorm:"size:64;uniqueIndex;not null"`
|
TokenHash string `json:"-" gorm:"size:64;uniqueIndex;not null"`
|
||||||
MaskedToken string `json:"masked_token" gorm:"size:64;not null"`
|
MaskedToken string `json:"masked_token" gorm:"size:64;not null"`
|
||||||
|
IsAdmin bool `json:"is_admin" gorm:"default:false"`
|
||||||
LastUsedAt *time.Time `json:"last_used_at"`
|
LastUsedAt *time.Time `json:"last_used_at"`
|
||||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
|
||||||
|
|||||||
Reference in New Issue
Block a user