u
This commit is contained in:
1 parent
8a3dea8545
commit
1903861354
21 files changed
+335
-236
No files matched your search
@@ -12,11 +12,11 @@ import (
|
|||||||
"allapp/utils/requestx"
|
"allapp/utils/requestx"
|
||||||
"allapp/utils/responsex"
|
"allapp/utils/responsex"
|
||||||
"allapp/utils/uniqueid"
|
"allapp/utils/uniqueid"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
"github.com/gofiber/fiber/v3/client"
|
"github.com/gofiber/fiber/v3/client"
|
||||||
|
"github.com/gofiber/fiber/v3/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LoginQq qq登录
|
// LoginQq qq登录
|
||||||
@@ -48,6 +48,11 @@ func LoginWechat(c fiber.Ctx) error {
|
|||||||
|
|
||||||
openid, accessToken, err := getWechatAccess(dto.Code)
|
openid, accessToken, err := getWechatAccess(dto.Code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorw("调用微信 access 接口失败",
|
||||||
|
"code", dto.Code, // 不打印原始 code
|
||||||
|
"error", err.Error(),
|
||||||
|
)
|
||||||
|
fmt.Println(err.Error())
|
||||||
return responsex.Error(c, "微信登录失败,请重试")
|
return responsex.Error(c, "微信登录失败,请重试")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +73,10 @@ func LoginWechat(c fiber.Ctx) error {
|
|||||||
// 新用户 → 拉用户信息
|
// 新用户 → 拉用户信息
|
||||||
nickname, avatar, err := getWechatUserInfo(accessToken, openid)
|
nickname, avatar, err := getWechatUserInfo(accessToken, openid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorw("调用微信用户信息接口失败",
|
||||||
|
"openid", openid,
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
return responsex.Error(c, "微信登录失败,请重试")
|
return responsex.Error(c, "微信登录失败,请重试")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +113,7 @@ func getWechatAccess(code string) (openid string, accessToken string, err error)
|
|||||||
openid, _ = accessMap["openid"].(string)
|
openid, _ = accessMap["openid"].(string)
|
||||||
|
|
||||||
if accessToken == "" || openid == "" {
|
if accessToken == "" || openid == "" {
|
||||||
return "", "", errors.New("获取微信登录凭证失败")
|
return "", "", fmt.Errorf("获取微信登录凭证失败: %s", bodyData)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func Logger() fiber.Handler {
|
|||||||
duration := time.Since(start)
|
duration := time.Since(start)
|
||||||
|
|
||||||
// 记录日志
|
// 记录日志
|
||||||
log.Infow("API Request",
|
log.WithContext(c.Context()).Infow("API Request",
|
||||||
"method", c.Method(),
|
"method", c.Method(),
|
||||||
"path", c.Path(),
|
"path", c.Path(),
|
||||||
"status", c.Response().StatusCode(),
|
"status", c.Response().StatusCode(),
|
||||||
|
|||||||
@@ -17,46 +17,60 @@ const CtxSpaceIDKey = "space_id"
|
|||||||
func SpaceGuard() fiber.Handler {
|
func SpaceGuard() fiber.Handler {
|
||||||
return func(c fiber.Ctx) error {
|
return func(c fiber.Ctx) error {
|
||||||
userID, err := GetUserID(c)
|
userID, err := GetUserID(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
spaceIDStr := strings.TrimSpace(c.Get("SPACE_ID"))
|
spaceIDStr := strings.TrimSpace(c.Get("SPACE_ID"))
|
||||||
|
|
||||||
spaceID, err := strconv.ParseInt(spaceIDStr, 10, 64)
|
spaceID, err := strconv.ParseInt(spaceIDStr, 10, 64)
|
||||||
if err != nil || spaceID <= 0 {
|
if err != nil || spaceID <= 0 {
|
||||||
return responsex.SpaceError(c, "请选择空间")
|
return responsex.SpaceError(c, "请选择空间")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验
|
// 1 先查 Redis
|
||||||
inSpace, err := redisx.SpaceIsMember(spaceID, userID)
|
inSpace, err := redisx.SpaceIsMember(spaceID, userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if inSpace {
|
if inSpace {
|
||||||
|
c.Locals(CtxSpaceIDKey, spaceID)
|
||||||
return c.Next()
|
return c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检验空间是否存在
|
// 2 Redis miss → 查数据库
|
||||||
space, err := db.Queries.GetSpaceById(c.Context(), spaceID)
|
role, err := db.Queries.GetUserSpaceRole(c.Context(), repo.GetUserSpaceRoleParams{
|
||||||
|
SpaceID: spaceID,
|
||||||
|
UserID: userID,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errorx.IsNotFound(err) {
|
if errorx.IsNotFound(err) {
|
||||||
// 未找到
|
return responsex.SpaceError(c, "未加入空间")
|
||||||
return responsex.SpaceError(c, "空间已失效")
|
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 空间有效,判断是否在该空间内
|
// 3 写回 Redis(重建缓存)
|
||||||
role, err := db.Queries.GetUserSpaceRole(c.Context(), repo.GetUserSpaceRoleParams{
|
_ = redisx.SpaceAddOrUpdateMember(spaceID, userID, int(role))
|
||||||
SpaceID: spaceID,
|
c.Locals(CtxSpaceIDKey, spaceID)
|
||||||
UserID: spaceID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
if errorx.IsNotFound(err) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 4 放行
|
||||||
|
return c.Next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetSpaceID(c fiber.Ctx) (int64, error) {
|
||||||
|
v := c.Locals(CtxSpaceIDKey)
|
||||||
|
|
||||||
|
if v == nil {
|
||||||
|
return 0, responsex.SpaceError(c, "空间无效")
|
||||||
|
}
|
||||||
|
|
||||||
|
spaceID, ok := v.(int64)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return 0, responsex.SpaceError(c, "空间无效")
|
||||||
|
}
|
||||||
|
|
||||||
|
return spaceID, nil
|
||||||
|
}
|
||||||
@@ -10,13 +10,22 @@ import (
|
|||||||
func SetupRouters(app *fiber.App) {
|
func SetupRouters(app *fiber.App) {
|
||||||
api := app.Group(config.GetConf().Server.BaseUrl)
|
api := app.Group(config.GetConf().Server.BaseUrl)
|
||||||
|
|
||||||
public := api.Group("")
|
// 1️⃣ 公开接口(无需登录)
|
||||||
//private := api.Group("", middleware.Auth())
|
publicGroup := api.Group("")
|
||||||
|
|
||||||
|
// 2️⃣ 需要登录
|
||||||
|
//authGroup := api.Group("", middleware.Auth())
|
||||||
|
//
|
||||||
|
//// 3️⃣ 需要登录 + 需要空间
|
||||||
|
//spaceGroup := api.Group("", middleware.Auth(), middleware.SpaceGuard())
|
||||||
|
|
||||||
|
// --------------------
|
||||||
// auth
|
// auth
|
||||||
authRouter := public.Group("/auth")
|
authRouter := publicGroup.Group("/auth")
|
||||||
authRouter.Post("/login/qq", auth.LoginQq)
|
authRouter.Post("/login/qq", auth.LoginQq)
|
||||||
authRouter.Post("/login/wechat", auth.LoginWechat)
|
authRouter.Post("/login/wechat", auth.LoginWechat)
|
||||||
|
|
||||||
// 其他的都带中间件
|
// 示例
|
||||||
|
// authGroup.Get("/user/profile", user.GetProfile)
|
||||||
|
// spaceGroup.Get("/space/info", space.GetInfo)
|
||||||
}
|
}
|
||||||
@@ -28,4 +28,4 @@ INSERT INTO "user" (
|
|||||||
-- name: UpdateUserLastLoginTime :exec
|
-- name: UpdateUserLastLoginTime :exec
|
||||||
UPDATE "user"
|
UPDATE "user"
|
||||||
SET last_login_time = NOW()
|
SET last_login_time = NOW()
|
||||||
WHERE id = $1;
|
WHERE id = $1;
|
||||||
@@ -3,9 +3,13 @@ module allapp
|
|||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1
|
||||||
github.com/gofiber/contrib/v3/zap v1.0.0
|
github.com/gofiber/contrib/v3/zap v1.0.0
|
||||||
github.com/gofiber/fiber/v3 v3.1.0
|
github.com/gofiber/fiber/v3 v3.1.0
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
|
github.com/jackc/pgx/v5 v5.8.0
|
||||||
|
github.com/redis/go-redis/v9 v9.18.0
|
||||||
|
github.com/shopspring/decimal v1.4.0
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
go.uber.org/zap v1.27.1
|
go.uber.org/zap v1.27.1
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
@@ -18,17 +22,15 @@ require (
|
|||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
github.com/gofiber/schema v1.7.0 // indirect
|
github.com/gofiber/schema v1.7.0 // indirect
|
||||||
github.com/gofiber/utils/v2 v2.0.2 // indirect
|
github.com/gofiber/utils/v2 v2.0.2 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/pgx/v5 v5.8.0 // indirect
|
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/klauspost/compress v1.18.4 // indirect
|
github.com/klauspost/compress v1.18.4 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
@@ -36,9 +38,7 @@ require (
|
|||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/philhofer/fwd v1.2.0 // indirect
|
github.com/philhofer/fwd v1.2.0 // indirect
|
||||||
github.com/redis/go-redis/v9 v9.18.0 // indirect
|
|
||||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||||
github.com/shopspring/decimal v1.4.0 // indirect
|
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||||
github.com/spf13/afero v1.15.0 // indirect
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
github.com/spf13/cast v1.10.0 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa
|
|||||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ log:
|
|||||||
max_age: 7
|
max_age: 7
|
||||||
compress: true
|
compress: true
|
||||||
|
|
||||||
|
wechat:
|
||||||
|
app_id: wxdab3e21a1f7e392f
|
||||||
|
app_secret: 0895443c6fedfc21cb92c9cd60de2a26
|
||||||
|
|
||||||
jwt:
|
jwt:
|
||||||
secret: 3Bde3BGEbYqtqyEUzW3ry8jKFcaPH17fRmTmqE7MDr05Lwj95uruRKrrkb44TJ4s
|
secret: 3Bde3BGEbYqtqyEUzW3ry8jKFcaPH17fRmTmqE7MDr05Lwj95uruRKrrkb44TJ4s
|
||||||
access_expiry: -1
|
access_expiry: -1
|
||||||
@@ -68,22 +68,6 @@ func SpaceIsMember(spaceID int64, userID int64) (bool, error) {
|
|||||||
return ok, err
|
return ok, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// SpaceInitMembers 初始化用户成员
|
|
||||||
func SpaceInitMembers(spaceID int64, members map[int64]int) error {
|
|
||||||
key := spaceMemberKey(spaceID)
|
|
||||||
|
|
||||||
data := make(map[string]interface{})
|
|
||||||
for userID, role := range members {
|
|
||||||
data[strconv.FormatInt(userID, 10)] = role
|
|
||||||
}
|
|
||||||
|
|
||||||
return Client.HSet(
|
|
||||||
context.Background(),
|
|
||||||
key,
|
|
||||||
data,
|
|
||||||
).Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SpaceDelete 删除空间
|
// SpaceDelete 删除空间
|
||||||
func SpaceDelete(spaceID int64) error {
|
func SpaceDelete(spaceID int64) error {
|
||||||
key := spaceMemberKey(spaceID)
|
key := spaceMemberKey(spaceID)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ type Result struct {
|
|||||||
Code int `json:"code"`
|
Code int `json:"code"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Data interface{} `json:"data"`
|
Data interface{} `json:"data"`
|
||||||
TrackId string `json:"trackId,omitempty"`
|
TrackId string `json:"track_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 业务码
|
// 业务码
|
||||||
|
|||||||
@@ -21,15 +21,31 @@ export default function Index() {
|
|||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text className="text-base text-gray-500 text-center mb-10">
|
<Text className="text-base text-gray-500 text-center mb-10">
|
||||||
记录生活,记录情绪,也记录每一笔重要的支出
|
记录生活,记录情绪
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<AButton
|
{/* <AButton
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onPress={() => router.replace('/money')}
|
onPress={() => router.replace('/money')}
|
||||||
>
|
>
|
||||||
开始使用
|
开始使用
|
||||||
</AButton>
|
</AButton> */}
|
||||||
|
<View className="flex flex-row gap-3 mt-4">
|
||||||
|
<AButton
|
||||||
|
className="flex-1"
|
||||||
|
onPress={() => router.replace('/money')}
|
||||||
|
>
|
||||||
|
创建空间
|
||||||
|
</AButton>
|
||||||
|
<AButton
|
||||||
|
className="flex-1"
|
||||||
|
variant="outline"
|
||||||
|
onPress={() => router.replace('/money')}
|
||||||
|
>
|
||||||
|
加入空间
|
||||||
|
</AButton>
|
||||||
|
</View>
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import ListItem from './components/ListItem'
|
|||||||
|
|
||||||
export default function User() {
|
export default function User() {
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
console.log(userStore.userInfo)
|
||||||
return (
|
return (
|
||||||
<SafeAreaView className="flex-1 p-4">
|
<SafeAreaView className="flex-1 p-4">
|
||||||
<View className="flex flex-row items-center">
|
<View className="flex flex-row items-center">
|
||||||
@@ -20,7 +20,7 @@ export default function User() {
|
|||||||
source={{ uri: userStore.userInfo?.avatar }}
|
source={{ uri: userStore.userInfo?.avatar }}
|
||||||
/>
|
/>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Text className="text-xl font-semibold ml-3">{userStore.userInfo?.username}</Text>
|
<Text className="text-xl font-semibold ml-3">{userStore.userInfo?.nickname}</Text>
|
||||||
<LucideIcon icon="ChevronRight" size={26} className="ml-auto" color="#aaa" strokeWidth={1.5} />
|
<LucideIcon icon="ChevronRight" size={26} className="ml-auto" color="#aaa" strokeWidth={1.5} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEvent } from 'expo'
|
import { useEvent } from 'expo'
|
||||||
// import ExpoQQ from 'expo-qq'
|
import ExpoQQ from 'expo-qq'
|
||||||
import { useRouter } from 'expo-router'
|
import { useRouter } from 'expo-router'
|
||||||
// import ExpoWeChat from 'expo-wechat'
|
import ExpoWeChat from 'expo-wechat'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Image, Text, View } from 'react-native'
|
import { Image, Text, View } from 'react-native'
|
||||||
import { AButton, ACheckbox } from '@/components/common/ao'
|
import { AButton, ACheckbox } from '@/components/common/ao'
|
||||||
@@ -25,176 +25,174 @@ export default function Auth() {
|
|||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
|
||||||
// const onLoginFinished = useEvent(ExpoQQ, 'onLoginFinished')
|
const onLoginFinished = useEvent(ExpoQQ, 'onLoginFinished')
|
||||||
// const onGetUserInfo = useEvent(ExpoQQ, 'onGetUserInfo')
|
const onGetUserInfo = useEvent(ExpoQQ, 'onGetUserInfo')
|
||||||
// const authResult = useEvent(ExpoWeChat, 'onAuthResult')
|
const authResult = useEvent(ExpoWeChat, 'onAuthResult')
|
||||||
|
|
||||||
// async function onQQLogin() {
|
async function onQQLogin() {
|
||||||
// setLoading(true)
|
setLoading(true)
|
||||||
// setForm(val => ({
|
setForm(val => ({
|
||||||
// ...val,
|
...val,
|
||||||
// type: 1,
|
type: 1,
|
||||||
// }))
|
}))
|
||||||
// await ExpoQQ.login(['get_user_info'])
|
await ExpoQQ.login(['get_user_info'])
|
||||||
// }
|
|
||||||
|
|
||||||
// async function onWeChatLogin() {
|
|
||||||
// setLoading(true)
|
|
||||||
|
|
||||||
// // 检查微信是否安装
|
|
||||||
// const isInstalled = await ExpoWeChat.isWXAppInstalled()
|
|
||||||
// if (!isInstalled) {
|
|
||||||
// toast.show({
|
|
||||||
// title: onLoginFinished?.errorMessage || '请先安装微信',
|
|
||||||
// })
|
|
||||||
// setLoading(false)
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// setForm(val => ({
|
|
||||||
// ...val,
|
|
||||||
// type: 0,
|
|
||||||
// }))
|
|
||||||
|
|
||||||
// ExpoWeChat.sendAuthRequest('snsapi_userinfo', 'state123')
|
|
||||||
// }
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// ExpoQQ.init('102826474', '')
|
|
||||||
// ExpoWeChat.registerApp('wxdab3e21a1f7e392f', '')
|
|
||||||
// }, [])
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (!onLoginFinished) {
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (!onLoginFinished.success) {
|
|
||||||
// toast.show({
|
|
||||||
// title: onLoginFinished?.errorMessage || '授权失败,请检查手机是否安装 QQ 或确认授权',
|
|
||||||
// })
|
|
||||||
// setLoading(false)
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const fetchUserInfo = async () => {
|
|
||||||
// try {
|
|
||||||
// const tokenInfo = await ExpoQQ.getLoginTokenInfo()
|
|
||||||
// if (!tokenInfo.openId) {
|
|
||||||
// toast.show({
|
|
||||||
// title: tokenInfo?.errorMessage || '授权失败,请检查手机是否安装 QQ 或确认授权',
|
|
||||||
// })
|
|
||||||
// setLoading(false)
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// setForm(val => ({
|
|
||||||
// ...val,
|
|
||||||
// openid: String(tokenInfo.openId),
|
|
||||||
// }))
|
|
||||||
|
|
||||||
// await ExpoQQ.sendGetUserInfoReq()
|
|
||||||
// }
|
|
||||||
// catch (e) {
|
|
||||||
// console.error('QQ 登录未完成', e)
|
|
||||||
// setLoading(false)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fetchUserInfo()
|
|
||||||
// }, [onLoginFinished])
|
|
||||||
|
|
||||||
// // QQ登录
|
|
||||||
// useEffect(() => {
|
|
||||||
// const handleLogin = async () => {
|
|
||||||
// if (!onGetUserInfo)
|
|
||||||
// return
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// const loginRes = await loginQqApi({
|
|
||||||
// openid: form.openid,
|
|
||||||
// username: onGetUserInfo.nickname || '',
|
|
||||||
// avatar: onGetUserInfo.figureurlQq_2 || onGetUserInfo.figureurl_2,
|
|
||||||
// })
|
|
||||||
|
|
||||||
// if (loginRes.isSuccess) {
|
|
||||||
// userStore.setUserInfo({
|
|
||||||
// token: loginRes.data.token,
|
|
||||||
// id: loginRes.data.userId,
|
|
||||||
// username: loginRes.data.username,
|
|
||||||
// avatar: loginRes.data.avatar,
|
|
||||||
// })
|
|
||||||
|
|
||||||
// console.log(loginRes.data.token)
|
|
||||||
|
|
||||||
// router.push('/')
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// catch (err) {
|
|
||||||
// console.error('登录接口出错', err)
|
|
||||||
// }
|
|
||||||
// finally {
|
|
||||||
// setLoading(false)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// handleLogin()
|
|
||||||
// }, [onGetUserInfo])
|
|
||||||
|
|
||||||
// // 微信登录
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (!authResult)
|
|
||||||
// return
|
|
||||||
|
|
||||||
// const handleLogin = async () => {
|
|
||||||
// if (authResult.errorCode === 0) {
|
|
||||||
// try {
|
|
||||||
// const loginRes = await loginWxApi({
|
|
||||||
// code: authResult.code,
|
|
||||||
// })
|
|
||||||
|
|
||||||
// if (loginRes.isSuccess) {
|
|
||||||
// userStore.setUserInfo({
|
|
||||||
// token: loginRes.data.token,
|
|
||||||
// id: loginRes.data.userId,
|
|
||||||
// username: loginRes.data.username,
|
|
||||||
// avatar: loginRes.data.avatar,
|
|
||||||
// })
|
|
||||||
|
|
||||||
// router.push('/')
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// catch (err) {
|
|
||||||
// console.error('登录接口出错', err)
|
|
||||||
// }
|
|
||||||
// finally {
|
|
||||||
// setLoading(false)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// else {
|
|
||||||
// console.log('授权失败:', authResult.errorMessage)
|
|
||||||
// setLoading(false)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// handleLogin()
|
|
||||||
// }, [authResult])
|
|
||||||
|
|
||||||
function testLogin() {
|
|
||||||
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo3NjY3ODU5ODIxNjUwNjEsImlhdCI6MTc2OTM0ODI0Nn0.17ZNaHD3HSLM8dVn_JQK-gvsG9MKj47IRYV9D4xZkzs
|
|
||||||
|
|
||||||
userStore.setUserInfo({
|
|
||||||
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo3Njc4NjM3NTU5MzE3MTcsImlhdCI6MTc2OTYwMzEzOX0.xfW-uyFBi799u4VJ-93A88c2CC8jwpYOPsALUuaIizg',
|
|
||||||
id: 767863755931717,
|
|
||||||
username: 'Zhang',
|
|
||||||
avatar: 'http://thirdqq.qlogo.cn/ek_qqapp/AQHVialeUibE42oV8RvTTVZ4tyRlPYeJrUibWTwtCPfV68E4rSzn9cw00GkXQ3QDTiawwhjoqQb7/100',
|
|
||||||
})
|
|
||||||
|
|
||||||
appStore.init()
|
|
||||||
|
|
||||||
router.push('/')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onWeChatLogin() {
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
// 检查微信是否安装
|
||||||
|
const isInstalled = await ExpoWeChat.isWXAppInstalled()
|
||||||
|
if (!isInstalled) {
|
||||||
|
toast.show({
|
||||||
|
title: onLoginFinished?.errorMessage || '请先安装微信',
|
||||||
|
})
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setForm(val => ({
|
||||||
|
...val,
|
||||||
|
type: 0,
|
||||||
|
}))
|
||||||
|
|
||||||
|
ExpoWeChat.sendAuthRequest('snsapi_userinfo', 'state123')
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
ExpoQQ.init('102826474', '')
|
||||||
|
ExpoWeChat.registerApp('wxdab3e21a1f7e392f', '')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!onLoginFinished) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!onLoginFinished.success) {
|
||||||
|
toast.show({
|
||||||
|
title: onLoginFinished?.errorMessage || '授权失败,请检查手机是否安装 QQ 或确认授权',
|
||||||
|
})
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchUserInfo = async () => {
|
||||||
|
try {
|
||||||
|
const tokenInfo = await ExpoQQ.getLoginTokenInfo()
|
||||||
|
if (!tokenInfo.openId) {
|
||||||
|
toast.show({
|
||||||
|
title: tokenInfo?.errorMessage || '授权失败,请检查手机是否安装 QQ 或确认授权',
|
||||||
|
})
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setForm(val => ({
|
||||||
|
...val,
|
||||||
|
openid: String(tokenInfo.openId),
|
||||||
|
}))
|
||||||
|
|
||||||
|
await ExpoQQ.sendGetUserInfoReq()
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error('QQ 登录未完成', e)
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchUserInfo()
|
||||||
|
}, [onLoginFinished])
|
||||||
|
|
||||||
|
// QQ登录
|
||||||
|
useEffect(() => {
|
||||||
|
const handleLogin = async () => {
|
||||||
|
if (!onGetUserInfo)
|
||||||
|
return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const loginRes = await loginQqApi({
|
||||||
|
openid: form.openid,
|
||||||
|
nickname: onGetUserInfo.nickname || '',
|
||||||
|
avatar: onGetUserInfo.figureurlQq_2 || onGetUserInfo.figureurl_2,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (loginRes.isSuccess) {
|
||||||
|
userStore.setUserInfo({
|
||||||
|
token: loginRes.data.token,
|
||||||
|
id: loginRes.data.userId,
|
||||||
|
nickname: loginRes.data.nickname,
|
||||||
|
avatar: loginRes.data.avatar,
|
||||||
|
})
|
||||||
|
|
||||||
|
router.push('/auth/space')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error('登录接口出错', err)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLogin()
|
||||||
|
}, [onGetUserInfo])
|
||||||
|
|
||||||
|
// 微信登录
|
||||||
|
useEffect(() => {
|
||||||
|
if (!authResult)
|
||||||
|
return
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
if (authResult.errorCode === 0) {
|
||||||
|
try {
|
||||||
|
const loginRes = await loginWxApi({
|
||||||
|
code: authResult.code,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (loginRes.isSuccess) {
|
||||||
|
userStore.setUserInfo({
|
||||||
|
token: loginRes.data.token,
|
||||||
|
id: loginRes.data.userId,
|
||||||
|
nickname: loginRes.data.nickname,
|
||||||
|
avatar: loginRes.data.avatar,
|
||||||
|
})
|
||||||
|
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error('登录接口出错', err)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log('授权失败:', authResult.errorMessage)
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLogin()
|
||||||
|
}, [authResult])
|
||||||
|
|
||||||
|
// function testLogin() {
|
||||||
|
// // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo3NjY3ODU5ODIxNjUwNjEsImlhdCI6MTc2OTM0ODI0Nn0.17ZNaHD3HSLM8dVn_JQK-gvsG9MKj47IRYV9D4xZkzs
|
||||||
|
|
||||||
|
// userStore.setUserInfo({
|
||||||
|
// token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo3NzgxMTUwMzIxMDA5MzMsImlhdCI6MTc3MjExMDQ5N30.Ab7_YNwftJigktnPt8f-FwwcLeS45CkrH0jT2jLjRtA',
|
||||||
|
// id: 778115032100933,
|
||||||
|
// nickname: 'Zhang',
|
||||||
|
// avatar: 'http://thirdqq.qlogo.cn/ek_qqapp/AQHVialeUibE42oV8RvTTVZ4tyRlPYeJrUibWTwtCPfV68E4rSzn9cw00GkXQ3QDTiawwhjoqQb7/100',
|
||||||
|
// })
|
||||||
|
|
||||||
|
// appStore.init()
|
||||||
|
|
||||||
|
// router.push('/')
|
||||||
|
// }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="flex-1 bg-white px-6 justify-center">
|
<View className="flex-1 bg-white px-6 justify-center">
|
||||||
<CustomHeader
|
<CustomHeader
|
||||||
@@ -212,7 +210,7 @@ export default function Auth() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="flex flex-col gap-4">
|
<View className="flex flex-col gap-4">
|
||||||
{/* <AButton
|
<AButton
|
||||||
leftIcon={
|
leftIcon={
|
||||||
<IconWeixin color="#fff" />
|
<IconWeixin color="#fff" />
|
||||||
}
|
}
|
||||||
@@ -231,7 +229,7 @@ export default function Auth() {
|
|||||||
onPress={onQQLogin}
|
onPress={onQQLogin}
|
||||||
>
|
>
|
||||||
QQ
|
QQ
|
||||||
</AButton> */}
|
</AButton>
|
||||||
|
|
||||||
<AButton
|
<AButton
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -239,7 +237,7 @@ export default function Auth() {
|
|||||||
<IconQq />
|
<IconQq />
|
||||||
}
|
}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onPress={testLogin}
|
// onPress={testLogin}
|
||||||
>
|
>
|
||||||
模拟登录
|
模拟登录
|
||||||
</AButton>
|
</AButton>
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { Text, View } from 'react-native'
|
||||||
|
import { AButton, LucideIcon } from '@/components/common/ao'
|
||||||
|
import EmptyState from '@/components/common/EmptyStatus'
|
||||||
|
import CustomHeader from '@/components/custom/Header'
|
||||||
|
|
||||||
|
// name renshu
|
||||||
|
|
||||||
|
export default function AuthSpace() {
|
||||||
|
return (
|
||||||
|
<View className="flex-1">
|
||||||
|
<CustomHeader title="空间列表" />
|
||||||
|
<View className="flex-1 px-4 pt-2 pb-8">
|
||||||
|
<View>
|
||||||
|
<Text className="text-lg font-semibold">你的空间</Text>
|
||||||
|
<Text className="text-sm text-gray-600">选择要进入的空间</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="flex-1 my-4">
|
||||||
|
{/* <View className="items-center justify-center">
|
||||||
|
<EmptyState title="暂无空间" description="请选择 创建 或 加入 空间" />
|
||||||
|
</View> */}
|
||||||
|
|
||||||
|
<View>
|
||||||
|
<View className="rounded-lg p-3 flex flex-row items-center justify-between bg-gray-100">
|
||||||
|
<View className="flex-row items-center">
|
||||||
|
<View className="mr-4">
|
||||||
|
<LucideIcon
|
||||||
|
icon="Grip"
|
||||||
|
size={20}
|
||||||
|
color="#666"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="">
|
||||||
|
<View>
|
||||||
|
<Text className="font-semibold">A 的空间</Text>
|
||||||
|
</View>
|
||||||
|
<Text className="text-sm text-gray-500 mt-1">张三 · 8 名成员</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View>
|
||||||
|
<LucideIcon
|
||||||
|
icon="Bolt"
|
||||||
|
size={20}
|
||||||
|
color="#aaa"
|
||||||
|
style={{ padding: 6 }}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="flex gap-3">
|
||||||
|
<AButton>创建新空间</AButton>
|
||||||
|
<AButton variant="outline">加入其他空间</AButton>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -39,7 +39,7 @@ export default function EmptyState({
|
|||||||
{image ?? defaultImages[type]}
|
{image ?? defaultImages[type]}
|
||||||
|
|
||||||
{title && (
|
{title && (
|
||||||
<Text className="text-2xl font-semibold text-gray-900 mb-2">{title}</Text>
|
<Text className="text-2xl font-semibold text-gray-900 mb-2 text-center ">{title}</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{description && (
|
{description && (
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface LucideIconProps {
|
|||||||
style?: ViewStyle
|
style?: ViewStyle
|
||||||
clickable?: boolean
|
clickable?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
|
fill?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LucideIcon({
|
export default function LucideIcon({
|
||||||
@@ -22,6 +23,7 @@ export default function LucideIcon({
|
|||||||
onPress,
|
onPress,
|
||||||
style,
|
style,
|
||||||
clickable = true,
|
clickable = true,
|
||||||
|
fill = 'none',
|
||||||
className,
|
className,
|
||||||
}: LucideIconProps) {
|
}: LucideIconProps) {
|
||||||
const IconComponent = icons[icon]
|
const IconComponent = icons[icon]
|
||||||
@@ -30,12 +32,12 @@ export default function LucideIcon({
|
|||||||
return null
|
return null
|
||||||
|
|
||||||
if (!clickable) {
|
if (!clickable) {
|
||||||
return <IconComponent size={size} color={color} strokeWidth={strokeWidth} style={style} className={className} />
|
return <IconComponent size={size} color={color} strokeWidth={strokeWidth} style={style} className={className} fill={fill} />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable onPress={onPress} style={style} className={className}>
|
<Pressable onPress={onPress} style={style} className={className}>
|
||||||
<IconComponent size={size} color={color} strokeWidth={strokeWidth} />
|
<IconComponent size={size} color={color} strokeWidth={strokeWidth} fill={fill} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -8,5 +8,5 @@ export function loginQqApi(params: LoginQqParams) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function loginWxApi(params: LoginWxParams) {
|
export function loginWxApi(params: LoginWxParams) {
|
||||||
return alovaInstance.Post<ResponseData<LoginResult>>(`${API_PREFIX}/login/wx`, params)
|
return alovaInstance.Post<ResponseData<LoginResult>>(`${API_PREFIX}/login/wechat`, params)
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,7 @@ import { useUserStore } from '@/store/user'
|
|||||||
import { toast } from '@/utils/toast'
|
import { toast } from '@/utils/toast'
|
||||||
|
|
||||||
enum HttpCode {
|
enum HttpCode {
|
||||||
Success = 200,
|
Success = 1000,
|
||||||
Unauthorized = 401,
|
Unauthorized = 401,
|
||||||
Fail = 500,
|
Fail = 500,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const DEFAULT_NAV_MENUS: NavItem[] = [
|
|||||||
{ label: '恋爱记', path: '/love', icon: 'Heart' },
|
{ label: '恋爱记', path: '/love', icon: 'Heart' },
|
||||||
{ label: '聊天', path: '/chat', icon: 'MessageCircle' },
|
{ label: '聊天', path: '/chat', icon: 'MessageCircle' },
|
||||||
{ label: '个人中心', path: '/user', icon: 'CircleUserRound' },
|
{ label: '个人中心', path: '/user', icon: 'CircleUserRound' },
|
||||||
|
{ label: '我的空间', path: '/auth/space', icon: 'Earth' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export const useNavStore = create<NavState>()(
|
export const useNavStore = create<NavState>()(
|
||||||
@@ -43,7 +44,7 @@ export const useNavStore = create<NavState>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'nav-storage',
|
name: 'nav-storage',
|
||||||
storage: createJSONStorage(() => AsyncStorage),
|
// storage: createJSONStorage(() => AsyncStorage),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
export interface LoginQqParams {
|
export interface LoginQqParams {
|
||||||
openid: string
|
openid: string
|
||||||
username: string
|
nickname: string
|
||||||
avatar: string
|
avatar: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11,6 +11,6 @@ export interface LoginWxParams {
|
|||||||
export interface LoginResult {
|
export interface LoginResult {
|
||||||
token: string
|
token: string
|
||||||
userId: number
|
userId: number
|
||||||
username: string
|
nickname: string
|
||||||
avatar: string
|
avatar: string
|
||||||
}
|
}
|
||||||
+2
-3
@@ -2,7 +2,7 @@ export {}
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface ResponseData<T> {
|
interface ResponseData<T> {
|
||||||
trackId: string
|
track_id: string
|
||||||
code: number
|
code: number
|
||||||
message: string
|
message: string
|
||||||
data: T
|
data: T
|
||||||
@@ -12,14 +12,13 @@ declare global {
|
|||||||
interface UserInfo {
|
interface UserInfo {
|
||||||
id: number
|
id: number
|
||||||
token: string
|
token: string
|
||||||
username?: string
|
nickname?: string
|
||||||
avatar?: string
|
avatar?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserSpace {
|
interface UserSpace {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
invite_code: string
|
|
||||||
role: number
|
role: number
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user