diff --git a/.github/workflows/build-latest-backend.yaml b/.github/workflows/build-latest-backend.yaml
index 61ef7b8..6efcde7 100644
--- a/.github/workflows/build-latest-backend.yaml
+++ b/.github/workflows/build-latest-backend.yaml
@@ -56,5 +56,5 @@ jobs:
--header 'Authorization: Bearer ${{ secrets.ONEBOT_V11_TOKEN }}' \
--data '{
"group_id": ${{ secrets.ONEBOT_V11_GROUP_ID }},
- "message": "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 构建完成。"
+ "message": "Campux 构建完成。"
}'
\ No newline at end of file
diff --git a/backend/config/config.go b/backend/config/config.go
index 02b5e17..85e1a2b 100644
--- a/backend/config/config.go
+++ b/backend/config/config.go
@@ -2,6 +2,8 @@ package config
import (
"github.com/spf13/viper"
+
+ "github.com/google/uuid"
)
type Config struct {
@@ -12,9 +14,14 @@ func SetDefault() {
viper.SetDefault("backend.port", "8080")
// jwt
- viper.SetDefault("auth.jwt.secret", "campux")
+ viper.SetDefault("auth.jwt.secret", uuid.New().String())
viper.SetDefault("auth.jwt.expire", 3600*6)
+ // oauth2
+ viper.SetDefault("oauth2.server.code_secret", uuid.New().String())
+ viper.SetDefault("oauth2.server.access_secret", uuid.New().String())
+ viper.SetDefault("oauth2.server.ak_expire", 3600*24*14)
+
// 服务token
viper.SetDefault("service.token", "campux")
viper.SetDefault("service.bots", []int64{123456789})
@@ -38,6 +45,7 @@ func SetDefault() {
viper.SetDefault("mq.redis.stream.new_post", "campux_new_post")
viper.SetDefault("mq.redis.stream.post_cancel", "campux_post_cancel")
viper.SetDefault("mq.redis.hash.post_publish_status", "campux_post_publish_status")
+ viper.SetDefault("mq.redis.prefix.oauth2_code", "campux_oauth2_code")
}
diff --git a/backend/controller/admapi.go b/backend/controller/admapi.go
new file mode 100644
index 0000000..e9e028f
--- /dev/null
+++ b/backend/controller/admapi.go
@@ -0,0 +1,119 @@
+package controller
+
+import (
+ "github.com/RockChinQ/Campux/backend/database"
+ "github.com/RockChinQ/Campux/backend/service"
+ "github.com/gin-gonic/gin"
+)
+
+type AdminRouter struct {
+ APIRouter
+ AdminService service.AdminService
+}
+
+func NewAdminRouter(rg *gin.RouterGroup, as service.AdminService) *AdminRouter {
+ ar := &AdminRouter{
+ AdminService: as,
+ }
+
+ group := rg.Group("/admin")
+
+ // bind routes
+ group.POST("/add-oauth2-app", ar.AddOAuth2App)
+ group.GET("/get-oauth2-apps", ar.GetOAuth2AppList)
+ group.DELETE("/del-oauth2-app/:id", ar.DeleteOAuth2App)
+
+ return ar
+}
+
+// 添加一个OAuth2应用
+func (ar *AdminRouter) AddOAuth2App(c *gin.Context) {
+
+ uin, err := ar.Auth(c, UserOnly)
+
+ if err != nil {
+ return
+ }
+
+ if !ar.AdminService.CheckUserGroup(uin, []database.UserGroup{
+ database.USER_GROUP_ADMIN,
+ }) {
+ ar.StatusCode(c, 401, "权限不足")
+ return
+ }
+
+ // 取body的json里的appname
+ var body OAuth2AppCreateBody
+
+ if err := c.ShouldBindJSON(&body); err != nil {
+ ar.Fail(c, 1, err.Error())
+ return
+ }
+
+ // 创建OAuth2应用
+ app, err := ar.AdminService.AddOAuth2App(body.Name, body.Emoji)
+
+ if err != nil {
+ ar.Fail(c, 2, err.Error())
+ return
+ }
+
+ ar.Success(c, app)
+}
+
+// 获取 OAuth2 应用列表
+func (ar *AdminRouter) GetOAuth2AppList(c *gin.Context) {
+ uin, err := ar.Auth(c, UserOnly)
+
+ if err != nil {
+ return
+ }
+
+ if !ar.AdminService.CheckUserGroup(uin, []database.UserGroup{
+ database.USER_GROUP_ADMIN,
+ }) {
+ ar.StatusCode(c, 401, "权限不足")
+ return
+ }
+
+ // 获取OAuth2应用列表
+ list, err := ar.AdminService.GetOAuth2Apps()
+
+ if err != nil {
+ ar.Fail(c, 1, err.Error())
+ return
+ }
+
+ ar.Success(c, gin.H{
+ "list": list,
+ })
+}
+
+// 删除一个OAuth2应用
+func (ar *AdminRouter) DeleteOAuth2App(c *gin.Context) {
+ uin, err := ar.Auth(c, UserOnly)
+
+ if err != nil {
+ return
+ }
+
+ if !ar.AdminService.CheckUserGroup(uin, []database.UserGroup{
+ database.USER_GROUP_ADMIN,
+ }) {
+ ar.StatusCode(c, 401, "权限不足")
+ return
+ }
+
+ // 取路由参数
+ appID := c.Param("id")
+
+ // 删除OAuth2应用
+ err = ar.AdminService.DeleteOAuth2App(appID)
+
+ if err != nil {
+ ar.Fail(c, 1, err.Error())
+ return
+ }
+
+ ar.Success(c, nil)
+}
diff --git a/backend/controller/api.go b/backend/controller/api.go
index 6b48eda..12fc652 100644
--- a/backend/controller/api.go
+++ b/backend/controller/api.go
@@ -21,6 +21,8 @@ func NewApiController(
as service.AccountService,
ps service.PostService,
ms service.MiscService,
+ ads service.AdminService,
+ oas service.OAuth2Service,
) *APIController {
r := gin.Default()
@@ -63,6 +65,8 @@ func NewApiController(
NewAccountRouter(rg, as)
NewPostRouter(rg, ps, as)
NewMiscRouter(rg, ms)
+ NewAdminRouter(rg, ads)
+ NewOAuth2Router(rg, oas)
return &APIController{
R: r,
@@ -165,7 +169,7 @@ func (ar *APIRouter) GetUin(c *gin.Context) (int64, error) {
// 删除Bearer
jwtToken = jwtToken[7:]
- uin, err := util.ParseJWTToken(jwtToken)
+ uin, err := util.ParseUserJWTToken(jwtToken)
return uin, err
} else {
@@ -176,7 +180,7 @@ func (ar *APIRouter) GetUin(c *gin.Context) (int64, error) {
return -1, err
}
- uin, err := util.ParseJWTToken(jwtToken)
+ uin, err := util.ParseUserJWTToken(jwtToken)
return uin, err
}
diff --git a/backend/controller/dto.go b/backend/controller/dto.go
index eb8d7f8..7ab4b4f 100644
--- a/backend/controller/dto.go
+++ b/backend/controller/dto.go
@@ -151,3 +151,27 @@ type GetBanListBody struct {
// 时间排序
TimeOrder *int `json:"time_order" binding:"required"`
}
+
+type OAuth2AppCreateBody struct {
+ // 名称
+ Name string `json:"name" binding:"required"`
+
+ // emoji
+ Emoji string `json:"emoji" binding:"required"`
+}
+
+type OAuth2AuthorizeBody struct {
+ // 应用id
+ ClientID string `json:"client_id" binding:"required"`
+}
+
+type OAuth2GetAccessTokenBody struct {
+ // 应用id
+ ClientID string `json:"client_id" binding:"required"`
+
+ // 应用密钥
+ ClientSecret string `json:"client_secret" binding:"required"`
+
+ // 授权码
+ Code string `json:"code" binding:"required"`
+}
diff --git a/backend/controller/oauthapi.go b/backend/controller/oauthapi.go
new file mode 100644
index 0000000..5f10673
--- /dev/null
+++ b/backend/controller/oauthapi.go
@@ -0,0 +1,110 @@
+package controller
+
+import (
+ "github.com/RockChinQ/Campux/backend/service"
+ "github.com/gin-gonic/gin"
+)
+
+type OAuth2Router struct {
+ APIRouter
+ OAuth2Service service.OAuth2Service
+}
+
+func NewOAuth2Router(rg *gin.RouterGroup, oas service.OAuth2Service) *OAuth2Router {
+
+ oar := &OAuth2Router{
+ OAuth2Service: oas,
+ }
+
+ group := rg.Group("/oauth2")
+
+ group.GET("/get-app-info", oar.GetOAuth2AppInfo)
+ group.GET("/authorize", oar.Authorize)
+ group.POST("/get-access-token", oar.GetAccessToken)
+
+ return oar
+}
+
+func (oar *OAuth2Router) GetOAuth2AppInfo(c *gin.Context) {
+ clientID := c.Query("client_id")
+
+ app, err := oar.OAuth2Service.GetOAuth2AppByClientID(clientID)
+
+ if err != nil {
+ oar.Fail(c, 1, err.Error())
+ return
+ }
+
+ if app == nil {
+ oar.Fail(c, 2, "此应用未注册")
+ return
+ }
+
+ oar.Success(c, gin.H{
+ "client_id": app.ClientID,
+ "name": app.Name,
+ })
+}
+
+func (oar *OAuth2Router) Authorize(c *gin.Context) {
+
+ uin, err := oar.Auth(c, UserOnly)
+
+ if err != nil {
+ return
+ }
+
+ clientID := c.Query("client_id")
+
+ if clientID == "" {
+ oar.Fail(c, 1, "未提供 client_id")
+ return
+ }
+
+ // 检查是否存在这个应用
+ app, err := oar.OAuth2Service.GetOAuth2AppByClientID(clientID)
+
+ if err != nil {
+ oar.Fail(c, 2, err.Error())
+ return
+ }
+
+ if app == nil {
+ oar.Fail(c, 3, "此应用未注册")
+ return
+ }
+
+ // 计算code
+ code, err := oar.OAuth2Service.GenerateCode(app.ClientID, uin)
+
+ if err != nil {
+ oar.Fail(c, 4, err.Error())
+ return
+ }
+
+ oar.Success(c, gin.H{
+ "code": code,
+ })
+}
+
+func (oar *OAuth2Router) GetAccessToken(c *gin.Context) {
+
+ var body OAuth2GetAccessTokenBody
+
+ if err := c.ShouldBindJSON(&body); err != nil {
+ oar.Fail(c, 1, err.Error())
+ return
+ }
+
+ // 检查code
+ ak, err := oar.OAuth2Service.GetAccessToken(body.ClientID, body.ClientSecret, body.Code)
+
+ if err != nil {
+ oar.Fail(c, 2, err.Error())
+ return
+ }
+
+ oar.Success(c, gin.H{
+ "access_token": ak,
+ })
+}
diff --git a/backend/core/app.go b/backend/core/app.go
index a79120e..7c92571 100644
--- a/backend/core/app.go
+++ b/backend/core/app.go
@@ -27,6 +27,8 @@ func NewApplication() *Application {
as := service.NewAccountService(*db)
ps := service.NewPostService(*db, *fs, *msq)
ms := service.NewMiscService(*db)
+ ads := service.NewAdminService(*db)
+ oas := service.NewOAuth2Service(*db, *msq)
err := ScheduleRoutines(*db, *msq)
if err != nil {
@@ -34,7 +36,7 @@ func NewApplication() *Application {
}
return &Application{
- API: controller.NewApiController(*as, *ps, *ms),
+ API: controller.NewApiController(*as, *ps, *ms, *ads, *oas),
}
}
diff --git a/backend/database/mongo.go b/backend/database/mongo.go
index c611674..6d13db1 100644
--- a/backend/database/mongo.go
+++ b/backend/database/mongo.go
@@ -18,6 +18,7 @@ const (
POST_VERBOSE_COLLECTION = "post_verbose"
METADATA_COLLECTION = "metadata"
BAN_LIST_COLLECTION = "ban_list"
+ OAUTH_APP_COLLECTION = "oauth_app"
)
type Metadata struct {
@@ -563,3 +564,62 @@ func (m *MongoDBManager) GetMetadata(key string) (string, error) {
}
return meta.Value, nil
}
+
+func (m *MongoDBManager) AddOAuth2App(app *OAuthAppPO) error {
+ _, err := m.Client.Database(viper.GetString("database.mongo.db")).Collection(OAUTH_APP_COLLECTION).InsertOne(context.TODO(), app)
+ return err
+}
+
+func (m *MongoDBManager) GetOAuth2App(clientID string) (*OAuthAppPO, error) {
+ var app OAuthAppPO
+ err := m.Client.Database(viper.GetString("database.mongo.db")).Collection(OAUTH_APP_COLLECTION).FindOne(
+ context.TODO(),
+ bson.M{"client_id": clientID},
+ ).Decode(&app)
+ if err != nil {
+ if err == mongo.ErrNoDocuments {
+ return nil, nil
+ }
+ return nil, err
+ }
+ return &app, nil
+}
+
+func (m *MongoDBManager) GetOAuth2AppByName(name string) (*OAuthAppPO, error) {
+ // 若不存在,返回 nil, nil
+ var app OAuthAppPO
+
+ err := m.Client.Database(viper.GetString("database.mongo.db")).Collection(OAUTH_APP_COLLECTION).FindOne(
+ context.TODO(),
+ bson.M{"name": name},
+ ).Decode(&app)
+ if err != nil {
+ if err == mongo.ErrNoDocuments {
+ return nil, nil
+ }
+ return nil, err
+ }
+ return &app, nil
+}
+
+// list
+func (m *MongoDBManager) GetOAuth2Apps() ([]OAuthAppPO, error) {
+ var apps []OAuthAppPO
+ cursor, err := m.Client.Database(viper.GetString("database.mongo.db")).Collection(OAUTH_APP_COLLECTION).Find(context.TODO(), bson.M{})
+ if err != nil {
+ return nil, err
+ }
+ defer cursor.Close(context.Background())
+
+ err = cursor.All(context.Background(), &apps)
+ if err != nil {
+ return nil, err
+ }
+
+ return apps, nil
+}
+
+func (m *MongoDBManager) DeleteOAuth2App(clientID string) error {
+ _, err := m.Client.Database(viper.GetString("database.mongo.db")).Collection(OAUTH_APP_COLLECTION).DeleteOne(context.TODO(), bson.M{"client_id": clientID})
+ return err
+}
diff --git a/backend/database/po.go b/backend/database/po.go
index 5b5d6b5..5502982 100644
--- a/backend/database/po.go
+++ b/backend/database/po.go
@@ -84,3 +84,11 @@ const (
REVIEW_OPTION_APPROVE ReviewOption = "approve"
REVIEW_OPTION_REJECT ReviewOption = "reject"
)
+
+type OAuthAppPO struct {
+ Name string `json:"name" bson:"name"` // 应用名称
+ Emoji string `json:"emoji" bson:"emoji"` // Emoji
+ ClientID string `json:"client_id" bson:"client_id"` // 客户端ID
+ ClientSecret string `json:"client_secret" bson:"client_secret"` // 客户端密钥
+ CreatedAt time.Time `json:"created_at" bson:"created_at"` // CST时间
+}
diff --git a/backend/mq/redis.go b/backend/mq/redis.go
index a5f2929..2b5150a 100644
--- a/backend/mq/redis.go
+++ b/backend/mq/redis.go
@@ -3,6 +3,7 @@ package mq
import (
"context"
"strconv"
+ "time"
"github.com/redis/go-redis/v9"
"github.com/spf13/viper"
@@ -109,3 +110,13 @@ func (r *RedisStreamMQ) DeletePostPublishStatus(postID int) error {
_, err := r.Client.Del(context.Background(), viper.GetString("mq.redis.hash.post_publish_status")+strconv.Itoa(postID)).Result()
return err
}
+
+// 存储oauth2_code和uin对应关系 十分钟过期
+func (r *RedisStreamMQ) SetOauth2Code(code string, uin int64) error {
+ return r.Client.Set(context.Background(), viper.GetString("mq.redis.prefix.oauth2_code")+code, uin, 60*10*time.Second).Err()
+}
+
+// 获取oauth2_code对应的uin
+func (r *RedisStreamMQ) GetOauth2Uin(code string) (int64, error) {
+ return r.Client.Get(context.Background(), viper.GetString("mq.redis.prefix.oauth2_code")+code).Int64()
+}
diff --git a/backend/service/account.go b/backend/service/account.go
index 7268c64..45ee01d 100644
--- a/backend/service/account.go
+++ b/backend/service/account.go
@@ -67,7 +67,7 @@ func (as *AccountService) CheckAccount(uin int64, pwd string) (string, error) {
return "", ErrPasswordIncorrect
}
- jwt, err := util.GenerateJWTToken(uin)
+ jwt, err := util.GenerateUserJWTToken(uin)
return jwt, err
}
diff --git a/backend/service/admin.go b/backend/service/admin.go
new file mode 100644
index 0000000..88ac97f
--- /dev/null
+++ b/backend/service/admin.go
@@ -0,0 +1,52 @@
+package service
+
+import (
+ "github.com/RockChinQ/Campux/backend/database"
+ "github.com/RockChinQ/Campux/backend/util"
+ "github.com/google/uuid"
+)
+
+type AdminService struct {
+ CommonService
+}
+
+func NewAdminService(db database.MongoDBManager) *AdminService {
+ return &AdminService{
+ CommonService: CommonService{
+ DB: db,
+ },
+ }
+}
+
+func (as *AdminService) AddOAuth2App(name, emoji string) (*database.OAuthAppPO, error) {
+ check, err := as.DB.GetOAuth2AppByName(name)
+
+ if err != nil {
+ return nil, err
+ }
+
+ if check != nil {
+ return nil, ErrOAuth2AppAlreadyExist
+ }
+
+ app := &database.OAuthAppPO{
+ Name: name,
+ Emoji: emoji,
+ ClientID: util.RandomString(16),
+ ClientSecret: uuid.New().String(),
+ CreatedAt: util.GetCSTTime(),
+ }
+
+ err = as.DB.AddOAuth2App(app)
+
+ return app, err
+}
+
+func (as *AdminService) GetOAuth2Apps() ([]database.OAuthAppPO, error) {
+ return as.DB.GetOAuth2Apps()
+}
+
+// delete
+func (as *AdminService) DeleteOAuth2App(appID string) error {
+ return as.DB.DeleteOAuth2App(appID)
+}
diff --git a/backend/service/errors.go b/backend/service/errors.go
index 4bf8c6c..5232843 100644
--- a/backend/service/errors.go
+++ b/backend/service/errors.go
@@ -13,3 +13,9 @@ var ErrPasswordIncorrect = errors.New("密码错误")
// 不允许的图片后缀
var ErrInvalidImageSuffix = errors.New("不允许的图片后缀")
+
+// OAuth2应用名称已存在
+var ErrOAuth2AppAlreadyExist = errors.New("OAuth2应用名称已存在")
+
+// OAuth2认证 Secret 不匹配
+var ErrOAuth2SecretNotMatch = errors.New("OAuth2 认证 Secret 不匹配")
diff --git a/backend/service/oauth.go b/backend/service/oauth.go
new file mode 100644
index 0000000..e494aa5
--- /dev/null
+++ b/backend/service/oauth.go
@@ -0,0 +1,67 @@
+package service
+
+import (
+ "github.com/RockChinQ/Campux/backend/database"
+ "github.com/RockChinQ/Campux/backend/mq"
+ "github.com/RockChinQ/Campux/backend/util"
+
+ "github.com/google/uuid"
+)
+
+type OAuth2Service struct {
+ CommonService
+ MQ mq.RedisStreamMQ
+}
+
+func NewOAuth2Service(db database.MongoDBManager, mq mq.RedisStreamMQ) *OAuth2Service {
+ return &OAuth2Service{
+ CommonService: CommonService{
+ DB: db,
+ },
+ MQ: mq,
+ }
+}
+
+func (oas *OAuth2Service) GetOAuth2AppByClientID(clientID string) (*database.OAuthAppPO, error) {
+ return oas.DB.GetOAuth2App(clientID)
+}
+
+func (oas *OAuth2Service) GenerateCode(clientID string, uin int64) (string, error) {
+ codeUUID := uuid.New().String()
+
+ err := oas.MQ.SetOauth2Code(codeUUID, uin)
+
+ if err != nil {
+ return "", err
+ }
+
+ return util.GenerateOAuth2CodeJWTToken(codeUUID, clientID)
+}
+
+func (oas *OAuth2Service) GetAccessToken(clientID, clientSecret, code string) (string, error) {
+ codeUUID, err := util.ParseOAuth2CodeJWTToken(code, clientID)
+
+ if err != nil {
+ return "", err
+ }
+ // 检查secret
+ app, err := oas.DB.GetOAuth2App(clientID)
+
+ if err != nil {
+ return "", err
+ }
+
+ if app.ClientSecret != clientSecret {
+ return "", ErrOAuth2SecretNotMatch
+ }
+
+ uin, err := oas.MQ.GetOauth2Uin(codeUUID)
+
+ if err != nil {
+ return "", err
+ }
+
+ accessToken, err := util.GenerateOAuth2AccessTokenJWTToken(uin, clientID)
+
+ return accessToken, err
+}
diff --git a/backend/util/jwt.go b/backend/util/jwt.go
index 407be9b..bd98241 100644
--- a/backend/util/jwt.go
+++ b/backend/util/jwt.go
@@ -8,8 +8,8 @@ import (
)
// 生成jwt token
-func GenerateJWTToken(uin int64) (string, error) {
-
+func GenerateUserJWTToken(uin int64) (string, error) {
+
// 生成token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"uin": uin,
@@ -26,8 +26,8 @@ func GenerateJWTToken(uin int64) (string, error) {
}
// 解析jwt token
-func ParseJWTToken(tokenString string) (int64, error) {
-
+func ParseUserJWTToken(tokenString string) (int64, error) {
+
// 解析token
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte(viper.GetString("auth.jwt.secret")), nil
@@ -48,4 +48,81 @@ func ParseJWTToken(tokenString string) (int64, error) {
}
return int64(uin), nil
-}
\ No newline at end of file
+}
+
+func GenerateOAuth2CodeJWTToken(codeUUID, clientID string) (string, error) {
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
+ "codeuuid": codeUUID,
+ "exp": time.Now().Add(time.Second * 60 * 10).Unix(),
+ })
+
+ tokenString, err := token.SignedString([]byte(viper.GetString("oauth2.server.code_secret") + clientID))
+ if err != nil {
+ return "", err
+ }
+
+ return tokenString, nil
+}
+
+func ParseOAuth2CodeJWTToken(tokenString, clientID string) (string, error) {
+ token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
+ return []byte(viper.GetString("oauth2.server.code_secret") + clientID), nil
+ })
+
+ if err != nil {
+ return "", err
+ }
+
+ claims, ok := token.Claims.(jwt.MapClaims)
+ if !ok {
+ return "", err
+ }
+
+ codeUUID, ok := claims["codeuuid"].(string)
+ if !ok {
+ return "", err
+ }
+
+ return codeUUID, nil
+}
+
+func GenerateOAuth2AccessTokenJWTToken(uin int64, clientID string) (string, error) {
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
+ "uin": uin,
+ "cid": clientID,
+ "exp": time.Now().Add(time.Second * time.Duration(viper.GetInt("oauth2.server.ak_expire"))).Unix(),
+ })
+
+ tokenString, err := token.SignedString([]byte(viper.GetString("oauth2.server.access_secret")))
+ if err != nil {
+ return "", err
+ }
+
+ return tokenString, nil
+}
+
+func ParseOAuth2AccessTokenJWTToken(tokenString string) (int64, string, error) {
+ token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
+ return []byte(viper.GetString("oauth2.server.access_secret")), nil
+ })
+ if err != nil {
+ return 0, "", err
+ }
+
+ claims, ok := token.Claims.(jwt.MapClaims)
+ if !ok {
+ return 0, "", err
+ }
+
+ uin, ok := claims["uin"].(float64)
+ if !ok {
+ return 0, "", err
+ }
+
+ cid, ok := claims["cid"].(string)
+ if !ok {
+ return 0, "", err
+ }
+
+ return int64(uin), cid, nil
+}
diff --git a/backend/util/string.go b/backend/util/string.go
index b0320b9..eef5e9d 100644
--- a/backend/util/string.go
+++ b/backend/util/string.go
@@ -1,5 +1,7 @@
package util
+import "math/rand"
+
func StringInSlice(str string, list []string) bool {
for _, v := range list {
if v == str {
@@ -8,3 +10,15 @@ func StringInSlice(str string, list []string) bool {
}
return false
}
+
+func RandomString(length int) string {
+ list := []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
+
+ var result []byte
+
+ for i := 0; i < length; i++ {
+ result = append(result, list[rand.Intn(len(list))])
+ }
+
+ return string(result)
+}
diff --git a/frontend/package.json b/frontend/package.json
index 1cc48d5..985f96a 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -12,9 +12,11 @@
"axios": "^1.6.8",
"core-js": "^3.34.0",
"js-cookie": "^3.0.5",
+ "mitt": "^3.0.1",
"roboto-fontface": "*",
"vue": "^3.4.0",
"vue-cookies": "^1.8.4",
+ "vue3-emoji-picker": "^1.1.8",
"vuetify": "^3.5.0",
"vuex": "^4.1.0"
},
diff --git a/frontend/src/components/BanRecordCard.vue b/frontend/src/components/BanRecordCard.vue
index 1287ff9..60fac4c 100644
--- a/frontend/src/components/BanRecordCard.vue
+++ b/frontend/src/components/BanRecordCard.vue
@@ -30,7 +30,7 @@
+
+
\ No newline at end of file
diff --git a/frontend/src/main.js b/frontend/src/main.js
index 3859cea..03aa566 100644
--- a/frontend/src/main.js
+++ b/frontend/src/main.js
@@ -23,6 +23,10 @@ const app = createApp(App)
app.use(store)
app.use(VueCookies)
+import mitt from 'mitt'
+
+const bus = mitt()
+
// let config=require("../config.json");
// fetch('/config.json').then(response => response.json()).then(config => {
// console.log(config)
@@ -41,6 +45,8 @@ const axiosInstance = axios.create({
})
app.config.globalProperties.$axios = { ...axiosInstance }
+app.config.globalProperties.$bus = bus
+
registerPlugins(app)
app.mount('#app')
diff --git a/frontend/src/pages/admin.vue b/frontend/src/pages/admin.vue
index 97acc0c..6d75453 100644
--- a/frontend/src/pages/admin.vue
+++ b/frontend/src/pages/admin.vue
@@ -7,6 +7,7 @@
🪪 账号
🚫 封禁记录
+ 🔑 OAuth 2 应用
@@ -23,7 +24,7 @@
- 查找
+ 查找
- 查找
+ 查找
@@ -61,6 +62,22 @@
+
+
+
+
+ 新建 OAuth2 应用
+ 刷新
+
+
+
+
+
+
+
+
@@ -77,13 +94,40 @@
+
+
+ 新建 OAuth2 应用
+
+
+
+
{{ newOAuthApp.emoji }}
+
+
+
+
+ 取消
+ 确定
+
+
+
+