package repository import ( "time" "hyapp-admin-server/internal/model" "gorm.io/gorm" ) // UserAuthorization is the current server-side authorization state used for // every protected request. JWT claims identify the session, but never own RBAC // state because role changes must take effect before a long-lived token expires. type UserAuthorization struct { Username string Status string Permissions []string } type userAuthorizationRow struct { Username string Status string PermissionCode *string } func (s *Store) FindUserByUsername(username string) (*model.User, error) { var user model.User err := s.db.Preload("Roles.Permissions").Preload("TeamRecord").Where("username = ?", username).First(&user).Error if err != nil { return nil, err } return &user, nil } func (s *Store) FindUserByID(id uint) (*model.User, error) { var user model.User err := s.db.Preload("Roles.Permissions").Preload("TeamRecord").First(&user, id).Error if err != nil { return nil, err } return &user, nil } func (s *Store) CurrentAuthorizationForUser(userID uint) (UserAuthorization, error) { if userID == 0 { return UserAuthorization{}, gorm.ErrRecordNotFound } var rows []userAuthorizationRow // The query starts from admin_users.id and follows the two composite-primary-key // relation tables. It therefore resolves live permissions in one indexed query // without loading the full user/role models on every protected HTTP request. result := s.db.Raw(` SELECT admin_users.username, admin_users.status, admin_permissions.code AS permission_code FROM admin_users LEFT JOIN admin_user_roles ON admin_user_roles.user_id = admin_users.id LEFT JOIN admin_role_permissions ON admin_role_permissions.role_id = admin_user_roles.role_id LEFT JOIN admin_permissions ON admin_permissions.id = admin_role_permissions.permission_id WHERE admin_users.id = ? ORDER BY admin_permissions.code ASC `, userID).Scan(&rows) if result.Error != nil { return UserAuthorization{}, result.Error } if len(rows) == 0 { return UserAuthorization{}, gorm.ErrRecordNotFound } authorization := UserAuthorization{ Username: rows[0].Username, Status: rows[0].Status, } seen := make(map[string]struct{}, len(rows)) for _, row := range rows { if row.PermissionCode == nil || *row.PermissionCode == "" { continue } if _, ok := seen[*row.PermissionCode]; ok { continue } seen[*row.PermissionCode] = struct{}{} authorization.Permissions = append(authorization.Permissions, *row.PermissionCode) } return authorization, nil } func PermissionsForUser(user model.User) []string { set := map[string]struct{}{} for _, role := range user.Roles { for _, permission := range role.Permissions { set[permission.Code] = struct{}{} } } out := make([]string, 0, len(set)) for code := range set { out = append(out, code) } return out } func (s *Store) TouchUserLogin(id uint) error { nowMS := time.Now().UTC().UnixMilli() return s.db.Model(&model.User{}).Where("id = ?", id).Updates(map[string]any{ "last_login_at_ms": &nowMS, "updated_at_ms": nowMS, }).Error } func (s *Store) CreateRefreshToken(token model.RefreshToken) error { return s.db.Create(&token).Error } func (s *Store) RotateRefreshToken(oldTokenID uint, token model.RefreshToken) error { nowMS := time.Now().UTC().UnixMilli() return s.db.Transaction(func(tx *gorm.DB) error { if err := tx.Create(&token).Error; err != nil { return err } return tx.Model(&model.RefreshToken{}). Where("id = ? AND revoked_at_ms IS NULL", oldTokenID). Update("revoked_at_ms", &nowMS).Error }) } func (s *Store) FindRefreshToken(hash string) (*model.RefreshToken, error) { var token model.RefreshToken err := s.db.Where("token_hash = ? AND revoked_at_ms IS NULL AND expires_at_ms > ?", hash, time.Now().UTC().UnixMilli()).First(&token).Error if err != nil { return nil, err } return &token, nil } func (s *Store) RevokeRefreshToken(hash string) error { nowMS := time.Now().UTC().UnixMilli() return s.db.Model(&model.RefreshToken{}).Where("token_hash = ? AND revoked_at_ms IS NULL", hash).Update("revoked_at_ms", &nowMS).Error }