feat: complete remaining Go backend modules

This commit is contained in:
zn-admin committed 2026-08-14 00:02:43 +08:00
1 parent cea2751dc5
commit aef5a97165
145 files changed
+18376 -199

No files matched your search

@@ -0,0 +1,102 @@
package identity
import (
"context"
"errors"
"fmt"
"strings"
"time"
)
type PasswordChanger interface {
ChangeOwnPassword(context.Context, string, string, string, time.Time) (AuthorizationSnapshot, error)
}
type PasswordChangeCommand struct {
AccountID, CurrentPassword, NewPassword string
}
type PasswordChangeFailure string
const (
PasswordChangeInvalidInput PasswordChangeFailure = "invalid_input"
PasswordChangeInvalidNewPassword PasswordChangeFailure = "invalid_new_password"
PasswordChangeNotFound PasswordChangeFailure = "not_found"
PasswordChangeCurrentIncorrect PasswordChangeFailure = "current_password_incorrect"
)
var ErrPasswordChange = errors.New("password change failed")
type PasswordChangeError struct{ Reason PasswordChangeFailure }
func (e *PasswordChangeError) Error() string {
return fmt.Sprintf("%s: %s", ErrPasswordChange, e.Reason)
}
func (e *PasswordChangeError) Unwrap() error { return ErrPasswordChange }
func IsPasswordChangeFailure(err error, reason PasswordChangeFailure) bool {
var target *PasswordChangeError
return errors.As(err, &target) && target.Reason == reason
}
type PasswordChange struct {
store PasswordChanger
now func() time.Time
}
func NewPasswordChange(store PasswordChanger, now func() time.Time) *PasswordChange {
if now == nil {
now = time.Now
}
return &PasswordChange{store: store, now: now}
}
func (change *PasswordChange) Change(ctx context.Context, command PasswordChangeCommand) (Session, error) {
command.AccountID = strings.TrimSpace(command.AccountID)
command.CurrentPassword = strings.TrimSpace(command.CurrentPassword)
command.NewPassword = strings.TrimSpace(command.NewPassword)
if command.AccountID == "" || command.CurrentPassword == "" {
return Session{}, &PasswordChangeError{Reason: PasswordChangeInvalidInput}
}
if len(command.NewPassword) < 8 {
return Session{}, &PasswordChangeError{Reason: PasswordChangeInvalidNewPassword}
}
if change == nil || change.store == nil {
return Session{}, fmt.Errorf("password change is not configured")
}
now := change.now()
snapshot, err := change.store.ChangeOwnPassword(ctx, command.AccountID, command.CurrentPassword, command.NewPassword, now)
if err != nil {
return Session{}, err
}
return sessionFromSnapshot(snapshot, now)
}
func sessionFromSnapshot(snapshot AuthorizationSnapshot, now time.Time) (Session, error) {
account := snapshot.Account
if account.Status != "active" {
return Session{}, &PasswordChangeError{Reason: PasswordChangeNotFound}
}
authMode, authorities, valid := roleClaims(account.Role)
if !valid {
return Session{}, NewPasswordLoginError(LoginFailureInvalidRole)
}
organizationName := ""
if account.Role != "super_admin" {
if account.OrganizationID == "" {
return Session{}, NewPasswordLoginError(LoginFailureOrganizationRequired)
}
if snapshot.Organization == nil || snapshot.Organization.ID != account.OrganizationID || snapshot.Organization.Status != "active" {
return Session{}, NewPasswordLoginError(LoginFailureOrganizationNotActive)
}
organizationName = snapshot.Organization.Name
} else if snapshot.Organization != nil && snapshot.Organization.ID == account.OrganizationID {
organizationName = snapshot.Organization.Name
}
version := account.SessionVersion
return Session{Version: 1, AuthMode: authMode, IssuedAt: now.Unix(), ExpiresAt: now.Add(passwordSessionTTL).Unix(), SessionVersion: &version, User: User{
ID: account.ID, Subject: account.ID, Username: account.Phone, Phone: account.Phone,
DisplayName: account.DisplayName, ClientID: "platform", OrganizationID: account.OrganizationID,
OrganizationName: organizationName, Role: account.Role, Status: account.Status,
Authorities: authorities, Scope: []string{},
}}, nil
}
@@ -0,0 +1,44 @@
package identity
import (
"context"
"testing"
"time"
)
type passwordChangeStoreStub struct {
snapshot AuthorizationSnapshot
err error
id, current, next string
}
func (s *passwordChangeStoreStub) ChangeOwnPassword(_ context.Context, id, current, next string, _ time.Time) (AuthorizationSnapshot, error) {
s.id, s.current, s.next = id, current, next
return s.snapshot, s.err
}
func TestPasswordChangeReturnsRefreshedSession(t *testing.T) {
now := time.Unix(1770000000, 0)
store := &passwordChangeStoreStub{snapshot: AuthorizationSnapshot{
Account: AccountSnapshot{ID: "user-1", Phone: "13800138000", DisplayName: "User", Role: "user", OrganizationID: "org-1", Status: "active", SessionVersion: 8},
Organization: &OrganizationSnapshot{ID: "org-1", Name: "Acme", Status: "active"},
}}
session, err := NewPasswordChange(store, func() time.Time { return now }).Change(context.Background(), PasswordChangeCommand{AccountID: "user-1", CurrentPassword: " current-pass ", NewPassword: "next-pass"})
if err != nil {
t.Fatal(err)
}
if store.current != "current-pass" || store.next != "next-pass" || session.SessionVersion == nil || *session.SessionVersion != 8 || session.User.OrganizationName != "Acme" {
t.Fatalf("store=%+v session=%+v", store, session)
}
}
func TestPasswordChangeValidatesInput(t *testing.T) {
store := &passwordChangeStoreStub{}
_, err := NewPasswordChange(store, nil).Change(context.Background(), PasswordChangeCommand{AccountID: "user-1", CurrentPassword: "old", NewPassword: "short"})
if !IsPasswordChangeFailure(err, PasswordChangeInvalidNewPassword) {
t.Fatalf("err=%v", err)
}
if store.id != "" {
t.Fatal("store called for invalid input")
}
}