feat(server/auth): switch user identity to phone with email optional
Phone becomes the required, unique login identifier; email and name turn optional. Login now accepts either via a single identifier field, refresh re-checks tenant membership, and the dev seed provides phone numbers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ package app
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@@ -37,8 +38,9 @@ func NewAuthService(
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
Identifier string `json:"identifier"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
type LoginResponse struct {
|
||||
@@ -50,8 +52,9 @@ type LoginResponse struct {
|
||||
|
||||
type UserInfo struct {
|
||||
ID int64 `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Email *string `json:"email"`
|
||||
Phone string `json:"phone"`
|
||||
Name *string `json:"name"`
|
||||
AvatarURL *string `json:"avatar_url"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
PrimaryWorkspaceID int64 `json:"primary_workspace_id"`
|
||||
@@ -86,11 +89,23 @@ type KolProfileBrief struct {
|
||||
}
|
||||
|
||||
func (s *AuthService) Login(ctx context.Context, req LoginRequest) (*LoginResponse, error) {
|
||||
user, err := s.repo.GetUserByEmail(ctx, req.Email)
|
||||
identifier := strings.TrimSpace(req.Identifier)
|
||||
if identifier == "" {
|
||||
identifier = strings.TrimSpace(req.Email)
|
||||
}
|
||||
if identifier == "" {
|
||||
return nil, response.ErrBadRequest(40001, "invalid_params", "login identifier is required")
|
||||
}
|
||||
|
||||
user, err := s.repo.GetUserByLoginIdentifier(ctx, identifier)
|
||||
if err != nil {
|
||||
return nil, response.ErrUnauthorized(40110, "invalid_credentials", "email or password is incorrect")
|
||||
}
|
||||
|
||||
if user.Status != "active" {
|
||||
return nil, response.ErrForbidden(40311, "user_disabled", "user account is disabled")
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
|
||||
return nil, response.ErrUnauthorized(40110, "invalid_credentials", "email or password is incorrect")
|
||||
}
|
||||
@@ -126,6 +141,7 @@ func (s *AuthService) Login(ctx context.Context, req LoginRequest) (*LoginRespon
|
||||
User: UserInfo{
|
||||
ID: user.ID,
|
||||
Email: user.Email,
|
||||
Phone: user.Phone,
|
||||
Name: user.Name,
|
||||
AvatarURL: user.AvatarURL,
|
||||
TenantID: membership.TenantID,
|
||||
@@ -154,7 +170,12 @@ func (s *AuthService) Refresh(ctx context.Context, req RefreshRequest) (*Refresh
|
||||
return nil, response.ErrUnauthorized(40120, "invalid_refresh_token", "refresh token is invalid or expired")
|
||||
}
|
||||
|
||||
pair, err := s.jwt.Issue(claims.UserID, claims.TenantID, claims.PrimaryWorkspaceID, claims.Role)
|
||||
membership, err := s.repo.GetTenantMembershipByTenantAndUser(ctx, claims.TenantID, claims.UserID)
|
||||
if err != nil {
|
||||
return nil, response.ErrUnauthorized(40123, "membership_revoked", "tenant membership is no longer active")
|
||||
}
|
||||
|
||||
pair, err := s.jwt.Issue(membership.UserID, membership.TenantID, membership.PrimaryWorkspaceID, membership.TenantRole)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("issue token: %w", err)
|
||||
}
|
||||
@@ -202,6 +223,7 @@ func (s *AuthService) Me(ctx context.Context, actor auth.Actor) (*UserInfo, erro
|
||||
return &UserInfo{
|
||||
ID: user.ID,
|
||||
Email: user.Email,
|
||||
Phone: user.Phone,
|
||||
Name: user.Name,
|
||||
AvatarURL: user.AvatarURL,
|
||||
TenantID: actor.TenantID,
|
||||
|
||||
Reference in New Issue
Block a user