feat: support username admin login
This commit is contained in:
@@ -94,6 +94,7 @@ func (a *App) handleListUsers(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (a *App) handleCreateUser(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
LoginName string `json:"loginName"`
|
||||
Email string `json:"email"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Role string `json:"role"`
|
||||
@@ -107,14 +108,14 @@ func (a *App) handleCreateUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
actor := currentUser(r)
|
||||
email := normalizeEmail(req.Email)
|
||||
if email == "" || !strings.Contains(email, "@") {
|
||||
badRequest(w, errors.New("invalid email"))
|
||||
loginName, err := cleanLoginName(req.LoginName, req.Email)
|
||||
if err != nil {
|
||||
badRequest(w, err)
|
||||
return
|
||||
}
|
||||
displayName := strings.TrimSpace(req.DisplayName)
|
||||
if displayName == "" {
|
||||
displayName = email
|
||||
displayName = loginName
|
||||
}
|
||||
role := strings.TrimSpace(req.Role)
|
||||
if role == "" {
|
||||
@@ -154,7 +155,7 @@ func (a *App) handleCreateUser(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if _, err = tx.ExecContext(r.Context(), `INSERT INTO users(id,email,display_name,role,password_hash,disabled,mailbox_limit_override,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?)`, id, email, displayName, role, string(passwordHash), boolInt(req.Disabled), nullableInt(mailboxLimitOverride), now, now); err != nil {
|
||||
VALUES(?,?,?,?,?,?,?,?,?)`, id, loginName, displayName, role, string(passwordHash), boolInt(req.Disabled), nullableInt(mailboxLimitOverride), now, now); err != nil {
|
||||
badRequest(w, err)
|
||||
return
|
||||
}
|
||||
@@ -515,14 +516,15 @@ func (a *App) handleListMailboxes(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (a *App) handleCreateMailbox(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
DomainID string `json:"domainId"`
|
||||
LocalPart string `json:"localPart"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Password string `json:"password"`
|
||||
QuotaMB int `json:"quotaMb"`
|
||||
Role string `json:"role"`
|
||||
OwnerEmail string `json:"ownerEmail"`
|
||||
UserID string `json:"userId"`
|
||||
DomainID string `json:"domainId"`
|
||||
LocalPart string `json:"localPart"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Password string `json:"password"`
|
||||
QuotaMB int `json:"quotaMb"`
|
||||
Role string `json:"role"`
|
||||
OwnerLoginName string `json:"ownerLoginName"`
|
||||
OwnerEmail string `json:"ownerEmail"`
|
||||
UserID string `json:"userId"`
|
||||
}
|
||||
if err := decodeJSON(r, &req); err != nil {
|
||||
badRequest(w, err)
|
||||
@@ -591,15 +593,12 @@ func (a *App) handleCreateMailbox(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
ownerEmail := normalizeEmail(req.OwnerEmail)
|
||||
if ownerEmail == "" {
|
||||
ownerEmail = address
|
||||
}
|
||||
if !strings.Contains(ownerEmail, "@") {
|
||||
badRequest(w, errors.New("invalid owner email"))
|
||||
ownerLoginName, err := cleanLoginName(req.OwnerLoginName, req.OwnerEmail, address)
|
||||
if err != nil {
|
||||
badRequest(w, err)
|
||||
return
|
||||
}
|
||||
err = tx.QueryRowContext(r.Context(), `SELECT id FROM users WHERE email=? AND disabled=0`, ownerEmail).Scan(&userID)
|
||||
err = tx.QueryRowContext(r.Context(), `SELECT id FROM users WHERE email=? AND disabled=0`, ownerLoginName).Scan(&userID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
@@ -608,11 +607,11 @@ func (a *App) handleCreateMailbox(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
userID = newID("usr")
|
||||
ownerDisplayName := displayName
|
||||
if !strings.EqualFold(ownerEmail, address) {
|
||||
ownerDisplayName = ownerEmail
|
||||
if !strings.EqualFold(ownerLoginName, address) {
|
||||
ownerDisplayName = ownerLoginName
|
||||
}
|
||||
_, err = tx.ExecContext(r.Context(), `INSERT INTO users(id,email,display_name,role,password_hash,disabled,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?)`, userID, ownerEmail, ownerDisplayName, role, string(passwordHash), 0, now, now)
|
||||
VALUES(?,?,?,?,?,?,?,?)`, userID, ownerLoginName, ownerDisplayName, role, string(passwordHash), 0, now, now)
|
||||
if err != nil {
|
||||
badRequest(w, err)
|
||||
return
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
func (a *App) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
LoginName string `json:"loginName"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
TurnstileToken string `json:"turnstileToken"`
|
||||
@@ -49,14 +50,18 @@ func (a *App) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
respondError(w, http.StatusUnauthorized, "人机验证失败,请重试")
|
||||
return
|
||||
}
|
||||
email := normalizeEmail(req.Email)
|
||||
user, passwordHash, err := a.userByEmail(r.Context(), email)
|
||||
loginName, err := cleanLoginName(req.LoginName, req.Email)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusUnauthorized, "账号或密码错误")
|
||||
return
|
||||
}
|
||||
user, passwordHash, err := a.userByEmail(r.Context(), loginName)
|
||||
if err != nil || user.Disabled {
|
||||
respondError(w, http.StatusUnauthorized, "邮箱或密码错误")
|
||||
respondError(w, http.StatusUnauthorized, "账号或密码错误")
|
||||
return
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(req.Password)); err != nil {
|
||||
respondError(w, http.StatusUnauthorized, "邮箱或密码错误")
|
||||
respondError(w, http.StatusUnauthorized, "账号或密码错误")
|
||||
return
|
||||
}
|
||||
if a.cfg.TwoFactorEnabled && user.TwoFactorEnabled {
|
||||
|
||||
@@ -616,6 +616,9 @@ func (a *App) attachUserAuthorization(ctx context.Context, u *User) error {
|
||||
if u == nil {
|
||||
return nil
|
||||
}
|
||||
if u.LoginName == "" {
|
||||
u.LoginName = u.Email
|
||||
}
|
||||
permissions, err := a.permissionsForUser(ctx, u.ID, u.Role)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -3,19 +3,20 @@ package app
|
||||
import "time"
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Role string `json:"role"`
|
||||
Disabled bool `json:"disabled"`
|
||||
Protected bool `json:"protected"`
|
||||
TwoFactorEnabled bool `json:"twoFactorEnabled"`
|
||||
MailboxLimitOverride *int `json:"mailboxLimitOverride,omitempty"`
|
||||
Permissions []string `json:"permissions"`
|
||||
Limits PermissionLimits `json:"limits"`
|
||||
PermissionGroupIDs []string `json:"permissionGroupIds"`
|
||||
PermissionGroups []PermissionGroupSummary `json:"permissionGroups"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
ID string `json:"id"`
|
||||
LoginName string `json:"loginName"`
|
||||
Email string `json:"email"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Role string `json:"role"`
|
||||
Disabled bool `json:"disabled"`
|
||||
Protected bool `json:"protected"`
|
||||
TwoFactorEnabled bool `json:"twoFactorEnabled"`
|
||||
MailboxLimitOverride *int `json:"mailboxLimitOverride,omitempty"`
|
||||
Permissions []string `json:"permissions"`
|
||||
Limits PermissionLimits `json:"limits"`
|
||||
PermissionGroupIDs []string `json:"permissionGroupIds"`
|
||||
PermissionGroups []PermissionGroupSummary `json:"permissionGroups"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type AdminUser struct {
|
||||
|
||||
@@ -149,6 +149,44 @@ func normalizeEmail(s string) string {
|
||||
return normalizeLocalPart(parts[0]) + "@" + normalizeDomain(parts[1])
|
||||
}
|
||||
|
||||
func normalizeLoginName(s string) string {
|
||||
s = strings.ToLower(strings.TrimSpace(s))
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.Contains(s, "@") {
|
||||
return normalizeEmail(s)
|
||||
}
|
||||
return normalizeLocalPart(s)
|
||||
}
|
||||
|
||||
func cleanLoginName(value string, fallbacks ...string) (string, error) {
|
||||
loginName := normalizeLoginName(value)
|
||||
for _, fallback := range fallbacks {
|
||||
if loginName != "" {
|
||||
break
|
||||
}
|
||||
loginName = normalizeLoginName(fallback)
|
||||
}
|
||||
if loginName == "" {
|
||||
return "", errors.New("登录名不能为空")
|
||||
}
|
||||
if len([]rune(loginName)) > 80 {
|
||||
return "", errors.New("登录名不能超过 80 个字符")
|
||||
}
|
||||
if strings.Contains(loginName, "@") {
|
||||
parts := strings.SplitN(loginName, "@", 2)
|
||||
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
||||
return "", errors.New("登录名格式无效")
|
||||
}
|
||||
return loginName, nil
|
||||
}
|
||||
if len([]rune(loginName)) < 2 {
|
||||
return "", errors.New("登录名至少需要 2 个字符")
|
||||
}
|
||||
return loginName, nil
|
||||
}
|
||||
|
||||
func dedupeEmails(items []string) []string {
|
||||
seen := map[string]bool{}
|
||||
out := make([]string, 0, len(items))
|
||||
|
||||
Reference in New Issue
Block a user