thread-master/backend/internal/model/member/usecase/usecase.go

208 lines
7.7 KiB
Go
Raw Normal View History

2026-06-26 08:37:04 +00:00
package usecase
import (
"context"
"strings"
app "haixun-backend/internal/library/errors"
"haixun-backend/internal/library/errors/code"
authusecase "haixun-backend/internal/model/auth/domain/usecase"
"haixun-backend/internal/model/member/domain/entity"
domrepo "haixun-backend/internal/model/member/domain/repository"
domusecase "haixun-backend/internal/model/member/domain/usecase"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
)
type memberUseCase struct {
repo domrepo.Repository
tokens authusecase.TokenUseCase
}
func NewUseCase(repo domrepo.Repository, tokens authusecase.TokenUseCase) domusecase.UseCase {
return &memberUseCase{repo: repo, tokens: tokens}
}
func (u *memberUseCase) Register(ctx context.Context, req domusecase.RegisterRequest) (*entity.Member, *domusecase.AuthToken, error) {
tenantID := strings.TrimSpace(req.TenantID)
email := normalizeEmail(req.Email)
if tenantID == "" || email == "" || req.Password == "" {
return nil, nil, app.For(code.Member).InputMissingRequired("tenant_id, email, and password are required")
}
if len(req.Password) < 8 {
return nil, nil, app.For(code.Member).InputInvalidFormat("password must be at least 8 characters")
}
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return nil, nil, app.For(code.Member).SysInternal("hash password failed").WithCause(err)
}
member, err := u.repo.Create(ctx, &entity.Member{
TenantID: tenantID,
UID: uuid.NewString(),
Email: email,
DisplayName: strings.TrimSpace(req.DisplayName),
Language: strings.TrimSpace(req.Language),
Status: entity.StatusOpen,
Origin: entity.OriginNative,
PasswordHash: string(hash),
Roles: []string{"user"},
})
if err != nil {
return nil, nil, err
}
token, err := u.issue(ctx, member)
return member, token, err
}
func (u *memberUseCase) Login(ctx context.Context, req domusecase.LoginRequest) (*entity.Member, *domusecase.AuthToken, error) {
tenantID := strings.TrimSpace(req.TenantID)
email := normalizeEmail(req.Email)
if tenantID == "" || email == "" || req.Password == "" {
return nil, nil, app.For(code.Auth).InputMissingRequired("tenant_id, email, and password are required")
}
member, err := u.repo.FindByEmail(ctx, tenantID, email)
if err != nil {
return nil, nil, app.For(code.Auth).AuthUnauthorized("invalid email or password").WithCause(err)
}
if member.Status != entity.StatusOpen {
return nil, nil, app.For(code.Auth).AuthForbidden("member is not active")
}
if err := bcrypt.CompareHashAndPassword([]byte(member.PasswordHash), []byte(req.Password)); err != nil {
return nil, nil, app.For(code.Auth).AuthUnauthorized("invalid email or password")
}
token, err := u.issue(ctx, member)
return member, token, err
}
2026-07-07 14:02:41 +00:00
func (u *memberUseCase) ListMembers(ctx context.Context, req domusecase.ListMembersRequest) (*domusecase.ListMembersResult, error) {
if strings.TrimSpace(req.TenantID) == "" {
return nil, app.For(code.Member).InputMissingRequired("tenant_id is required")
}
items, total, page, pageSize, err := u.repo.List(ctx, strings.TrimSpace(req.TenantID), req.Page, req.PageSize)
if err != nil {
return nil, err
}
totalPages := int64(0)
if pageSize > 0 {
totalPages = (total + pageSize - 1) / pageSize
}
return &domusecase.ListMembersResult{List: items, Total: total, Page: page, PageSize: pageSize, TotalPages: totalPages}, nil
}
2026-06-26 08:37:04 +00:00
func (u *memberUseCase) GetByUID(ctx context.Context, tenantID, uid string) (*entity.Member, error) {
if tenantID == "" || uid == "" {
return nil, app.For(code.Member).InputMissingRequired("tenant_id and uid are required")
}
return u.repo.FindByUID(ctx, tenantID, uid)
}
func (u *memberUseCase) SetActiveThreadsAccountID(ctx context.Context, tenantID, uid, accountID string) error {
if tenantID == "" || uid == "" {
return app.For(code.Member).InputMissingRequired("tenant_id and uid are required")
}
return u.repo.SetActiveThreadsAccountID(ctx, tenantID, uid, accountID)
}
2026-07-07 14:02:41 +00:00
func (u *memberUseCase) SetRoles(ctx context.Context, tenantID, uid string, roles []string) error {
if tenantID == "" || uid == "" {
return app.For(code.Member).InputMissingRequired("tenant_id and uid are required")
}
normalized := normalizeRoles(roles)
if len(normalized) == 0 {
return app.For(code.Member).InputInvalidFormat("roles must include user or admin")
}
for _, role := range normalized {
if role != "user" && role != "admin" {
return app.For(code.Member).InputInvalidFormat("roles only support user or admin")
}
}
return u.repo.SetRoles(ctx, tenantID, uid, normalized)
}
func (u *memberUseCase) SetEmailVerificationCode(ctx context.Context, tenantID, uid, verifyCode string, expiresAt int64) error {
if tenantID == "" || uid == "" || strings.TrimSpace(verifyCode) == "" || expiresAt <= 0 {
return app.For(code.Member).InputMissingRequired("tenant_id, uid, code, and expires_at are required")
}
return u.repo.SetEmailVerificationCode(ctx, tenantID, uid, strings.TrimSpace(verifyCode), expiresAt)
}
func (u *memberUseCase) UpdatePassword(ctx context.Context, tenantID, uid, password string) error {
if tenantID == "" || uid == "" || password == "" {
return app.For(code.Member).InputMissingRequired("tenant_id, uid, and password are required")
}
if len(password) < 8 {
return app.For(code.Member).InputInvalidFormat("password must be at least 8 characters")
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return app.For(code.Member).SysInternal("hash password failed").WithCause(err)
}
return u.repo.UpdatePassword(ctx, tenantID, uid, string(hash))
}
2026-06-26 08:37:04 +00:00
func (u *memberUseCase) UpdateProfile(ctx context.Context, req domusecase.UpdateProfileRequest) (*entity.Member, error) {
if req.TenantID == "" || req.UID == "" {
return nil, app.For(code.Member).InputMissingRequired("tenant_id and uid are required")
}
2026-07-07 14:02:41 +00:00
var status *entity.Status
if req.Status != nil {
next := entity.Status(strings.TrimSpace(*req.Status))
if next != entity.StatusOpen && next != entity.StatusSuspended && next != entity.StatusDeleted {
return nil, app.For(code.Member).InputInvalidFormat("status only supports open, suspended, or deleted")
}
status = &next
}
2026-06-26 08:37:04 +00:00
return u.repo.UpdateProfile(ctx, req.TenantID, req.UID, domrepo.ProfileUpdate{
2026-07-07 14:02:41 +00:00
DisplayName: req.DisplayName,
Avatar: req.Avatar,
Language: req.Language,
Currency: req.Currency,
Phone: req.Phone,
EmailVerified: req.EmailVerified,
Status: status,
BusinessEmail: req.BusinessEmail,
BusinessEmailVerified: req.BusinessEmailVerified,
BusinessPhone: req.BusinessPhone,
BusinessPhoneVerified: req.BusinessPhoneVerified,
2026-06-26 08:37:04 +00:00
})
}
func (u *memberUseCase) issue(ctx context.Context, member *entity.Member) (*domusecase.AuthToken, error) {
if u.tokens == nil {
return nil, app.For(code.Auth).SysNotImplemented("token usecase is not configured")
}
pair, err := u.tokens.IssuePair(ctx, authusecase.IssuePairRequest{
TenantID: member.TenantID,
UID: member.UID,
})
if err != nil {
return nil, err
}
return &domusecase.AuthToken{
AccessToken: pair.AccessToken,
RefreshToken: pair.RefreshToken,
ExpiresIn: pair.ExpiresIn,
UID: member.UID,
TokenType: pair.TokenType,
}, nil
}
func normalizeEmail(email string) string {
return strings.ToLower(strings.TrimSpace(email))
}
2026-07-07 14:02:41 +00:00
func normalizeRoles(roles []string) []string {
seen := make(map[string]bool, len(roles))
out := make([]string, 0, len(roles))
for _, role := range roles {
role = strings.ToLower(strings.TrimSpace(role))
if role == "" || seen[role] {
continue
}
seen[role] = true
out = append(out, role)
}
return out
}