做统一认证登录
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"server/models"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// ApiPlatformUpgradeController 平台更新内容接口
|
||||
type ApiPlatformUpgradeController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// List GET /api/platform/upgrade/list 获取平台更新列表(公开接口)
|
||||
func (c *ApiPlatformUpgradeController) List() {
|
||||
// 可选参数:limit - 返回数量限制,默认 10
|
||||
limitStr := c.GetString("limit")
|
||||
limit := 10
|
||||
if limitStr != "" {
|
||||
val, err := strconv.Atoi(limitStr)
|
||||
if err == nil {
|
||||
limit = val
|
||||
}
|
||||
}
|
||||
if limit <= 0 || limit > 50 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
// 查询显示状态的更新记录,按排序字段和创建时间倒序
|
||||
var upgradeList []models.PlatformUpgradeNotice
|
||||
qs := models.Orm.QueryTable(new(models.PlatformUpgradeNotice)).
|
||||
Filter("status", 1).
|
||||
Filter("delete_time__isnull", true)
|
||||
|
||||
_, err := qs.OrderBy("-sort", "-create_time").Limit(limit).All(&upgradeList)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 500,
|
||||
"msg": "查询失败",
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为前端需要的格式
|
||||
data := make([]map[string]interface{}, 0, len(upgradeList))
|
||||
for _, item := range upgradeList {
|
||||
record := map[string]interface{}{
|
||||
"id": item.ID,
|
||||
"update_date": item.UpdateDate,
|
||||
"title": item.Title,
|
||||
"content": item.Content,
|
||||
"create_time": item.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
data = append(data, record)
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": data,
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Detail GET /api/platform/upgrade/detail?id={id} 获取单条更新详情
|
||||
func (c *ApiPlatformUpgradeController) Detail() {
|
||||
idStr := strings.TrimSpace(c.GetString("id"))
|
||||
if idStr == "" {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 400,
|
||||
"msg": "缺少参数 id",
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
var id uint64
|
||||
var parseErr error
|
||||
id, parseErr = strconv.ParseUint(idStr, 10, 64)
|
||||
if parseErr != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 400,
|
||||
"msg": "参数错误",
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
var row models.PlatformUpgradeNotice
|
||||
var queryErr error
|
||||
queryErr = models.Orm.QueryTable(new(models.PlatformUpgradeNotice)).
|
||||
Filter("id", id).
|
||||
Filter("status", 1).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&row)
|
||||
if queryErr != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 404,
|
||||
"msg": "更新内容不存在",
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"id": row.ID,
|
||||
"tenant_id": row.TenantID,
|
||||
"update_date": row.UpdateDate,
|
||||
"title": row.Title,
|
||||
"content": row.Content,
|
||||
"sort": row.Sort,
|
||||
"create_time": row.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": data,
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
authsvc "server/services/auth"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
"github.com/beego/beego/v2/server/web/context"
|
||||
)
|
||||
|
||||
// AuthLoginController 认证中心登录相关端点
|
||||
type AuthLoginController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *AuthLoginController) serveJSON(data map[string]interface{}) {
|
||||
c.Data["json"] = data
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// LoginPage 统一登录页(beego 模板渲染)
|
||||
// GET /auth/login?redirect=<base64(回跳URL)>&client_id=xxx&step=tenant
|
||||
func (c *AuthLoginController) LoginPage() {
|
||||
clientID := strings.TrimSpace(c.GetString("client_id"))
|
||||
redirect := c.GetString("redirect")
|
||||
step := c.GetString("step")
|
||||
errMsg := c.GetString("error")
|
||||
|
||||
// 已登录且已选企业:直接放行到 authorize,无需再输密码
|
||||
sid := strings.TrimSpace(c.Ctx.GetCookie(sessionCookieName))
|
||||
if sid != "" && step != "tenant" {
|
||||
if session, err := authsvc.GetSession(sid); err == nil && session.Tid != authsvc.PendingTenantID {
|
||||
if back := decodeRedirect(redirect); back != "" {
|
||||
c.Redirect(back, 302)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 企业选择步骤:需要展示可进入的企业列表
|
||||
tenants := make([]authsvc.TenantOption, 0)
|
||||
if sid != "" {
|
||||
if session, err := authsvc.GetSession(sid); err == nil {
|
||||
list, _ := authsvc.ListTenantOptions(session.IdentityID)
|
||||
tenants = list
|
||||
// 只有一家企业直接进入,无需展示选择页
|
||||
if len(tenants) == 1 && step == "tenant" {
|
||||
if _, _, err := authsvc.ChooseTenant(sid, tenants[0].Tid, clientID); err == nil {
|
||||
if back := decodeRedirect(redirect); back != "" {
|
||||
c.Redirect(back, 302)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.Data["ClientID"] = clientID
|
||||
c.Data["Redirect"] = redirect
|
||||
c.Data["Step"] = step
|
||||
c.Data["Error"] = errMsg
|
||||
c.Data["Tenants"] = tenants
|
||||
c.TplName = "auth/login.tpl"
|
||||
}
|
||||
|
||||
// LoginSubmit 提交登录
|
||||
// POST /auth/login { account, password, client_id, redirect }
|
||||
func (c *AuthLoginController) LoginSubmit() {
|
||||
var req struct {
|
||||
Account string `json:"account"`
|
||||
Password string `json:"password"`
|
||||
ClientID string `json:"client_id"`
|
||||
Redirect string `json:"redirect"`
|
||||
DeviceID string `json:"device_id"`
|
||||
}
|
||||
|
||||
body := c.Ctx.Input.RequestBody
|
||||
if len(body) == 0 {
|
||||
var err error
|
||||
body, err = io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil || len(body) == 0 {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"})
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"})
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.Account) == "" || req.Password == "" {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "请输入账号和密码"})
|
||||
return
|
||||
}
|
||||
|
||||
clientIP := c.Ctx.Input.IP()
|
||||
userAgent := c.Ctx.Request.UserAgent()
|
||||
|
||||
result, err := authsvc.PasswordLogin(req.Account, req.Password, req.ClientID, clientIP, userAgent)
|
||||
if err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 401, "msg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 写入认证中心会话 Cookie,供后续 authorize 识别登录态
|
||||
setSessionCookieForCtx(c.Ctx, result.Tokens.Sid)
|
||||
|
||||
if result.NeedChooseTenant {
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "请选择要进入的企业",
|
||||
"need_choose_tenant": true,
|
||||
"tenants": result.Tenants,
|
||||
"sid": result.Tokens.Sid,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
back := decodeRedirect(req.Redirect)
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "登录成功",
|
||||
"redirect": back,
|
||||
"tokens": result.Tokens,
|
||||
"tenants": result.Tenants,
|
||||
})
|
||||
}
|
||||
|
||||
// Tenants 当前登录身份可进入的企业列表(Bearer Token)
|
||||
// GET /auth/tenants
|
||||
func (c *AuthLoginController) Tenants() {
|
||||
claims := claimsFromHeaderLogin(c)
|
||||
if claims == nil {
|
||||
c.Ctx.Output.SetStatus(401)
|
||||
c.serveJSON(map[string]interface{}{"code": 401, "msg": "未登录"})
|
||||
return
|
||||
}
|
||||
list, err := authsvc.ListTenantOptions(uint64(claims.UserID))
|
||||
if err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 500, "msg": "查询失败"})
|
||||
return
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{"code": 200, "data": list})
|
||||
}
|
||||
|
||||
// SwitchTenant 免密切换企业
|
||||
// POST /auth/switch-tenant { tid, client_id, redirect }
|
||||
func (c *AuthLoginController) SwitchTenant() {
|
||||
var req struct {
|
||||
Tid uint64 `json:"tid"`
|
||||
ClientID string `json:"client_id"`
|
||||
Redirect string `json:"redirect"`
|
||||
}
|
||||
body := c.Ctx.Input.RequestBody
|
||||
if len(body) == 0 {
|
||||
body, _ = io.ReadAll(c.Ctx.Request.Body)
|
||||
}
|
||||
_ = json.Unmarshal(body, &req)
|
||||
|
||||
sid := strings.TrimSpace(c.Ctx.GetCookie(sessionCookieName))
|
||||
if sid == "" {
|
||||
if claims := claimsFromHeaderLogin(c); claims != nil {
|
||||
sid = claims.Sid
|
||||
}
|
||||
}
|
||||
if sid == "" || req.Tid == 0 {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
tokens, profile, err := authsvc.ChooseTenant(sid, req.Tid, req.ClientID)
|
||||
if err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 403, "msg": err.Error()})
|
||||
return
|
||||
}
|
||||
setSessionCookieForCtx(c.Ctx, tokens.Sid)
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "切换成功",
|
||||
"redirect": decodeRedirect(req.Redirect),
|
||||
"data": map[string]interface{}{"tokens": tokens, "identity": profile},
|
||||
})
|
||||
}
|
||||
|
||||
// LogoutPage 单点登出入口(OIDC end_session_endpoint)
|
||||
// GET /auth/logout?post_logout_redirect_uri=&client_id=
|
||||
//
|
||||
// 清除认证中心 Cookie 并吊销会话与令牌;回跳地址必须在应用白名单内。
|
||||
// 登记了 backchannel_logout_uri 的应用会收到服务端登出通知(P2 阶段接入)。
|
||||
func (c *AuthLoginController) LogoutPage() {
|
||||
sid := strings.TrimSpace(c.Ctx.GetCookie(sessionCookieName))
|
||||
access := bearerTokenLogin(c)
|
||||
if sid != "" {
|
||||
_ = authsvc.RevokeSession(sid, models.RevokeReasonLogout)
|
||||
}
|
||||
if access != "" {
|
||||
_ = authsvc.RevokeTokenPair("", access, models.RevokeReasonLogout)
|
||||
}
|
||||
clearSessionCookieForCtx(c.Ctx)
|
||||
|
||||
back := strings.TrimSpace(c.GetString("post_logout_redirect_uri"))
|
||||
cid := strings.TrimSpace(c.GetString("client_id"))
|
||||
if back != "" && cid != "" {
|
||||
if client, err := findClient(cid); err == nil && allowRedirect(client, back) {
|
||||
c.Redirect(back, 302)
|
||||
return
|
||||
}
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{"code": 200, "msg": "已登出"})
|
||||
}
|
||||
|
||||
// Sessions 在线设备列表
|
||||
// GET /auth/sessions
|
||||
func (c *AuthLoginController) Sessions() {
|
||||
claims := claimsFromHeaderLogin(c)
|
||||
if claims == nil {
|
||||
c.Ctx.Output.SetStatus(401)
|
||||
c.serveJSON(map[string]interface{}{"code": 401, "msg": "未登录"})
|
||||
return
|
||||
}
|
||||
list, err := authsvc.ListActiveSessions(uint64(claims.UserID))
|
||||
if err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 500, "msg": "查询失败"})
|
||||
return
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{"code": 200, "data": list})
|
||||
}
|
||||
|
||||
// KickSession 踢下线指定设备
|
||||
// POST /auth/sessions/kick { sid }
|
||||
func (c *AuthLoginController) KickSession() {
|
||||
var req struct {
|
||||
Sid string `json:"sid"`
|
||||
}
|
||||
body := c.Ctx.Input.RequestBody
|
||||
if len(body) == 0 {
|
||||
body, _ = io.ReadAll(c.Ctx.Request.Body)
|
||||
}
|
||||
_ = json.Unmarshal(body, &req)
|
||||
|
||||
claims := claimsFromHeaderLogin(c)
|
||||
if claims == nil {
|
||||
c.Ctx.Output.SetStatus(401)
|
||||
c.serveJSON(map[string]interface{}{"code": 401, "msg": "未登录"})
|
||||
return
|
||||
}
|
||||
if req.Sid == "" {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"})
|
||||
return
|
||||
}
|
||||
// 只允许踢自己名下的会话
|
||||
owned := false
|
||||
if list, err := authsvc.ListActiveSessions(uint64(claims.UserID)); err == nil {
|
||||
for _, s := range list {
|
||||
if s.Sid == req.Sid {
|
||||
owned = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !owned {
|
||||
c.serveJSON(map[string]interface{}{"code": 403, "msg": "无权操作"})
|
||||
return
|
||||
}
|
||||
if err := authsvc.RevokeSession(req.Sid, models.RevokeReasonAdmin); err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 500, "msg": "操作失败"})
|
||||
return
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{"code": 200, "msg": "已下线"})
|
||||
}
|
||||
|
||||
// LogoutAction 退出登录(清 Cookie + 吊销会话与令牌)
|
||||
// POST /auth/logout
|
||||
func (c *AuthLoginController) LogoutAction() {
|
||||
sid := strings.TrimSpace(c.Ctx.GetCookie(sessionCookieName))
|
||||
access := ""
|
||||
if claims := claimsFromHeaderLogin(c); claims != nil {
|
||||
if sid == "" {
|
||||
sid = claims.Sid
|
||||
}
|
||||
access = bearerTokenLogin(c)
|
||||
}
|
||||
_ = authsvc.Logout(access, "")
|
||||
if sid != "" {
|
||||
_ = authsvc.RevokeSession(sid, models.RevokeReasonLogout)
|
||||
}
|
||||
clearSessionCookieForCtx(c.Ctx)
|
||||
c.serveJSON(map[string]interface{}{"code": 200, "msg": "已登出"})
|
||||
}
|
||||
|
||||
// VerifyConfig 租户登录验证配置(替代 /backend/login/getOpenVerify)
|
||||
// GET /auth/verify-config?tid=
|
||||
func (c *AuthLoginController) VerifyConfig() {
|
||||
tid, _ := c.GetInt64("tid", 0)
|
||||
cfg := authsvc.GetTenantSessionPolicy(uint64(tid))
|
||||
|
||||
var authCfg models.AuthTenantAuthConfig
|
||||
verifyType := "captcha"
|
||||
openVerify := 1
|
||||
if err := models.Orm.QueryTable(new(models.AuthTenantAuthConfig)).
|
||||
Filter("tid", tid).One(&authCfg); err == nil {
|
||||
verifyType = authCfg.VerifyType
|
||||
openVerify = int(authCfg.OpenVerify)
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200,
|
||||
"data": map[string]interface{}{
|
||||
"openVerify": openVerify,
|
||||
"verifyType": verifyType,
|
||||
"sessionTTL": cfg.SessionTTL,
|
||||
"maxSession": cfg.MaxSession,
|
||||
"mfaRequired": 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 工具
|
||||
|
||||
func decodeRedirect(encoded string) string {
|
||||
if encoded == "" {
|
||||
return ""
|
||||
}
|
||||
raw, err := base64.RawURLEncoding.DecodeString(encoded)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
back := string(raw)
|
||||
// 只允许站内地址,防开放重定向
|
||||
if !strings.HasPrefix(back, "/auth/") && !strings.HasPrefix(back, "http") {
|
||||
return ""
|
||||
}
|
||||
return back
|
||||
}
|
||||
|
||||
func claimsFromHeaderLogin(c *AuthLoginController) *jwtutil.Claims {
|
||||
token := bearerTokenLogin(c)
|
||||
if token == "" {
|
||||
return nil
|
||||
}
|
||||
claims, err := jwtutil.ParseToken(token)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return claims
|
||||
}
|
||||
|
||||
func bearerTokenLogin(c *AuthLoginController) string {
|
||||
header := c.Ctx.Request.Header.Get("Authorization")
|
||||
if header == "" {
|
||||
return ""
|
||||
}
|
||||
parts := strings.SplitN(header, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(parts[1])
|
||||
}
|
||||
|
||||
func setSessionCookieForCtx(ctx *context.Context, sid string) {
|
||||
ctx.Output.Header("Set-Cookie",
|
||||
fmt.Sprintf("%s=%s; Path=/; Max-Age=%d; HttpOnly; Secure; SameSite=Lax",
|
||||
sessionCookieName, sid, sessionCookieTTL))
|
||||
}
|
||||
|
||||
func clearSessionCookieForCtx(ctx *context.Context) {
|
||||
ctx.Output.Header("Set-Cookie",
|
||||
fmt.Sprintf("%s=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Lax", sessionCookieName))
|
||||
}
|
||||
@@ -0,0 +1,526 @@
|
||||
// Package auth 统一认证中心(UAC)控制器:api.yunzer.cn/auth
|
||||
//
|
||||
// 实现 OIDC 1.0(基于 OAuth 2.1 + PKCE)标准端点,
|
||||
// 以后每开发一个新软件,只需在 yz_auth_client 注册一条即可接入。
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
authsvc "server/services/auth"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// 认证中心会话 Cookie(仅作用于认证中心域名,用于 authorize 阶段识别登录态)
|
||||
const (
|
||||
sessionCookieName = "yz_sid"
|
||||
sessionCookieTTL = 7200
|
||||
)
|
||||
|
||||
// grant_type 常量
|
||||
const (
|
||||
GrantAuthCode = "authorization_code"
|
||||
GrantRefresh = "refresh_token"
|
||||
ResponseTypeCode = "code"
|
||||
)
|
||||
|
||||
// AuthOidcController OIDC 标准端点
|
||||
type AuthOidcController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *AuthOidcController) serveJSON(data map[string]interface{}) {
|
||||
c.Data["json"] = data
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *AuthOidcController) fail(status int, msg string) {
|
||||
c.Ctx.Output.SetStatus(status)
|
||||
c.serveJSON(map[string]interface{}{"error": msg})
|
||||
}
|
||||
|
||||
// Discovery OIDC 发现文档
|
||||
// GET /auth/.well-known/openid-configuration
|
||||
func (c *AuthOidcController) Discovery() {
|
||||
issuer := jwtutil.Issuer()
|
||||
if issuer == "" {
|
||||
issuer = fmt.Sprintf("https://%s/auth", c.Ctx.Request.Host)
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"issuer": issuer,
|
||||
"authorization_endpoint": issuer + "/authorize",
|
||||
"token_endpoint": issuer + "/token",
|
||||
"userinfo_endpoint": issuer + "/userinfo",
|
||||
"introspection_endpoint": issuer + "/introspect",
|
||||
"revocation_endpoint": issuer + "/revoke",
|
||||
"end_session_endpoint": issuer + "/logout",
|
||||
"jwks_uri": issuer + "/jwks.json",
|
||||
"response_types_supported": []string{"code"},
|
||||
"grant_types_supported": []string{GrantAuthCode, GrantRefresh},
|
||||
"subject_types_supported": []string{"public"},
|
||||
"id_token_signing_alg_values_supported": []string{jwtutil.AlgRS256, jwtutil.AlgHS256},
|
||||
"code_challenge_methods_supported": []string{"S256"},
|
||||
"scopes_supported": []string{"openid", "profile", "tenant"},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// JWKS 公钥集合(各应用本地验签用)
|
||||
// GET /auth/jwks.json
|
||||
func (c *AuthOidcController) JWKS() {
|
||||
keys := jwtutil.JWKS()
|
||||
if keys == nil {
|
||||
keys = []jwtutil.JWK{}
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"keys": keys}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Authorize 授权端点
|
||||
// GET /auth/authorize?client_id=&redirect_uri=&response_type=code&scope=&state=&code_challenge=&code_challenge_method=S256
|
||||
//
|
||||
// 未登录时重定向到统一登录页,登录后再回到本端点完成授权。
|
||||
func (c *AuthOidcController) Authorize() {
|
||||
clientID := strings.TrimSpace(c.GetString("client_id"))
|
||||
redirectURI := strings.TrimSpace(c.GetString("redirect_uri"))
|
||||
responseType := strings.TrimSpace(c.GetString("response_type"))
|
||||
state := c.GetString("state")
|
||||
scope := c.GetString("scope")
|
||||
if scope == "" {
|
||||
scope = "openid"
|
||||
}
|
||||
challenge := c.GetString("code_challenge")
|
||||
challengeMethod := c.GetString("code_challenge_method")
|
||||
if challengeMethod == "" {
|
||||
challengeMethod = "S256"
|
||||
}
|
||||
nonce := c.GetString("nonce")
|
||||
|
||||
if clientID == "" || redirectURI == "" {
|
||||
c.Ctx.Output.SetStatus(400)
|
||||
_, _ = c.Ctx.ResponseWriter.Write([]byte("缺少 client_id 或 redirect_uri"))
|
||||
return
|
||||
}
|
||||
if responseType != ResponseTypeCode {
|
||||
c.Ctx.Output.SetStatus(400)
|
||||
_, _ = c.Ctx.ResponseWriter.Write([]byte("仅支持 response_type=code"))
|
||||
return
|
||||
}
|
||||
|
||||
client, err := findClient(clientID)
|
||||
if err != nil {
|
||||
c.Ctx.Output.SetStatus(400)
|
||||
_, _ = c.Ctx.ResponseWriter.Write([]byte("client_id 无效"))
|
||||
return
|
||||
}
|
||||
if !allowRedirect(client, redirectURI) {
|
||||
c.Ctx.Output.SetStatus(400)
|
||||
_, _ = c.Ctx.ResponseWriter.Write([]byte("redirect_uri 未登记"))
|
||||
return
|
||||
}
|
||||
|
||||
// 登录态:Cookie 优先(浏览器跳转),其次 Authorization(服务端调用)
|
||||
sid := strings.TrimSpace(c.Ctx.GetCookie(sessionCookieName))
|
||||
if sid == "" {
|
||||
if claims := claimsFromHeader(c); claims != nil {
|
||||
sid = claims.Sid
|
||||
}
|
||||
}
|
||||
if sid == "" {
|
||||
// 未登录 → 去登录页,登录后带着参数回来
|
||||
back := fmt.Sprintf("%s?%s", authorizePath(c), c.Ctx.Request.URL.RawQuery)
|
||||
target := fmt.Sprintf("/auth/login?redirect=%s&client_id=%s",
|
||||
base64.RawURLEncoding.EncodeToString([]byte(back)), clientID)
|
||||
c.Redirect(target, 302)
|
||||
return
|
||||
}
|
||||
|
||||
session, err := authsvc.GetSession(sid)
|
||||
if err != nil {
|
||||
clearSessionCookie(c)
|
||||
back := fmt.Sprintf("%s?%s", authorizePath(c), c.Ctx.Request.URL.RawQuery)
|
||||
target := fmt.Sprintf("/auth/login?redirect=%s&client_id=%s",
|
||||
base64.RawURLEncoding.EncodeToString([]byte(back)), clientID)
|
||||
c.Redirect(target, 302)
|
||||
return
|
||||
}
|
||||
|
||||
// 已登录但未选择企业:跳登录页的企业选择步骤
|
||||
if session.Tid == authsvc.PendingTenantID {
|
||||
back := fmt.Sprintf("%s?%s", authorizePath(c), c.Ctx.Request.URL.RawQuery)
|
||||
target := fmt.Sprintf("/auth/login?step=tenant&redirect=%s&client_id=%s",
|
||||
base64.RawURLEncoding.EncodeToString([]byte(back)), clientID)
|
||||
c.Redirect(target, 302)
|
||||
return
|
||||
}
|
||||
|
||||
code, err := issueAuthCode(client.ClientID, session.IdentityID, session.Tid, redirectURI, challenge, challengeMethod, scope, nonce)
|
||||
if err != nil {
|
||||
c.Ctx.Output.SetStatus(500)
|
||||
_, _ = c.Ctx.ResponseWriter.Write([]byte("签发授权码失败"))
|
||||
return
|
||||
}
|
||||
|
||||
sep := "?"
|
||||
if strings.Contains(redirectURI, "?") {
|
||||
sep = "&"
|
||||
}
|
||||
c.Redirect(fmt.Sprintf("%s%scode=%s&state=%s", redirectURI, sep, code, state), 302)
|
||||
}
|
||||
|
||||
// Token 令牌端点
|
||||
// POST /auth/token
|
||||
// - grant_type=authorization_code:code + code_verifier(PKCE)+ client_id
|
||||
// - grant_type=refresh_token:refresh_token + client_id
|
||||
func (c *AuthOidcController) Token() {
|
||||
grantType := strings.TrimSpace(c.GetString("grant_type"))
|
||||
clientID := strings.TrimSpace(c.GetString("client_id"))
|
||||
if clientID == "" {
|
||||
// 兼容表单/JSON 以外的取参方式
|
||||
clientID = strings.TrimSpace(c.Ctx.Request.FormValue("client_id"))
|
||||
}
|
||||
if grantType == "" {
|
||||
grantType = strings.TrimSpace(c.Ctx.Request.FormValue("grant_type"))
|
||||
}
|
||||
|
||||
switch grantType {
|
||||
case GrantAuthCode:
|
||||
code := strings.TrimSpace(c.GetString("code"))
|
||||
verifier := strings.TrimSpace(c.GetString("code_verifier"))
|
||||
if code == "" || verifier == "" || clientID == "" {
|
||||
c.fail(400, "invalid_request")
|
||||
return
|
||||
}
|
||||
pair, claims, err := exchangeCode(code, verifier, clientID)
|
||||
if err != nil {
|
||||
c.fail(400, err.Error())
|
||||
return
|
||||
}
|
||||
idToken, _ := buildIDToken(claims, clientID)
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"access_token": pair.AccessToken,
|
||||
"refresh_token": pair.RefreshToken,
|
||||
"token_type": pair.TokenType,
|
||||
"expires_in": pair.ExpiresIn,
|
||||
"id_token": idToken,
|
||||
"sid": pair.Sid,
|
||||
})
|
||||
case GrantRefresh:
|
||||
refresh := strings.TrimSpace(c.GetString("refresh_token"))
|
||||
if refresh == "" || clientID == "" {
|
||||
c.fail(400, "invalid_request")
|
||||
return
|
||||
}
|
||||
pair, err := authsvc.RefreshTokens(refresh, clientID)
|
||||
if err != nil {
|
||||
c.fail(400, err.Error())
|
||||
return
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"access_token": pair.AccessToken,
|
||||
"refresh_token": pair.RefreshToken,
|
||||
"token_type": pair.TokenType,
|
||||
"expires_in": pair.ExpiresIn,
|
||||
"sid": pair.Sid,
|
||||
})
|
||||
default:
|
||||
c.fail(400, "unsupported_grant_type")
|
||||
}
|
||||
}
|
||||
|
||||
// UserInfo 用户信息端点,需 Bearer Token
|
||||
// GET /auth/userinfo
|
||||
func (c *AuthOidcController) UserInfo() {
|
||||
claims := claimsFromHeader(c)
|
||||
if claims == nil {
|
||||
c.Ctx.Output.SetStatus(401)
|
||||
c.serveJSON(map[string]interface{}{"error": "invalid_token"})
|
||||
return
|
||||
}
|
||||
if claims.UserID <= 0 {
|
||||
c.fail(401, "invalid_token")
|
||||
return
|
||||
}
|
||||
var identity models.AuthIdentity
|
||||
if err := models.Orm.QueryTable(new(models.AuthIdentity)).
|
||||
Filter("id", claims.UserID).One(&identity); err != nil {
|
||||
c.fail(401, "invalid_token")
|
||||
return
|
||||
}
|
||||
profile, err := authsvc.BuildProfile(&identity)
|
||||
if err != nil {
|
||||
c.fail(500, "server_error")
|
||||
return
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"sub": fmt.Sprintf("%d", identity.ID),
|
||||
"union_id": identity.UnionID,
|
||||
"tid": claims.TenantId,
|
||||
"nickname": profile.Nickname,
|
||||
"mobile": profile.Mobile,
|
||||
"email": profile.Email,
|
||||
"avatar": profile.Avatar,
|
||||
"tenants": profile.Tenants,
|
||||
"client_id": claims.ClientID,
|
||||
})
|
||||
}
|
||||
|
||||
// Introspect 令牌校验,供资源服务(各业务后端)调用
|
||||
// POST /auth/token 之外的独立端点;比本地 JWKS 验签更实时(可查黑名单)
|
||||
func (c *AuthOidcController) Introspect() {
|
||||
token := strings.TrimSpace(c.GetString("token"))
|
||||
if token == "" {
|
||||
token = strings.TrimSpace(c.Ctx.Request.FormValue("token"))
|
||||
}
|
||||
if token == "" {
|
||||
c.fail(400, "invalid_request")
|
||||
return
|
||||
}
|
||||
claims, err := jwtutil.ParseToken(token)
|
||||
if err != nil {
|
||||
c.serveJSON(map[string]interface{}{"active": false})
|
||||
return
|
||||
}
|
||||
if authsvc.IsBlacklisted(claims.ID) {
|
||||
c.serveJSON(map[string]interface{}{"active": false})
|
||||
return
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"active": true,
|
||||
"sub": claims.Subject,
|
||||
"user_id": claims.UserID,
|
||||
"tid": claims.TenantId,
|
||||
"client_id": claims.ClientID,
|
||||
"sid": claims.Sid,
|
||||
"scope": claims.Scope,
|
||||
"amr": claims.Amr,
|
||||
"exp": claims.ExpiresAt.Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
// Revoke 吊销令牌(登出/踢下线)
|
||||
// POST /auth/revoke
|
||||
func (c *AuthOidcController) Revoke() {
|
||||
token := strings.TrimSpace(c.GetString("token"))
|
||||
refresh := strings.TrimSpace(c.GetString("refresh_token"))
|
||||
if token == "" {
|
||||
token = strings.TrimSpace(c.Ctx.Request.FormValue("token"))
|
||||
}
|
||||
if token == "" && refresh == "" {
|
||||
c.fail(400, "invalid_request")
|
||||
return
|
||||
}
|
||||
_ = authsvc.RevokeTokenPair(refresh, token, models.RevokeReasonLogout)
|
||||
c.serveJSON(map[string]interface{}{"code": 200, "msg": "已吊销"})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 内部工具
|
||||
|
||||
func authorizePath(c *AuthOidcController) string {
|
||||
return "/auth/authorize"
|
||||
}
|
||||
|
||||
// clearSessionCookie 清除认证中心会话 Cookie
|
||||
func clearSessionCookie(c *AuthOidcController) {
|
||||
c.Ctx.Output.Header("Set-Cookie",
|
||||
fmt.Sprintf("%s=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Lax", sessionCookieName))
|
||||
}
|
||||
|
||||
// setSessionCookie 写入认证中心会话 Cookie
|
||||
func setSessionCookie(c *AuthOidcController, sid string) {
|
||||
c.Ctx.Output.Header("Set-Cookie",
|
||||
fmt.Sprintf("%s=%s; Path=/; Max-Age=%d; HttpOnly; Secure; SameSite=Lax",
|
||||
sessionCookieName, sid, sessionCookieTTL))
|
||||
}
|
||||
|
||||
func bearerToken(c *AuthOidcController) string {
|
||||
header := c.Ctx.Request.Header.Get("Authorization")
|
||||
if header == "" {
|
||||
return ""
|
||||
}
|
||||
parts := strings.SplitN(header, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(parts[1])
|
||||
}
|
||||
|
||||
func claimsFromHeader(c *AuthOidcController) *jwtutil.Claims {
|
||||
token := bearerToken(c)
|
||||
if token == "" {
|
||||
return nil
|
||||
}
|
||||
claims, err := jwtutil.ParseToken(token)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return claims
|
||||
}
|
||||
|
||||
// findClient 查询启用状态的应用
|
||||
func findClient(clientID string) (*models.AuthClient, error) {
|
||||
var client models.AuthClient
|
||||
err := models.Orm.QueryTable(new(models.AuthClient)).
|
||||
Filter("client_id", clientID).
|
||||
Filter("status", 1).
|
||||
One(&client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &client, nil
|
||||
}
|
||||
|
||||
// allowRedirect 校验回跳地址是否在白名单内(精确匹配,防钓鱼)
|
||||
func allowRedirect(client *models.AuthClient, uri string) bool {
|
||||
if client.RedirectURIs == nil || *client.RedirectURIs == "" {
|
||||
return false
|
||||
}
|
||||
var list []string
|
||||
if err := json.Unmarshal([]byte(*client.RedirectURIs), &list); err != nil {
|
||||
return false
|
||||
}
|
||||
for _, item := range list {
|
||||
if strings.TrimSpace(item) == strings.TrimSpace(uri) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// issueAuthCode 生成一次性授权码(明文返回,库中只存哈希)
|
||||
func issueAuthCode(clientID string, identityID, tid uint64, redirectURI, challenge, method, scope, nonce string) (string, error) {
|
||||
plain, err := randomString(32)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sum := sha256.Sum256([]byte(plain))
|
||||
code := &models.AuthCode{
|
||||
CodeHash: fmt.Sprintf("%x", sum[:]),
|
||||
ClientID: clientID,
|
||||
IdentityID: identityID,
|
||||
Tid: tid,
|
||||
RedirectURI: redirectURI,
|
||||
CodeChallenge: challenge,
|
||||
CodeChallengeMethod: method,
|
||||
ExpiresAt: time.Now().Add(60 * time.Second),
|
||||
}
|
||||
if scope != "" {
|
||||
code.Scope = &scope
|
||||
}
|
||||
if nonce != "" {
|
||||
code.Nonce = &nonce
|
||||
}
|
||||
if _, err := models.Orm.Insert(code); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return plain, nil
|
||||
}
|
||||
|
||||
// exchangeCode 用授权码换令牌(校验 PKCE、一次性、有效期)
|
||||
func exchangeCode(code, verifier, clientID string) (*authsvc.TokenPair, *jwtutil.Claims, error) {
|
||||
sum := sha256.Sum256([]byte(code))
|
||||
var stored models.AuthCode
|
||||
if err := models.Orm.QueryTable(new(models.AuthCode)).
|
||||
Filter("code_hash", fmt.Sprintf("%x", sum[:])).One(&stored); err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid_grant")
|
||||
}
|
||||
if stored.Used != 0 || stored.ExpiresAt.Before(time.Now()) {
|
||||
return nil, nil, fmt.Errorf("invalid_grant")
|
||||
}
|
||||
if stored.ClientID != clientID {
|
||||
return nil, nil, fmt.Errorf("invalid_client")
|
||||
}
|
||||
if !verifyPKCE(verifier, stored.CodeChallenge, stored.CodeChallengeMethod) {
|
||||
return nil, nil, fmt.Errorf("invalid_grant")
|
||||
}
|
||||
// 一次性:立即标记已用
|
||||
_, _ = models.Orm.QueryTable(new(models.AuthCode)).
|
||||
Filter("code_hash", stored.CodeHash).
|
||||
Update(map[string]interface{}{"used": 1})
|
||||
|
||||
session, err := authsvc.CreateSession(authsvc.SessionInfo{
|
||||
IdentityID: stored.IdentityID,
|
||||
Tid: stored.Tid,
|
||||
ClientID: clientID,
|
||||
LoginType: authsvc.LoginTypePassword,
|
||||
Amr: authsvc.AmrPwd,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
client, err := findClient(clientID)
|
||||
accessTTL := 1800
|
||||
refreshTTL := 2592000
|
||||
if err == nil {
|
||||
accessTTL = client.AccessTTL
|
||||
refreshTTL = client.RefreshTTL
|
||||
}
|
||||
|
||||
pair, err := authsvc.IssueTokens(authsvc.TokenIssue{
|
||||
IdentityID: stored.IdentityID,
|
||||
Tid: stored.Tid,
|
||||
ClientID: clientID,
|
||||
Sid: session.Sid,
|
||||
UserType: "tenant",
|
||||
Amr: authsvc.AmrPwd,
|
||||
AccessTTL: accessTTL,
|
||||
RefreshTTL: refreshTTL,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
claims, err := jwtutil.ParseToken(pair.AccessToken)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return pair, claims, nil
|
||||
}
|
||||
|
||||
// verifyPKCE 校验 PKCE(S256 或 plain)
|
||||
func verifyPKCE(verifier, challenge, method string) bool {
|
||||
if challenge == "" {
|
||||
return false
|
||||
}
|
||||
if method == "S256" {
|
||||
sum := sha256.Sum256([]byte(verifier))
|
||||
return base64.RawURLEncoding.EncodeToString(sum[:]) == challenge
|
||||
}
|
||||
return verifier == challenge
|
||||
}
|
||||
|
||||
// buildIDToken 生成 OIDC ID Token
|
||||
func buildIDToken(claims *jwtutil.Claims, clientID string) (string, error) {
|
||||
return jwtutil.SignToken(jwtutil.TokenOptions{
|
||||
Alg: jwtutil.AlgRS256,
|
||||
UserID: claims.UserID,
|
||||
TenantID: claims.TenantId,
|
||||
UserType: claims.UserType,
|
||||
ClientID: clientID,
|
||||
Sid: claims.Sid,
|
||||
Subject: claims.Subject,
|
||||
Audience: []string{clientID},
|
||||
Amr: claims.Amr,
|
||||
TTL: time.Hour,
|
||||
})
|
||||
}
|
||||
|
||||
// randomString 生成随机串
|
||||
func randomString(n int) (string, error) {
|
||||
buf := make([]byte, n)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(buf), nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,362 +1,436 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/passwordutil"
|
||||
"server/services"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// PlatformTenantUserController 平台租户用户绑定管理
|
||||
type PlatformTenantUserController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
type tenantUserPayload struct {
|
||||
Tid uint64 `json:"tid"`
|
||||
Uid uint64 `json:"uid"`
|
||||
Account *string `json:"account"`
|
||||
Name *string `json:"name"`
|
||||
Phone *string `json:"phone"`
|
||||
Email *string `json:"email"`
|
||||
Password *string `json:"password"`
|
||||
IsDefault *int8 `json:"is_default"`
|
||||
Status *int8 `json:"status"`
|
||||
OrgID *uint64 `json:"org_id"`
|
||||
Remark *string `json:"remark"`
|
||||
}
|
||||
|
||||
// GetTenantUserList 获取绑定列表(支持按 tid / uid 过滤,keyword 对姓名/手机/邮箱/账号模糊匹配)
|
||||
// GET /platform/tenantUser/list?tid=1&uid=2&keyword=xxx
|
||||
func (c *PlatformTenantUserController) GetTenantUserList() {
|
||||
tid, _ := c.GetUint64("tid")
|
||||
uid, _ := c.GetUint64("uid")
|
||||
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||
|
||||
qs := models.Orm.QueryTable(new(models.SystemTenantUser))
|
||||
|
||||
var cond *orm.Condition
|
||||
needCond := false
|
||||
if tid > 0 {
|
||||
if cond == nil {
|
||||
cond = orm.NewCondition()
|
||||
}
|
||||
cond = cond.And("tid", tid)
|
||||
needCond = true
|
||||
}
|
||||
if uid > 0 {
|
||||
if cond == nil {
|
||||
cond = orm.NewCondition()
|
||||
}
|
||||
cond = cond.And("uid", uid)
|
||||
needCond = true
|
||||
}
|
||||
if keyword != "" {
|
||||
kwCond := orm.NewCondition()
|
||||
kwCond = kwCond.Or("name__icontains", keyword).
|
||||
Or("phone__icontains", keyword).
|
||||
Or("email__icontains", keyword).
|
||||
Or("account__icontains", keyword)
|
||||
if cond == nil {
|
||||
cond = kwCond
|
||||
} else {
|
||||
cond = cond.AndCond(kwCond)
|
||||
}
|
||||
needCond = true
|
||||
}
|
||||
if needCond {
|
||||
qs = qs.SetCond(cond)
|
||||
}
|
||||
|
||||
var rows []models.SystemTenantUser
|
||||
_, err := qs.OrderBy("-is_default", "-id").All(&rows)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "查询失败: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"list": rows,
|
||||
"total": len(rows),
|
||||
},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetTenantUsersByTid 兼容旧路由,根据租户 ID 获取租户用户列表
|
||||
// GET /platform/getTenantUsers/:tid
|
||||
func (c *PlatformTenantUserController) GetTenantUsersByTid() {
|
||||
tidStr := c.Ctx.Input.Param(":tid")
|
||||
tid, _ := strconv.ParseUint(tidStr, 10, 64)
|
||||
if tid == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "tid 不能为空"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
var rows []models.SystemTenantUser
|
||||
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("tid", tid).
|
||||
OrderBy("-is_default", "-id").
|
||||
All(&rows)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "查询失败: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{"list": rows, "total": len(rows)},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetTenantUserDetail 获取绑定详情
|
||||
// GET /platform/tenantUser/detail/:id
|
||||
func (c *PlatformTenantUserController) GetTenantUserDetail() {
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "无效ID"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
var row models.SystemTenantUser
|
||||
err = models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("id", id).One(&row)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 404, "msg": "记录不存在"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": row}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// CreateTenantUser 创建租户用户绑定(写入 yz_system_tenant_user;uid 为空时自动生成)
|
||||
// POST /platform/tenantUser/create
|
||||
func (c *PlatformTenantUserController) CreateTenantUser() {
|
||||
p, ok := c.parsePayload()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if p.Tid == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "tid 不能为空"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if p.Account == nil || strings.TrimSpace(*p.Account) == "" {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "account 不能为空"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if p.Password == nil || strings.TrimSpace(*p.Password) == "" {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "password 不能为空"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
hashed, err := passwordutil.Hash(*p.Password)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
p.Password = &hashed
|
||||
if p.Uid == 0 {
|
||||
uid, err := generateTenantUID(p.Tid)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "生成租户用户ID失败"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
p.Uid = uid
|
||||
}
|
||||
|
||||
// 用户数配额校验:租户用户数达到上限时不允许新增(增购用户数后放开)
|
||||
if check, err := services.CheckTenantUserQuota(p.Tid, p.Uid); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "用户数校验失败: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
} else if !check.Allowed {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 400,
|
||||
"msg": fmt.Sprintf("该租户用户数已达上限(%d/%d),请先为租户增购用户数", check.Used, check.Quota),
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
isDefault := int8(0)
|
||||
status := int8(1)
|
||||
if p.IsDefault != nil {
|
||||
isDefault = *p.IsDefault
|
||||
}
|
||||
if p.Status != nil {
|
||||
status = *p.Status
|
||||
}
|
||||
|
||||
id, err := services.BindTenantUser(p.Tid, p.Uid, p.Account, p.Name, p.Phone, p.Email, nil, nil, p.Password, isDefault, status, p.Remark)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "创建失败: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if p.OrgID != nil && *p.OrgID > 0 {
|
||||
_, _ = models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("id", id).
|
||||
Update(map[string]interface{}{"org_id": *p.OrgID})
|
||||
}
|
||||
if isDefault == 1 {
|
||||
_ = services.SetDefaultTenant(p.Uid, p.Tid)
|
||||
// 租户的第一个账号即租户管理员:自动绑定全局「租户管理员」角色(拥有 backend 全权限)。
|
||||
if adminRoleID, err := models.GetTenantAdminRole(); err == nil && adminRoleID > 0 {
|
||||
_, _ = models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("id", id).
|
||||
Update(map[string]interface{}{"group_id": adminRoleID})
|
||||
}
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": map[string]interface{}{"id": id}}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// EditTenantUser 编辑绑定
|
||||
// POST /platform/tenantUser/edit/:id
|
||||
func (c *PlatformTenantUserController) EditTenantUser() {
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "无效ID"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
p, ok := c.parsePayload()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
update := map[string]interface{}{}
|
||||
if p.Tid > 0 {
|
||||
update["tid"] = p.Tid
|
||||
}
|
||||
if p.Uid > 0 {
|
||||
update["uid"] = p.Uid
|
||||
}
|
||||
if p.Account != nil {
|
||||
update["account"] = p.Account
|
||||
}
|
||||
if p.Name != nil {
|
||||
update["name"] = p.Name
|
||||
}
|
||||
if p.Phone != nil {
|
||||
update["phone"] = p.Phone
|
||||
}
|
||||
if p.Email != nil {
|
||||
update["email"] = p.Email
|
||||
}
|
||||
if p.Password != nil {
|
||||
hashed, err := passwordutil.Hash(*p.Password)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
update["password"] = hashed
|
||||
}
|
||||
if p.IsDefault != nil {
|
||||
update["is_default"] = *p.IsDefault
|
||||
}
|
||||
if p.Status != nil {
|
||||
update["status"] = *p.Status
|
||||
}
|
||||
if p.OrgID != nil {
|
||||
update["org_id"] = *p.OrgID
|
||||
}
|
||||
if p.Remark != nil {
|
||||
update["remark"] = p.Remark
|
||||
}
|
||||
|
||||
if len(update) == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "无更新字段"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
_, err = models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("id", id).Update(update)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "更新失败: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
if p.IsDefault != nil && *p.IsDefault == 1 && p.Uid > 0 && p.Tid > 0 {
|
||||
_ = services.SetDefaultTenant(p.Uid, p.Tid)
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteTenantUser 删除绑定
|
||||
// DELETE /platform/tenantUser/delete/:id
|
||||
func (c *PlatformTenantUserController) DeleteTenantUser() {
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "无效ID"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
if err := services.UnbindTenantUser(id); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "删除失败: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *PlatformTenantUserController) parsePayload() (tenantUserPayload, bool) {
|
||||
var p tenantUserPayload
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "参数错误"}
|
||||
_ = c.ServeJSON()
|
||||
return tenantUserPayload{}, false
|
||||
}
|
||||
return p, true
|
||||
}
|
||||
|
||||
func generateTenantUID(tid uint64) (uint64, error) {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
for i := 0; i < 8; i++ {
|
||||
uid := uint64(10000000 + rand.Intn(90000000))
|
||||
cnt, err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("tid", tid).
|
||||
Filter("uid", uid).
|
||||
Count()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if cnt == 0 {
|
||||
return uid, nil
|
||||
}
|
||||
}
|
||||
return 0, errors.New("uid collision")
|
||||
}
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/passwordutil"
|
||||
"server/services"
|
||||
authsvc "server/services/auth"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// syncTenantUserToAuth 把租户用户同步到统一认证中心(双写)。
|
||||
// 同步失败只记日志,不影响平台端主流程(认证中心为新增能力)。
|
||||
func syncTenantUserToAuth(row *models.SystemTenantUser) {
|
||||
if row == nil {
|
||||
return
|
||||
}
|
||||
in := authsvc.SyncTenantUserInput{
|
||||
Tid: row.Tid,
|
||||
GroupID: row.GroupID,
|
||||
OrgID: row.OrgID,
|
||||
Status: row.Status,
|
||||
IsDefault: row.IsDefault,
|
||||
}
|
||||
if row.Account != nil {
|
||||
in.Account = strings.TrimSpace(*row.Account)
|
||||
}
|
||||
if row.Name != nil {
|
||||
in.Name = strings.TrimSpace(*row.Name)
|
||||
}
|
||||
if row.Phone != nil {
|
||||
in.Phone = strings.TrimSpace(*row.Phone)
|
||||
}
|
||||
if row.Email != nil {
|
||||
in.Email = strings.TrimSpace(*row.Email)
|
||||
}
|
||||
if row.Password != nil {
|
||||
in.PasswordHash = *row.Password
|
||||
}
|
||||
if err := authsvc.SyncTenantUser(in); err != nil {
|
||||
log.Printf("[auth] 同步租户用户到认证中心失败 tid=%d account=%s: %v", row.Tid, in.Account, err)
|
||||
}
|
||||
}
|
||||
|
||||
// removeTenantUserFromAuth 删除认证中心中的企业绑定(保留身份)
|
||||
func removeTenantUserFromAuth(row *models.SystemTenantUser) {
|
||||
if row == nil {
|
||||
return
|
||||
}
|
||||
account, phone, email := "", "", ""
|
||||
if row.Account != nil {
|
||||
account = strings.TrimSpace(*row.Account)
|
||||
}
|
||||
if row.Phone != nil {
|
||||
phone = strings.TrimSpace(*row.Phone)
|
||||
}
|
||||
if row.Email != nil {
|
||||
email = strings.TrimSpace(*row.Email)
|
||||
}
|
||||
if err := authsvc.RemoveTenantUser(row.Tid, account, phone, email); err != nil {
|
||||
log.Printf("[auth] 删除认证中心企业绑定失败 tid=%d account=%s: %v", row.Tid, account, err)
|
||||
}
|
||||
}
|
||||
|
||||
// PlatformTenantUserController 平台租户用户绑定管理
|
||||
type PlatformTenantUserController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
type tenantUserPayload struct {
|
||||
Tid uint64 `json:"tid"`
|
||||
Uid uint64 `json:"uid"`
|
||||
Account *string `json:"account"`
|
||||
Name *string `json:"name"`
|
||||
Phone *string `json:"phone"`
|
||||
Email *string `json:"email"`
|
||||
Password *string `json:"password"`
|
||||
IsDefault *int8 `json:"is_default"`
|
||||
Status *int8 `json:"status"`
|
||||
OrgID *uint64 `json:"org_id"`
|
||||
Remark *string `json:"remark"`
|
||||
}
|
||||
|
||||
// GetTenantUserList 获取绑定列表(支持按 tid / uid 过滤,keyword 对姓名/手机/邮箱/账号模糊匹配)
|
||||
// GET /platform/tenantUser/list?tid=1&uid=2&keyword=xxx
|
||||
func (c *PlatformTenantUserController) GetTenantUserList() {
|
||||
tid, _ := c.GetUint64("tid")
|
||||
uid, _ := c.GetUint64("uid")
|
||||
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||
|
||||
qs := models.Orm.QueryTable(new(models.SystemTenantUser))
|
||||
|
||||
var cond *orm.Condition
|
||||
needCond := false
|
||||
if tid > 0 {
|
||||
if cond == nil {
|
||||
cond = orm.NewCondition()
|
||||
}
|
||||
cond = cond.And("tid", tid)
|
||||
needCond = true
|
||||
}
|
||||
if uid > 0 {
|
||||
if cond == nil {
|
||||
cond = orm.NewCondition()
|
||||
}
|
||||
cond = cond.And("uid", uid)
|
||||
needCond = true
|
||||
}
|
||||
if keyword != "" {
|
||||
kwCond := orm.NewCondition()
|
||||
kwCond = kwCond.Or("name__icontains", keyword).
|
||||
Or("phone__icontains", keyword).
|
||||
Or("email__icontains", keyword).
|
||||
Or("account__icontains", keyword)
|
||||
if cond == nil {
|
||||
cond = kwCond
|
||||
} else {
|
||||
cond = cond.AndCond(kwCond)
|
||||
}
|
||||
needCond = true
|
||||
}
|
||||
if needCond {
|
||||
qs = qs.SetCond(cond)
|
||||
}
|
||||
|
||||
var rows []models.SystemTenantUser
|
||||
_, err := qs.OrderBy("-is_default", "-id").All(&rows)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "查询失败: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"list": rows,
|
||||
"total": len(rows),
|
||||
},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetTenantUsersByTid 兼容旧路由,根据租户 ID 获取租户用户列表
|
||||
// GET /platform/getTenantUsers/:tid
|
||||
func (c *PlatformTenantUserController) GetTenantUsersByTid() {
|
||||
tidStr := c.Ctx.Input.Param(":tid")
|
||||
tid, _ := strconv.ParseUint(tidStr, 10, 64)
|
||||
if tid == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "tid 不能为空"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
var rows []models.SystemTenantUser
|
||||
_, err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("tid", tid).
|
||||
OrderBy("-is_default", "-id").
|
||||
All(&rows)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "查询失败: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{"list": rows, "total": len(rows)},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetTenantUserDetail 获取绑定详情
|
||||
// GET /platform/tenantUser/detail/:id
|
||||
func (c *PlatformTenantUserController) GetTenantUserDetail() {
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "无效ID"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
var row models.SystemTenantUser
|
||||
err = models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("id", id).One(&row)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 404, "msg": "记录不存在"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": row}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// CreateTenantUser 创建租户用户绑定(写入 yz_system_tenant_user;uid 为空时自动生成)
|
||||
// POST /platform/tenantUser/create
|
||||
func (c *PlatformTenantUserController) CreateTenantUser() {
|
||||
p, ok := c.parsePayload()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if p.Tid == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "tid 不能为空"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if p.Account == nil || strings.TrimSpace(*p.Account) == "" {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "account 不能为空"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if p.Password == nil || strings.TrimSpace(*p.Password) == "" {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "password 不能为空"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
hashed, err := passwordutil.Hash(*p.Password)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
p.Password = &hashed
|
||||
if p.Uid == 0 {
|
||||
uid, err := generateTenantUID(p.Tid)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "生成租户用户ID失败"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
p.Uid = uid
|
||||
}
|
||||
|
||||
// 用户数配额校验:租户用户数达到上限时不允许新增(增购用户数后放开)
|
||||
if check, err := services.CheckTenantUserQuota(p.Tid, p.Uid); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "用户数校验失败: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
} else if !check.Allowed {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 400,
|
||||
"msg": fmt.Sprintf("该租户用户数已达上限(%d/%d),请先为租户增购用户数", check.Used, check.Quota),
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
isDefault := int8(0)
|
||||
status := int8(1)
|
||||
if p.IsDefault != nil {
|
||||
isDefault = *p.IsDefault
|
||||
}
|
||||
if p.Status != nil {
|
||||
status = *p.Status
|
||||
}
|
||||
|
||||
id, err := services.BindTenantUser(p.Tid, p.Uid, p.Account, p.Name, p.Phone, p.Email, nil, nil, p.Password, isDefault, status, p.Remark)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "创建失败: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if p.OrgID != nil && *p.OrgID > 0 {
|
||||
_, _ = models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("id", id).
|
||||
Update(map[string]interface{}{"org_id": *p.OrgID})
|
||||
}
|
||||
if isDefault == 1 {
|
||||
_ = services.SetDefaultTenant(p.Uid, p.Tid)
|
||||
// 租户的第一个账号即租户管理员:自动绑定全局「租户管理员」角色(拥有 backend 全权限)。
|
||||
if adminRoleID, err := models.GetTenantAdminRole(); err == nil && adminRoleID > 0 {
|
||||
_, _ = models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("id", id).
|
||||
Update(map[string]interface{}{"group_id": adminRoleID})
|
||||
}
|
||||
}
|
||||
|
||||
// 双写:同步到统一认证中心(读回最新记录,确保 group_id/org_id 已落库)
|
||||
var created models.SystemTenantUser
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("id", id).One(&created); err == nil {
|
||||
syncTenantUserToAuth(&created)
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": map[string]interface{}{"id": id}}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// EditTenantUser 编辑绑定
|
||||
// POST /platform/tenantUser/edit/:id
|
||||
func (c *PlatformTenantUserController) EditTenantUser() {
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "无效ID"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
p, ok := c.parsePayload()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
update := map[string]interface{}{}
|
||||
if p.Tid > 0 {
|
||||
update["tid"] = p.Tid
|
||||
}
|
||||
if p.Uid > 0 {
|
||||
update["uid"] = p.Uid
|
||||
}
|
||||
if p.Account != nil {
|
||||
update["account"] = p.Account
|
||||
}
|
||||
if p.Name != nil {
|
||||
update["name"] = p.Name
|
||||
}
|
||||
if p.Phone != nil {
|
||||
update["phone"] = p.Phone
|
||||
}
|
||||
if p.Email != nil {
|
||||
update["email"] = p.Email
|
||||
}
|
||||
if p.Password != nil {
|
||||
hashed, err := passwordutil.Hash(*p.Password)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
update["password"] = hashed
|
||||
}
|
||||
if p.IsDefault != nil {
|
||||
update["is_default"] = *p.IsDefault
|
||||
}
|
||||
if p.Status != nil {
|
||||
update["status"] = *p.Status
|
||||
}
|
||||
if p.OrgID != nil {
|
||||
update["org_id"] = *p.OrgID
|
||||
}
|
||||
if p.Remark != nil {
|
||||
update["remark"] = p.Remark
|
||||
}
|
||||
|
||||
if len(update) == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "无更新字段"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
_, err = models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("id", id).Update(update)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "更新失败: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
if p.IsDefault != nil && *p.IsDefault == 1 && p.Uid > 0 && p.Tid > 0 {
|
||||
_ = services.SetDefaultTenant(p.Uid, p.Tid)
|
||||
}
|
||||
|
||||
// 双写:同步到统一认证中心
|
||||
var updated models.SystemTenantUser
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("id", id).One(&updated); err == nil {
|
||||
syncTenantUserToAuth(&updated)
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteTenantUser 删除绑定
|
||||
// DELETE /platform/tenantUser/delete/:id
|
||||
func (c *PlatformTenantUserController) DeleteTenantUser() {
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "无效ID"}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 删除前先读出记录,供认证中心侧定位(删除后就读不到了)
|
||||
var row models.SystemTenantUser
|
||||
_ = models.Orm.QueryTable(new(models.SystemTenantUser)).Filter("id", id).One(&row)
|
||||
|
||||
if err := services.UnbindTenantUser(id); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "删除失败: " + err.Error()}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 双写:删除认证中心中的企业绑定
|
||||
removeTenantUserFromAuth(&row)
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
func (c *PlatformTenantUserController) parsePayload() (tenantUserPayload, bool) {
|
||||
var p tenantUserPayload
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "参数错误"}
|
||||
_ = c.ServeJSON()
|
||||
return tenantUserPayload{}, false
|
||||
}
|
||||
return p, true
|
||||
}
|
||||
|
||||
func generateTenantUID(tid uint64) (uint64, error) {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
for i := 0; i < 8; i++ {
|
||||
uid := uint64(10000000 + rand.Intn(90000000))
|
||||
cnt, err := models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("tid", tid).
|
||||
Filter("uid", uid).
|
||||
Count()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if cnt == 0 {
|
||||
return uid, nil
|
||||
}
|
||||
}
|
||||
return 0, errors.New("uid collision")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user