Files
ai_site/platform/internal/userstore/store.go
2026-07-31 10:19:22 +08:00

1537 lines
43 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package userstore
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
"sync"
"time"
"golang.org/x/crypto/bcrypt"
"aijianzhan/platform/internal/authx"
)
const (
StatusPending = "pending"
StatusActive = "active"
RolePending = authx.Role待加入
)
type User struct {
UserID int64
TenantID int64 // 0 = 未加入任何租户
OrgUnitID int64 // 0 = 未绑定组织(可见租户全量,由角色决定)
Username string
Phone string // 绑定手机号,可作登录账号
UsernameLoginDisabled bool // 已绑手机时可禁用「用户名+密码」登录
DisplayName string
Role string
Status string
CreatedAt time.Time
}
func (u *User) HasTenant() bool {
return u != nil && u.TenantID > 0 && u.Status == StatusActive && u.Role != RolePending
}
func (u *User) IsPlatformAdmin() bool {
return u != nil && authx.IsPlatformAdmin(u.Role)
}
type TenantInfo struct {
TenantID int64 `json:"tenant_id"`
Name string `json:"name"`
Slug string `json:"slug"` // 全局唯一路径前缀,如 aaa → /aaa/
CreatedAt time.Time `json:"created_at"`
UserCount int64 `json:"user_count"`
AppCount int64 `json:"app_count"`
}
type Store interface {
Register(ctx context.Context, username, password, displayName string) (*User, error)
EnsureBootstrapOwner(ctx context.Context, username, password, displayName, companyName string) (*User, error)
EnsurePlatformAdmin(ctx context.Context, username, password, displayName string) (*User, error)
Login(ctx context.Context, username, password string) (*User, error)
GetByID(ctx context.Context, userID int64) (*User, error)
GetByPhone(ctx context.Context, phone string) (*User, error)
BindPhone(ctx context.Context, userID int64, phone string) (*User, error)
SetUsernameLoginDisabled(ctx context.Context, userID int64, disabled bool) (*User, error)
PhoneExists(ctx context.Context, phone string, excludeUserID int64) (bool, error)
JoinTenant(ctx context.Context, userID, tenantID int64, role string, orgUnitID int64) (*User, error)
CreateTenantAsOwner(ctx context.Context, userID int64, tenantName string) (*User, error)
SetOrgUnit(ctx context.Context, userID, tenantID, orgUnitID int64) (*User, error)
ListTenants(ctx context.Context) ([]TenantInfo, error)
CreateTenant(ctx context.Context, name, slug string) (*TenantInfo, error)
GetTenant(ctx context.Context, tenantID int64) (*TenantInfo, error)
GetTenantBySlug(ctx context.Context, slug string) (*TenantInfo, error)
UpdateTenant(ctx context.Context, tenantID int64, name, slug string) (*TenantInfo, error)
RenameTenantIf(ctx context.Context, oldName, newName string) error
EnsureTenantSlugs(ctx context.Context) error
ListMembers(ctx context.Context, tenantID int64) ([]User, error)
UpdateMember(ctx context.Context, tenantID, userID int64, role string, orgUnitID int64, status string) (*User, error)
// CreateMember 在指定公司直接创建登录账号password 为空则随机生成。返回明文密码(仅此一次)。
CreateMember(ctx context.Context, tenantID int64, username, password, displayName, role string, orgUnitID int64) (*User, string, error)
UsernameExists(ctx context.Context, username string) (bool, error)
// ChangePassword 校验旧密码后设置新密码(用户自行改密)。
ChangePassword(ctx context.Context, userID int64, oldPassword, newPassword string) error
// SetPassword 管理员强制设密password 为空则随机生成。返回明文密码。
SetPassword(ctx context.Context, userID int64, password string) (string, error)
}
type MemoryStore struct {
mu sync.Mutex
users map[string]*memUser
byID map[int64]*memUser
tenants map[int64]*TenantInfo
seqUser int64
seqTen int64
}
type memUser struct {
User
Hash string
}
func NewMemoryStore() *MemoryStore {
return &MemoryStore{
users: map[string]*memUser{},
byID: map[int64]*memUser{},
tenants: map[int64]*TenantInfo{},
}
}
func (s *MemoryStore) Register(_ context.Context, username, password, displayName string) (*User, error) {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.users[username]; ok {
return nil, fmt.Errorf("username already exists")
}
if len(username) < 3 || len(password) < 6 {
return nil, fmt.Errorf("username>=3 and password>=6 required")
}
hash, err := hashPassword(password)
if err != nil {
return nil, err
}
name := displayName
if name == "" {
name = username
}
s.seqUser++
u := &memUser{
User: User{
UserID: s.seqUser, TenantID: 0, Username: username,
DisplayName: name, Role: RolePending, Status: StatusPending, CreatedAt: time.Now().UTC(),
},
Hash: hash,
}
s.users[username] = u
s.byID[u.UserID] = u
cp := u.User
return &cp, nil
}
func (s *MemoryStore) EnsureBootstrapOwner(_ context.Context, username, password, displayName, companyName string) (*User, error) {
s.mu.Lock()
defer s.mu.Unlock()
if existing, ok := s.users[username]; ok {
cp := existing.User
return &cp, nil
}
hash, err := hashPassword(password)
if err != nil {
return nil, err
}
name := displayName
if name == "" {
name = username
}
tenName := strings.TrimSpace(companyName)
if tenName == "" {
tenName = name
}
s.seqTen++
s.seqUser++
u := &memUser{
User: User{
UserID: s.seqUser, TenantID: s.seqTen, Username: username,
DisplayName: name, Role: authx.Role管理员, Status: StatusActive, CreatedAt: time.Now().UTC(),
},
Hash: hash,
}
s.users[username] = u
s.byID[u.UserID] = u
s.tenants[s.seqTen] = &TenantInfo{TenantID: s.seqTen, Name: tenName, Slug: "demo", CreatedAt: time.Now().UTC()}
cp := u.User
return &cp, nil
}
func (s *MemoryStore) EnsurePlatformAdmin(_ context.Context, username, password, displayName string) (*User, error) {
s.mu.Lock()
defer s.mu.Unlock()
hash, err := hashPassword(password)
if err != nil {
return nil, err
}
name := displayName
if name == "" {
name = username
}
if existing, ok := s.users[username]; ok {
existing.Hash = hash
existing.Role = authx.Role超级管理员
existing.Status = StatusActive
existing.TenantID = 0
existing.OrgUnitID = 0
if name != "" {
existing.DisplayName = name
}
cp := existing.User
return &cp, nil
}
s.seqUser++
u := &memUser{
User: User{
UserID: s.seqUser, TenantID: 0, Username: username,
DisplayName: name, Role: authx.Role超级管理员, Status: StatusActive, CreatedAt: time.Now().UTC(),
},
Hash: hash,
}
s.users[username] = u
s.byID[u.UserID] = u
cp := u.User
return &cp, nil
}
func (s *MemoryStore) ListTenants(_ context.Context) ([]TenantInfo, error) {
s.mu.Lock()
defer s.mu.Unlock()
out := make([]TenantInfo, 0, len(s.tenants))
for _, t := range s.tenants {
info := *t
var uc int64
for _, u := range s.users {
if u.TenantID == t.TenantID && u.Status == StatusActive {
uc++
}
}
info.UserCount = uc
out = append(out, info)
}
return out, nil
}
func (s *MemoryStore) GetTenant(_ context.Context, tenantID int64) (*TenantInfo, error) {
s.mu.Lock()
defer s.mu.Unlock()
t, ok := s.tenants[tenantID]
if !ok {
return nil, fmt.Errorf("tenant not found")
}
cp := *t
return &cp, nil
}
func (s *MemoryStore) CreateTenant(_ context.Context, name, slug string) (*TenantInfo, error) {
s.mu.Lock()
defer s.mu.Unlock()
name = strings.TrimSpace(name)
if name == "" {
return nil, fmt.Errorf("name required")
}
if strings.TrimSpace(slug) == "" {
slug = SuggestTenantSlug(name)
}
ns, err := NormalizeTenantSlug(slug)
if err != nil {
return nil, err
}
for _, t := range s.tenants {
if t.Slug == ns {
return nil, fmt.Errorf("slug already exists: %s", ns)
}
}
s.seqTen++
t := &TenantInfo{TenantID: s.seqTen, Name: name, Slug: ns, CreatedAt: time.Now().UTC()}
s.tenants[s.seqTen] = t
cp := *t
return &cp, nil
}
func (s *MemoryStore) UpdateTenant(_ context.Context, tenantID int64, name, slug string) (*TenantInfo, error) {
s.mu.Lock()
defer s.mu.Unlock()
t, ok := s.tenants[tenantID]
if !ok {
return nil, fmt.Errorf("tenant not found")
}
name = strings.TrimSpace(name)
if name == "" {
return nil, fmt.Errorf("name required")
}
t.Name = name
if strings.TrimSpace(slug) != "" {
ns, err := NormalizeTenantSlug(slug)
if err != nil {
return nil, err
}
for id, other := range s.tenants {
if id != tenantID && other.Slug == ns {
return nil, fmt.Errorf("slug already exists: %s", ns)
}
}
t.Slug = ns
}
cp := *t
return &cp, nil
}
func (s *MemoryStore) GetTenantBySlug(_ context.Context, slug string) (*TenantInfo, error) {
s.mu.Lock()
defer s.mu.Unlock()
ns, err := NormalizeTenantSlug(slug)
if err != nil {
return nil, err
}
for _, t := range s.tenants {
if t.Slug == ns {
cp := *t
return &cp, nil
}
}
return nil, fmt.Errorf("tenant not found")
}
func (s *MemoryStore) EnsureTenantSlugs(_ context.Context) error {
s.mu.Lock()
defer s.mu.Unlock()
used := map[string]struct{}{}
for _, t := range s.tenants {
if t.Slug != "" {
used[t.Slug] = struct{}{}
}
}
for _, t := range s.tenants {
if t.Slug != "" {
continue
}
base := SuggestTenantSlug(t.Name)
if base == "" {
base = fmt.Sprintf("t%d", t.TenantID)
}
cand := base
for i := 2; ; i++ {
if _, ok := used[cand]; !ok {
if _, err := NormalizeTenantSlug(cand); err == nil {
break
}
}
cand = fmt.Sprintf("%s-%d", base, i)
if i > 100 {
cand = fmt.Sprintf("t%d", t.TenantID)
break
}
}
ns, err := NormalizeTenantSlug(cand)
if err != nil {
ns = fmt.Sprintf("t%d", t.TenantID)
}
t.Slug = ns
used[ns] = struct{}{}
}
return nil
}
func (s *MemoryStore) ListMembers(_ context.Context, tenantID int64) ([]User, error) {
s.mu.Lock()
defer s.mu.Unlock()
out := make([]User, 0)
for _, u := range s.users {
if u.TenantID == tenantID && !authx.IsPlatformAdmin(u.Role) {
cp := u.User
cp.Role = authx.NormalizeRole(cp.Role)
out = append(out, cp)
}
}
return out, nil
}
func (s *MemoryStore) UsernameExists(_ context.Context, username string) (bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
_, ok := s.users[strings.TrimSpace(username)]
return ok, nil
}
func (s *MemoryStore) CreateMember(_ context.Context, tenantID int64, username, password, displayName, role string, orgUnitID int64) (*User, string, error) {
s.mu.Lock()
defer s.mu.Unlock()
if tenantID <= 0 {
return nil, "", fmt.Errorf("tenant required")
}
if _, ok := s.tenants[tenantID]; !ok {
return nil, "", fmt.Errorf("tenant not found")
}
username = strings.TrimSpace(username)
if len(username) < 3 {
return nil, "", fmt.Errorf("username>=3 required")
}
if _, ok := s.users[username]; ok {
return nil, "", fmt.Errorf("username already exists")
}
plain := strings.TrimSpace(password)
var err error
if plain == "" {
plain, err = RandomPassword(12)
if err != nil {
return nil, "", err
}
}
if len(plain) < 6 {
return nil, "", fmt.Errorf("password>=6 required")
}
if !authx.ValidPlatformRole(role) {
return nil, "", fmt.Errorf("invalid role")
}
role = authx.NormalizeRole(role)
hash, err := hashPassword(plain)
if err != nil {
return nil, "", err
}
name := strings.TrimSpace(displayName)
if name == "" {
name = username
}
s.seqUser++
u := &memUser{
User: User{
UserID: s.seqUser, TenantID: tenantID, OrgUnitID: orgUnitID, Username: username,
DisplayName: name, Role: role, Status: StatusActive, CreatedAt: time.Now().UTC(),
},
Hash: hash,
}
s.users[username] = u
s.byID[u.UserID] = u
if t := s.tenants[tenantID]; t != nil {
t.UserCount++
}
cp := u.User
return &cp, plain, nil
}
func (s *MemoryStore) ChangePassword(_ context.Context, userID int64, oldPassword, newPassword string) error {
s.mu.Lock()
defer s.mu.Unlock()
u, ok := s.byID[userID]
if !ok {
return fmt.Errorf("user not found")
}
if bcrypt.CompareHashAndPassword([]byte(u.Hash), []byte(oldPassword)) != nil {
return fmt.Errorf("旧密码不正确")
}
newPassword = strings.TrimSpace(newPassword)
if len(newPassword) < 6 {
return fmt.Errorf("password>=6 required")
}
hash, err := hashPassword(newPassword)
if err != nil {
return err
}
u.Hash = hash
return nil
}
func (s *MemoryStore) SetPassword(_ context.Context, userID int64, password string) (string, error) {
s.mu.Lock()
defer s.mu.Unlock()
u, ok := s.byID[userID]
if !ok {
return "", fmt.Errorf("user not found")
}
plain := strings.TrimSpace(password)
var err error
if plain == "" {
plain, err = RandomPassword(12)
if err != nil {
return "", err
}
}
if len(plain) < 6 {
return "", fmt.Errorf("password>=6 required")
}
hash, err := hashPassword(plain)
if err != nil {
return "", err
}
u.Hash = hash
return plain, nil
}
func (s *MemoryStore) UpdateMember(_ context.Context, tenantID, userID int64, role string, orgUnitID int64, status string) (*User, error) {
s.mu.Lock()
defer s.mu.Unlock()
u, ok := s.byID[userID]
if !ok || u.TenantID != tenantID {
return nil, fmt.Errorf("member not found")
}
if authx.IsPlatformAdmin(u.Role) {
return nil, fmt.Errorf("cannot edit platform admin")
}
if role != "" {
if !authx.ValidPlatformRole(role) {
return nil, fmt.Errorf("invalid role")
}
u.Role = authx.NormalizeRole(role)
}
if status == StatusActive || status == StatusPending || status == "disabled" {
u.Status = status
}
u.OrgUnitID = orgUnitID
cp := u.User
return &cp, nil
}
func (s *MemoryStore) Login(_ context.Context, account, password string) (*User, error) {
s.mu.Lock()
defer s.mu.Unlock()
account = strings.TrimSpace(account)
u, ok := s.users[account]
if !ok {
if phone, err := NormalizePhone(account); err == nil {
for _, cand := range s.users {
if cand.Phone == phone {
u, ok = cand, true
break
}
}
}
}
if !ok || bcrypt.CompareHashAndPassword([]byte(u.Hash), []byte(password)) != nil {
return nil, fmt.Errorf("invalid username or password")
}
cp := u.User
cp.Role = authx.NormalizeRole(cp.Role)
if cp.Status == "disabled" {
return nil, fmt.Errorf("账号已停用")
}
return &cp, nil
}
func (s *MemoryStore) GetByID(_ context.Context, userID int64) (*User, error) {
s.mu.Lock()
defer s.mu.Unlock()
u, ok := s.byID[userID]
if !ok {
return nil, fmt.Errorf("user not found")
}
cp := u.User
cp.Role = authx.NormalizeRole(cp.Role)
return &cp, nil
}
func (s *MemoryStore) GetByPhone(_ context.Context, phone string) (*User, error) {
ns, err := NormalizePhone(phone)
if err != nil {
return nil, err
}
s.mu.Lock()
defer s.mu.Unlock()
for _, u := range s.users {
if u.Phone == ns {
cp := u.User
cp.Role = authx.NormalizeRole(cp.Role)
return &cp, nil
}
}
return nil, fmt.Errorf("user not found")
}
func (s *MemoryStore) PhoneExists(_ context.Context, phone string, excludeUserID int64) (bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
phone = strings.TrimSpace(phone)
if phone == "" {
return false, nil
}
for _, u := range s.users {
if u.Phone == phone && u.UserID != excludeUserID {
return true, nil
}
}
return false, nil
}
func (s *MemoryStore) BindPhone(_ context.Context, userID int64, phone string) (*User, error) {
s.mu.Lock()
defer s.mu.Unlock()
u, ok := s.byID[userID]
if !ok {
return nil, fmt.Errorf("user not found")
}
phone = strings.TrimSpace(phone)
if phone == "" {
u.Phone = ""
u.UsernameLoginDisabled = false
cp := u.User
return &cp, nil
}
ns, err := NormalizePhone(phone)
if err != nil {
return nil, err
}
for _, o := range s.users {
if o.Phone == ns && o.UserID != userID {
return nil, fmt.Errorf("该手机号已被绑定")
}
}
u.Phone = ns
cp := u.User
return &cp, nil
}
func (s *MemoryStore) SetUsernameLoginDisabled(_ context.Context, userID int64, disabled bool) (*User, error) {
s.mu.Lock()
defer s.mu.Unlock()
u, ok := s.byID[userID]
if !ok {
return nil, fmt.Errorf("user not found")
}
if disabled && strings.TrimSpace(u.Phone) == "" {
return nil, fmt.Errorf("请先绑定手机号,再禁用用户名登录")
}
u.UsernameLoginDisabled = disabled
cp := u.User
return &cp, nil
}
func (s *MemoryStore) JoinTenant(_ context.Context, userID, tenantID int64, role string, orgUnitID int64) (*User, error) {
s.mu.Lock()
defer s.mu.Unlock()
u, ok := s.byID[userID]
if !ok {
return nil, fmt.Errorf("user not found")
}
if u.TenantID > 0 && u.Status == StatusActive {
return nil, fmt.Errorf("user already joined a tenant")
}
if tenantID <= 0 {
return nil, fmt.Errorf("invalid tenant")
}
role = normalizePlatformRole(role)
u.TenantID = tenantID
u.OrgUnitID = orgUnitID
u.Role = role
u.Status = StatusActive
cp := u.User
return &cp, nil
}
func (s *MemoryStore) SetOrgUnit(_ context.Context, userID, tenantID, orgUnitID int64) (*User, error) {
s.mu.Lock()
defer s.mu.Unlock()
u, ok := s.byID[userID]
if !ok || u.TenantID != tenantID {
return nil, fmt.Errorf("user not found")
}
u.OrgUnitID = orgUnitID
cp := u.User
return &cp, nil
}
func (s *MemoryStore) CreateTenantAsOwner(_ context.Context, userID int64, tenantName string) (*User, error) {
s.mu.Lock()
defer s.mu.Unlock()
u, ok := s.byID[userID]
if !ok {
return nil, fmt.Errorf("user not found")
}
if u.TenantID > 0 && u.Status == StatusActive {
return nil, fmt.Errorf("user already joined a tenant")
}
name := strings.TrimSpace(tenantName)
if name == "" {
name = u.DisplayName
}
if name == "" {
name = u.Username
}
s.seqTen++
u.TenantID = s.seqTen
u.Role = authx.Role管理员
u.Status = StatusActive
slug := SuggestTenantSlug(name)
if slug == "" {
slug = fmt.Sprintf("t%d", s.seqTen)
}
base := slug
for i := 2; ; i++ {
clash := false
for _, t := range s.tenants {
if t.Slug == slug {
clash = true
break
}
}
if !clash {
if _, err := NormalizeTenantSlug(slug); err == nil {
break
}
}
slug = fmt.Sprintf("%s-%d", base, i)
if i > 50 {
slug = fmt.Sprintf("t%d", s.seqTen)
break
}
}
ns, _ := NormalizeTenantSlug(slug)
if ns == "" {
ns = fmt.Sprintf("t%d", s.seqTen)
}
s.tenants[s.seqTen] = &TenantInfo{TenantID: s.seqTen, Name: name, Slug: ns, CreatedAt: time.Now().UTC()}
cp := u.User
return &cp, nil
}
type PostgresStore struct {
DB *sql.DB
}
func NewPostgresStore(db *sql.DB) *PostgresStore {
return &PostgresStore{DB: db}
}
func (s *PostgresStore) Register(ctx context.Context, username, password, displayName string) (*User, error) {
if len(username) < 3 || len(password) < 6 {
return nil, fmt.Errorf("username>=3 and password>=6 required")
}
hash, err := hashPassword(password)
if err != nil {
return nil, err
}
name := displayName
if name == "" {
name = username
}
u := &User{TenantID: 0, Username: username, DisplayName: name, Role: RolePending, Status: StatusPending}
err = s.DB.QueryRowContext(ctx, `
INSERT INTO platform_meta.users(tenant_id, username, password_hash, display_name, role, status)
VALUES(NULL,$1,$2,$3,'pending','pending')
RETURNING user_id, created_at`, username, hash, name,
).Scan(&u.UserID, &u.CreatedAt)
if err != nil {
if isUnique(err) {
return nil, fmt.Errorf("username already exists")
}
return nil, err
}
return u, nil
}
func (s *PostgresStore) EnsureBootstrapOwner(ctx context.Context, username, password, displayName, companyName string) (*User, error) {
if u, err := s.Login(ctx, username, password); err == nil {
return u, nil
}
hash, err := hashPassword(password)
if err != nil {
return nil, err
}
tx, err := s.DB.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer func() { _ = tx.Rollback() }()
name := displayName
if name == "" {
name = username
}
tenName := strings.TrimSpace(companyName)
if tenName == "" {
tenName = name
}
tenSlug := "demo"
if tenName != "演示公司" && tenName != "演示账号" {
tenSlug = SuggestTenantSlug(tenName)
if tenSlug == "" {
tenSlug = "demo"
}
}
var tenantID int64
if err := tx.QueryRowContext(ctx,
`INSERT INTO platform_meta.tenants(name, slug) VALUES($1,$2) RETURNING tenant_id`, tenName, tenSlug,
).Scan(&tenantID); err != nil {
return nil, err
}
u := &User{TenantID: tenantID, Username: username, DisplayName: name, Role: authx.Role管理员, Status: StatusActive}
err = tx.QueryRowContext(ctx, `
INSERT INTO platform_meta.users(tenant_id, username, password_hash, display_name, role, status)
VALUES($1,$2,$3,$4,$5,'active')
RETURNING user_id, created_at`, tenantID, username, hash, name, authx.Role管理员,
).Scan(&u.UserID, &u.CreatedAt)
if err != nil {
if isUnique(err) {
_ = tx.Rollback()
return s.Login(ctx, username, password)
}
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
return u, nil
}
func (s *PostgresStore) EnsurePlatformAdmin(ctx context.Context, username, password, displayName string) (*User, error) {
hash, err := hashPassword(password)
if err != nil {
return nil, err
}
name := displayName
if name == "" {
name = username
}
var userID int64
err = s.DB.QueryRowContext(ctx, `SELECT user_id FROM platform_meta.users WHERE username=$1`, username).Scan(&userID)
if err == nil {
_, err = s.DB.ExecContext(ctx, `
UPDATE platform_meta.users
SET password_hash=$1, role=$2, status='active', tenant_id=NULL, org_unit_id=NULL,
display_name=CASE WHEN $3='' THEN display_name ELSE $3 END
WHERE user_id=$4`, hash, authx.Role超级管理员, name, userID)
if err != nil {
return nil, err
}
return s.GetByID(ctx, userID)
}
if !errors.Is(err, sql.ErrNoRows) {
return nil, err
}
u := &User{TenantID: 0, Username: username, DisplayName: name, Role: authx.Role超级管理员, Status: StatusActive}
err = s.DB.QueryRowContext(ctx, `
INSERT INTO platform_meta.users(tenant_id, username, password_hash, display_name, role, status)
VALUES(NULL,$1,$2,$3,$4,'active')
RETURNING user_id, created_at`, username, hash, name, authx.Role超级管理员,
).Scan(&u.UserID, &u.CreatedAt)
if err != nil {
if isUnique(err) {
// 并发创建:再走一次 upsert
return s.EnsurePlatformAdmin(ctx, username, password, displayName)
}
return nil, err
}
return u, nil
}
func (s *PostgresStore) ListTenants(ctx context.Context) ([]TenantInfo, error) {
rows, err := s.DB.QueryContext(ctx, `
SELECT t.tenant_id, t.name, COALESCE(t.slug,''), t.created_at,
(SELECT COUNT(*) FROM platform_meta.users u WHERE u.tenant_id=t.tenant_id AND COALESCE(u.status,'active')='active') AS user_count,
(SELECT COUNT(*) FROM platform_meta.tenant_apps a WHERE a.tenant_id=t.tenant_id) AS app_count
FROM platform_meta.tenants t
ORDER BY t.tenant_id`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []TenantInfo
for rows.Next() {
var t TenantInfo
if err := rows.Scan(&t.TenantID, &t.Name, &t.Slug, &t.CreatedAt, &t.UserCount, &t.AppCount); err != nil {
return nil, err
}
out = append(out, t)
}
return out, rows.Err()
}
func (s *PostgresStore) GetTenant(ctx context.Context, tenantID int64) (*TenantInfo, error) {
var t TenantInfo
err := s.DB.QueryRowContext(ctx, `
SELECT t.tenant_id, t.name, COALESCE(t.slug,''), t.created_at,
(SELECT COUNT(*) FROM platform_meta.users u WHERE u.tenant_id=t.tenant_id AND COALESCE(u.status,'active')='active'),
(SELECT COUNT(*) FROM platform_meta.tenant_apps a WHERE a.tenant_id=t.tenant_id)
FROM platform_meta.tenants t WHERE t.tenant_id=$1`, tenantID,
).Scan(&t.TenantID, &t.Name, &t.Slug, &t.CreatedAt, &t.UserCount, &t.AppCount)
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("tenant not found")
}
if err != nil {
return nil, err
}
return &t, nil
}
func (s *PostgresStore) GetTenantBySlug(ctx context.Context, slug string) (*TenantInfo, error) {
ns, err := NormalizeTenantSlug(slug)
if err != nil {
return nil, err
}
var t TenantInfo
err = s.DB.QueryRowContext(ctx, `
SELECT t.tenant_id, t.name, COALESCE(t.slug,''), t.created_at,
(SELECT COUNT(*) FROM platform_meta.users u WHERE u.tenant_id=t.tenant_id AND COALESCE(u.status,'active')='active'),
(SELECT COUNT(*) FROM platform_meta.tenant_apps a WHERE a.tenant_id=t.tenant_id)
FROM platform_meta.tenants t WHERE t.slug=$1`, ns,
).Scan(&t.TenantID, &t.Name, &t.Slug, &t.CreatedAt, &t.UserCount, &t.AppCount)
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("tenant not found")
}
if err != nil {
return nil, err
}
return &t, nil
}
func (s *PostgresStore) CreateTenant(ctx context.Context, name, slug string) (*TenantInfo, error) {
name = strings.TrimSpace(name)
if name == "" {
return nil, fmt.Errorf("name required")
}
if strings.TrimSpace(slug) == "" {
slug = SuggestTenantSlug(name)
}
ns, err := NormalizeTenantSlug(slug)
if err != nil {
return nil, err
}
var t TenantInfo
err = s.DB.QueryRowContext(ctx,
`INSERT INTO platform_meta.tenants(name, slug) VALUES($1,$2) RETURNING tenant_id, name, slug, created_at`, name, ns,
).Scan(&t.TenantID, &t.Name, &t.Slug, &t.CreatedAt)
if err != nil {
if isUnique(err) {
return nil, fmt.Errorf("slug already exists: %s", ns)
}
return nil, err
}
return &t, nil
}
func (s *PostgresStore) UpdateTenant(ctx context.Context, tenantID int64, name, slug string) (*TenantInfo, error) {
name = strings.TrimSpace(name)
if name == "" {
return nil, fmt.Errorf("name required")
}
if strings.TrimSpace(slug) == "" {
res, err := s.DB.ExecContext(ctx, `UPDATE platform_meta.tenants SET name=$1 WHERE tenant_id=$2`, name, tenantID)
if err != nil {
return nil, err
}
n, _ := res.RowsAffected()
if n == 0 {
return nil, fmt.Errorf("tenant not found")
}
return s.GetTenant(ctx, tenantID)
}
ns, err := NormalizeTenantSlug(slug)
if err != nil {
return nil, err
}
res, err := s.DB.ExecContext(ctx, `UPDATE platform_meta.tenants SET name=$1, slug=$2 WHERE tenant_id=$3`, name, ns, tenantID)
if err != nil {
if isUnique(err) {
return nil, fmt.Errorf("slug already exists: %s", ns)
}
return nil, err
}
n, _ := res.RowsAffected()
if n == 0 {
return nil, fmt.Errorf("tenant not found")
}
return s.GetTenant(ctx, tenantID)
}
func (s *PostgresStore) EnsureTenantSlugs(ctx context.Context) error {
rows, err := s.DB.QueryContext(ctx, `SELECT tenant_id, name FROM platform_meta.tenants WHERE COALESCE(slug,'')='' ORDER BY tenant_id`)
if err != nil {
return err
}
defer rows.Close()
type row struct {
id int64
name string
}
var need []row
for rows.Next() {
var r row
if err := rows.Scan(&r.id, &r.name); err != nil {
return err
}
need = append(need, r)
}
if err := rows.Err(); err != nil {
return err
}
for _, r := range need {
base := SuggestTenantSlug(r.name)
if base == "" {
base = fmt.Sprintf("t%d", r.id)
}
cand := base
for i := 2; i < 100; i++ {
ns, nerr := NormalizeTenantSlug(cand)
if nerr != nil {
cand = fmt.Sprintf("t%d", r.id)
ns = cand
}
_, err := s.DB.ExecContext(ctx, `UPDATE platform_meta.tenants SET slug=$1 WHERE tenant_id=$2 AND COALESCE(slug,'')=''`, ns, r.id)
if err == nil {
break
}
if isUnique(err) {
cand = fmt.Sprintf("%s-%d", base, i)
continue
}
return err
}
}
// 演示公司固定 slug=demo若仍空或为自动生成
_, _ = s.DB.ExecContext(ctx, `
UPDATE platform_meta.tenants SET slug='demo'
WHERE name IN ('演示公司','演示账号') AND (COALESCE(slug,'')='' OR slug LIKE 't%' OR slug='yan-shi')
AND NOT EXISTS (SELECT 1 FROM platform_meta.tenants x WHERE x.slug='demo' AND x.name NOT IN ('演示公司','演示账号'))`)
return nil
}
func (s *PostgresStore) RenameTenantIf(ctx context.Context, oldName, newName string) error {
oldName = strings.TrimSpace(oldName)
newName = strings.TrimSpace(newName)
if oldName == "" || newName == "" || oldName == newName {
return nil
}
_, err := s.DB.ExecContext(ctx, `UPDATE platform_meta.tenants SET name=$1 WHERE name=$2`, newName, oldName)
return err
}
func (s *MemoryStore) RenameTenantIf(_ context.Context, oldName, newName string) error {
s.mu.Lock()
defer s.mu.Unlock()
oldName = strings.TrimSpace(oldName)
newName = strings.TrimSpace(newName)
if oldName == "" || newName == "" {
return nil
}
for _, t := range s.tenants {
if t.Name == oldName {
t.Name = newName
}
}
return nil
}
func (s *PostgresStore) ListMembers(ctx context.Context, tenantID int64) ([]User, error) {
rows, err := s.DB.QueryContext(ctx, `
SELECT user_id, COALESCE(tenant_id,0), COALESCE(org_unit_id,0), username, COALESCE(phone,''), COALESCE(username_login_disabled,false), display_name, role, COALESCE(status,'active'), created_at
FROM platform_meta.users
WHERE tenant_id=$1
ORDER BY user_id`, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []User
for rows.Next() {
var u User
if err := rows.Scan(&u.UserID, &u.TenantID, &u.OrgUnitID, &u.Username, &u.Phone, &u.UsernameLoginDisabled, &u.DisplayName, &u.Role, &u.Status, &u.CreatedAt); err != nil {
return nil, err
}
u.Role = authx.NormalizeRole(u.Role)
if authx.IsPlatformAdmin(u.Role) {
continue
}
out = append(out, u)
}
return out, rows.Err()
}
func (s *PostgresStore) UsernameExists(ctx context.Context, username string) (bool, error) {
var n int
err := s.DB.QueryRowContext(ctx, `SELECT 1 FROM platform_meta.users WHERE username=$1 LIMIT 1`, strings.TrimSpace(username)).Scan(&n)
if errors.Is(err, sql.ErrNoRows) {
return false, nil
}
if err != nil {
return false, err
}
return true, nil
}
func (s *PostgresStore) CreateMember(ctx context.Context, tenantID int64, username, password, displayName, role string, orgUnitID int64) (*User, string, error) {
if tenantID <= 0 {
return nil, "", fmt.Errorf("tenant required")
}
if _, err := s.GetTenant(ctx, tenantID); err != nil {
return nil, "", err
}
username = strings.TrimSpace(username)
if len(username) < 3 {
return nil, "", fmt.Errorf("username>=3 required")
}
plain := strings.TrimSpace(password)
var err error
if plain == "" {
plain, err = RandomPassword(12)
if err != nil {
return nil, "", err
}
}
if len(plain) < 6 {
return nil, "", fmt.Errorf("password>=6 required")
}
if !authx.ValidPlatformRole(role) {
return nil, "", fmt.Errorf("invalid role")
}
role = authx.NormalizeRole(role)
hash, err := hashPassword(plain)
if err != nil {
return nil, "", err
}
name := strings.TrimSpace(displayName)
if name == "" {
name = username
}
u := &User{TenantID: tenantID, OrgUnitID: orgUnitID, Username: username, DisplayName: name, Role: role, Status: StatusActive}
err = s.DB.QueryRowContext(ctx, `
INSERT INTO platform_meta.users(tenant_id, org_unit_id, username, password_hash, display_name, role, status)
VALUES($1,NULLIF($2,0),$3,$4,$5,$6,'active')
RETURNING user_id, created_at`, tenantID, orgUnitID, username, hash, name, role,
).Scan(&u.UserID, &u.CreatedAt)
if err != nil {
if isUnique(err) {
return nil, "", fmt.Errorf("username already exists")
}
return nil, "", err
}
return u, plain, nil
}
func (s *PostgresStore) ChangePassword(ctx context.Context, userID int64, oldPassword, newPassword string) error {
var hash string
err := s.DB.QueryRowContext(ctx, `SELECT password_hash FROM platform_meta.users WHERE user_id=$1`, userID).Scan(&hash)
if errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("user not found")
}
if err != nil {
return err
}
if bcrypt.CompareHashAndPassword([]byte(hash), []byte(oldPassword)) != nil {
return fmt.Errorf("旧密码不正确")
}
newPassword = strings.TrimSpace(newPassword)
if len(newPassword) < 6 {
return fmt.Errorf("password>=6 required")
}
nh, err := hashPassword(newPassword)
if err != nil {
return err
}
_, err = s.DB.ExecContext(ctx, `UPDATE platform_meta.users SET password_hash=$1 WHERE user_id=$2`, nh, userID)
return err
}
func (s *PostgresStore) SetPassword(ctx context.Context, userID int64, password string) (string, error) {
plain := strings.TrimSpace(password)
var err error
if plain == "" {
plain, err = RandomPassword(12)
if err != nil {
return "", err
}
}
if len(plain) < 6 {
return "", fmt.Errorf("password>=6 required")
}
nh, err := hashPassword(plain)
if err != nil {
return "", err
}
res, err := s.DB.ExecContext(ctx, `UPDATE platform_meta.users SET password_hash=$1 WHERE user_id=$2`, nh, userID)
if err != nil {
return "", err
}
n, _ := res.RowsAffected()
if n == 0 {
return "", fmt.Errorf("user not found")
}
return plain, nil
}
func (s *PostgresStore) UpdateMember(ctx context.Context, tenantID, userID int64, role string, orgUnitID int64, status string) (*User, error) {
cur, err := s.GetByID(ctx, userID)
if err != nil {
return nil, err
}
if cur.TenantID != tenantID {
return nil, fmt.Errorf("member not found")
}
if authx.IsPlatformAdmin(cur.Role) {
return nil, fmt.Errorf("cannot edit platform admin")
}
if role != "" {
if !authx.ValidPlatformRole(role) {
return nil, fmt.Errorf("invalid role")
}
role = authx.NormalizeRole(role)
} else {
role = cur.Role
}
if status == "" {
status = cur.Status
}
if status != StatusActive && status != StatusPending && status != "disabled" {
return nil, fmt.Errorf("invalid status")
}
_, err = s.DB.ExecContext(ctx, `
UPDATE platform_meta.users SET role=$1, org_unit_id=NULLIF($2,0), status=$3
WHERE user_id=$4 AND tenant_id=$5`, role, orgUnitID, status, userID, tenantID)
if err != nil {
return nil, err
}
return s.GetByID(ctx, userID)
}
func (s *PostgresStore) Login(ctx context.Context, account, password string) (*User, error) {
account = strings.TrimSpace(account)
u, hash, err := s.loadAuthByUsername(ctx, account)
if errors.Is(err, sql.ErrNoRows) {
if phone, nerr := NormalizePhone(account); nerr == nil {
u, hash, err = s.loadAuthByPhone(ctx, phone)
}
}
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("invalid username or password")
}
if err != nil {
return nil, err
}
if bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) != nil {
return nil, fmt.Errorf("invalid username or password")
}
u.Role = authx.NormalizeRole(u.Role)
if u.Status == "disabled" {
return nil, fmt.Errorf("账号已停用")
}
return u, nil
}
func (s *PostgresStore) loadAuthByUsername(ctx context.Context, username string) (*User, string, error) {
var u User
var hash string
var tenant sql.NullInt64
err := s.DB.QueryRowContext(ctx, `
SELECT user_id, tenant_id, COALESCE(org_unit_id,0), username, COALESCE(phone,''), COALESCE(username_login_disabled,false), password_hash, display_name, role, COALESCE(status,'active'), created_at
FROM platform_meta.users WHERE username=$1`, username,
).Scan(&u.UserID, &tenant, &u.OrgUnitID, &u.Username, &u.Phone, &u.UsernameLoginDisabled, &hash, &u.DisplayName, &u.Role, &u.Status, &u.CreatedAt)
if err != nil {
return nil, "", err
}
if tenant.Valid {
u.TenantID = tenant.Int64
}
return &u, hash, nil
}
func (s *PostgresStore) loadAuthByPhone(ctx context.Context, phone string) (*User, string, error) {
var u User
var hash string
var tenant sql.NullInt64
err := s.DB.QueryRowContext(ctx, `
SELECT user_id, tenant_id, COALESCE(org_unit_id,0), username, COALESCE(phone,''), COALESCE(username_login_disabled,false), password_hash, display_name, role, COALESCE(status,'active'), created_at
FROM platform_meta.users WHERE phone=$1`, phone,
).Scan(&u.UserID, &tenant, &u.OrgUnitID, &u.Username, &u.Phone, &u.UsernameLoginDisabled, &hash, &u.DisplayName, &u.Role, &u.Status, &u.CreatedAt)
if err != nil {
return nil, "", err
}
if tenant.Valid {
u.TenantID = tenant.Int64
}
return &u, hash, nil
}
func (s *PostgresStore) GetByID(ctx context.Context, userID int64) (*User, error) {
var u User
var tenant sql.NullInt64
err := s.DB.QueryRowContext(ctx, `
SELECT user_id, tenant_id, COALESCE(org_unit_id,0), username, COALESCE(phone,''), COALESCE(username_login_disabled,false), display_name, role, COALESCE(status,'active'), created_at
FROM platform_meta.users WHERE user_id=$1`, userID,
).Scan(&u.UserID, &tenant, &u.OrgUnitID, &u.Username, &u.Phone, &u.UsernameLoginDisabled, &u.DisplayName, &u.Role, &u.Status, &u.CreatedAt)
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("user not found")
}
if err != nil {
return nil, err
}
if tenant.Valid {
u.TenantID = tenant.Int64
}
u.Role = authx.NormalizeRole(u.Role)
return &u, nil
}
func (s *PostgresStore) GetByPhone(ctx context.Context, phone string) (*User, error) {
ns, err := NormalizePhone(phone)
if err != nil {
return nil, err
}
var u User
var tenant sql.NullInt64
err = s.DB.QueryRowContext(ctx, `
SELECT user_id, tenant_id, COALESCE(org_unit_id,0), username, COALESCE(phone,''), COALESCE(username_login_disabled,false), display_name, role, COALESCE(status,'active'), created_at
FROM platform_meta.users WHERE phone=$1`, ns,
).Scan(&u.UserID, &tenant, &u.OrgUnitID, &u.Username, &u.Phone, &u.UsernameLoginDisabled, &u.DisplayName, &u.Role, &u.Status, &u.CreatedAt)
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("user not found")
}
if err != nil {
return nil, err
}
if tenant.Valid {
u.TenantID = tenant.Int64
}
u.Role = authx.NormalizeRole(u.Role)
return &u, nil
}
func (s *PostgresStore) PhoneExists(ctx context.Context, phone string, excludeUserID int64) (bool, error) {
phone = strings.TrimSpace(phone)
if phone == "" {
return false, nil
}
var n int
err := s.DB.QueryRowContext(ctx, `
SELECT 1 FROM platform_meta.users WHERE phone=$1 AND user_id<>$2 LIMIT 1`, phone, excludeUserID).Scan(&n)
if errors.Is(err, sql.ErrNoRows) {
return false, nil
}
if err != nil {
return false, err
}
return true, nil
}
func (s *PostgresStore) BindPhone(ctx context.Context, userID int64, phone string) (*User, error) {
phone = strings.TrimSpace(phone)
if phone == "" {
_, err := s.DB.ExecContext(ctx, `UPDATE platform_meta.users SET phone='', username_login_disabled=false WHERE user_id=$1`, userID)
if err != nil {
return nil, err
}
return s.GetByID(ctx, userID)
}
ns, err := NormalizePhone(phone)
if err != nil {
return nil, err
}
exists, err := s.PhoneExists(ctx, ns, userID)
if err != nil {
return nil, err
}
if exists {
return nil, fmt.Errorf("该手机号已被绑定")
}
res, err := s.DB.ExecContext(ctx, `UPDATE platform_meta.users SET phone=$1 WHERE user_id=$2`, ns, userID)
if err != nil {
if isUnique(err) {
return nil, fmt.Errorf("该手机号已被绑定")
}
return nil, err
}
n, _ := res.RowsAffected()
if n == 0 {
return nil, fmt.Errorf("user not found")
}
return s.GetByID(ctx, userID)
}
func (s *PostgresStore) SetUsernameLoginDisabled(ctx context.Context, userID int64, disabled bool) (*User, error) {
u, err := s.GetByID(ctx, userID)
if err != nil {
return nil, err
}
if disabled && strings.TrimSpace(u.Phone) == "" {
return nil, fmt.Errorf("请先绑定手机号,再禁用用户名登录")
}
_, err = s.DB.ExecContext(ctx, `UPDATE platform_meta.users SET username_login_disabled=$1 WHERE user_id=$2`, disabled, userID)
if err != nil {
return nil, err
}
return s.GetByID(ctx, userID)
}
func (s *PostgresStore) JoinTenant(ctx context.Context, userID, tenantID int64, role string, orgUnitID int64) (*User, error) {
if tenantID <= 0 {
return nil, fmt.Errorf("invalid tenant")
}
role = normalizePlatformRole(role)
res, err := s.DB.ExecContext(ctx, `
UPDATE platform_meta.users
SET tenant_id=$1, role=$2, status='active', org_unit_id=NULLIF($3,0)
WHERE user_id=$4 AND (tenant_id IS NULL OR status='pending' OR role='pending' OR role='待加入')`,
tenantID, role, orgUnitID, userID)
if err != nil {
return nil, err
}
n, _ := res.RowsAffected()
if n == 0 {
u, gerr := s.GetByID(ctx, userID)
if gerr != nil {
return nil, fmt.Errorf("user not found")
}
if u.HasTenant() {
return nil, fmt.Errorf("user already joined a tenant")
}
return nil, fmt.Errorf("join tenant failed")
}
return s.GetByID(ctx, userID)
}
func (s *PostgresStore) SetOrgUnit(ctx context.Context, userID, tenantID, orgUnitID int64) (*User, error) {
res, err := s.DB.ExecContext(ctx, `
UPDATE platform_meta.users SET org_unit_id=NULLIF($1,0)
WHERE user_id=$2 AND tenant_id=$3`, orgUnitID, userID, tenantID)
if err != nil {
return nil, err
}
n, _ := res.RowsAffected()
if n == 0 {
return nil, fmt.Errorf("user not found")
}
return s.GetByID(ctx, userID)
}
func (s *PostgresStore) CreateTenantAsOwner(ctx context.Context, userID int64, tenantName string) (*User, error) {
u, err := s.GetByID(ctx, userID)
if err != nil {
return nil, err
}
if u.HasTenant() {
return nil, fmt.Errorf("user already joined a tenant")
}
name := strings.TrimSpace(tenantName)
if name == "" {
name = u.DisplayName
}
if name == "" {
name = u.Username
}
tx, err := s.DB.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer func() { _ = tx.Rollback() }()
var tenantID int64
slug := SuggestTenantSlug(name)
if slug == "" {
slug = fmt.Sprintf("co%d", time.Now().Unix()%100000)
}
ns, nerr := NormalizeTenantSlug(slug)
if nerr != nil {
ns = fmt.Sprintf("t%d", time.Now().Unix()%1000000)
}
for i := 0; i < 20; i++ {
try := ns
if i > 0 {
try = fmt.Sprintf("%s-%d", ns, i+1)
}
err = tx.QueryRowContext(ctx,
`INSERT INTO platform_meta.tenants(name, slug) VALUES($1,$2) RETURNING tenant_id`, name, try,
).Scan(&tenantID)
if err == nil {
break
}
if !isUnique(err) {
return nil, err
}
}
if err != nil {
return nil, fmt.Errorf("create tenant slug conflict")
}
res, err := tx.ExecContext(ctx, `
UPDATE platform_meta.users
SET tenant_id=$1, role='owner', status='active'
WHERE user_id=$2 AND (tenant_id IS NULL OR status='pending' OR role='pending')`,
tenantID, userID)
if err != nil {
return nil, err
}
n, _ := res.RowsAffected()
if n == 0 {
return nil, fmt.Errorf("user already joined a tenant")
}
if err := tx.Commit(); err != nil {
return nil, err
}
return s.GetByID(ctx, userID)
}
// EnsureDemoUser 开发态确保 demo/demo123 为「演示公司」的公司管理员(不是超管自己)。
func EnsureDemoUser(ctx context.Context, store Store) {
if store == nil {
return
}
if _, err := store.Login(ctx, "demo", "demo123"); err != nil {
_, _ = store.EnsureBootstrapOwner(ctx, "demo", "demo123", "演示用户", "演示公司")
}
_ = store.RenameTenantIf(ctx, "演示账号", "演示公司")
_ = store.EnsureTenantSlugs(ctx)
ensureDevPhone(ctx, store, "demo", "13800000001")
}
// EnsurePlatformAdminUser 确保平台超级管理员 ljk_admin / ljk_admin。
func EnsurePlatformAdminUser(ctx context.Context, store Store) {
if store == nil {
return
}
if _, err := store.EnsurePlatformAdmin(ctx, "ljk_admin", "ljk_admin", "平台超级管理员"); err != nil {
fmt.Printf("ensure platform admin ljk_admin: %v\n", err)
}
ensureDevPhone(ctx, store, "ljk_admin", "13800000000")
}
func ensureDevPhone(ctx context.Context, store Store, username, phone string) {
var user *User
var err error
switch username {
case "demo":
user, err = store.Login(ctx, "demo", "demo123")
case "ljk_admin":
user, err = store.Login(ctx, "ljk_admin", "ljk_admin")
default:
return
}
if err != nil || user == nil {
return
}
if strings.TrimSpace(user.Phone) != "" {
return
}
if _, err := store.BindPhone(ctx, user.UserID, phone); err != nil {
fmt.Printf("ensure phone for %s: %v\n", username, err)
return
}
_, _ = store.SetUsernameLoginDisabled(ctx, user.UserID, true)
}
func hashPassword(pw string) (string, error) {
b, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
return string(b), err
}
func isUnique(err error) bool {
return err != nil && (strings.Contains(err.Error(), "duplicate") || strings.Contains(err.Error(), "unique"))
}
func normalizePlatformRole(role string) string {
n := authx.NormalizeRole(role)
if authx.ValidPlatformRole(n) {
return n
}
return authx.Role编辑
}