优化
This commit is contained in:
@@ -23,10 +23,10 @@ type AnnouncementFilter struct {
|
||||
|
||||
type AnnouncementRepo interface {
|
||||
Create(context.Context, *Announcement) error
|
||||
Delete(context.Context, uint) error
|
||||
DeleteByIDs(context.Context, []uint) error
|
||||
Delete(context.Context, string) error
|
||||
DeleteByIDs(context.Context, []string) error
|
||||
Update(context.Context, *Announcement) error
|
||||
Find(context.Context, uint) (*Announcement, error)
|
||||
Find(context.Context, string) (*Announcement, error)
|
||||
List(context.Context, AnnouncementFilter) ([]*Announcement, int64, error)
|
||||
UserOptions(context.Context) ([]UserOption, error)
|
||||
}
|
||||
@@ -50,15 +50,15 @@ func (uc *AnnouncementUsecase) Update(ctx context.Context, item *Announcement) e
|
||||
return uc.repo.Update(ctx, item)
|
||||
}
|
||||
|
||||
func (uc *AnnouncementUsecase) Delete(ctx context.Context, id uint) error {
|
||||
func (uc *AnnouncementUsecase) Delete(ctx context.Context, id string) error {
|
||||
return uc.repo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
func (uc *AnnouncementUsecase) DeleteByIDs(ctx context.Context, ids []uint) error {
|
||||
func (uc *AnnouncementUsecase) DeleteByIDs(ctx context.Context, ids []string) error {
|
||||
return uc.repo.DeleteByIDs(ctx, ids)
|
||||
}
|
||||
|
||||
func (uc *AnnouncementUsecase) Find(ctx context.Context, id uint) (*Announcement, error) {
|
||||
func (uc *AnnouncementUsecase) Find(ctx context.Context, id string) (*Announcement, error) {
|
||||
return uc.repo.Find(ctx, id)
|
||||
}
|
||||
|
||||
|
||||
+7
-1
@@ -79,7 +79,13 @@ func (uc *APIUsecase) SyncAPIs(ctx context.Context, routes []*API) (*APISyncDiff
|
||||
for _, item := range stored {
|
||||
storedSet[key(item)] = item
|
||||
}
|
||||
diff := &APISyncDiff{Ignored: ignored}
|
||||
// The sync API always returns JSON arrays for all three collections,
|
||||
// including when no routes were added or removed.
|
||||
diff := &APISyncDiff{
|
||||
Added: make([]*API, 0),
|
||||
Deleted: make([]*API, 0),
|
||||
Ignored: ignored,
|
||||
}
|
||||
added := make(map[string]bool)
|
||||
for _, item := range routes {
|
||||
routeKey := key(item)
|
||||
|
||||
@@ -142,17 +142,14 @@ func (uc *AuthenticationUsecase) SwitchAuthority(ctx context.Context, claims *Au
|
||||
if err := uc.users.SetUserAuthority(ctx, claims.ID, authorityID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, err := uc.users.User(ctx, claims.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// IssueToken receives the remaining lifetime rather than zero so the new
|
||||
// token expires at the same instant as the token it replaced.
|
||||
remaining := time.Until(claims.ExpiresAt)
|
||||
issued, err := uc.issuer.IssueToken(user, authorityID, claims.MustChangePwd, remaining)
|
||||
// Role switching signs the current claims again after replacing only the
|
||||
// authority ID. It must not reload mutable profile fields or reset JWT
|
||||
// timing/buffer claims as if this were a fresh login.
|
||||
issued, err := uc.issuer.ReissueToken(claims, authorityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user := &User{ID: claims.ID, UUID: claims.UUID, Username: claims.Username, NickName: claims.NickName, AuthorityID: authorityID, MustChangePassword: claims.MustChangePwd}
|
||||
return &AuthenticationResult{User: user, Token: issued.Value, ExpiresAt: issued.ExpiresAt, NeedChangePassword: claims.MustChangePwd}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ func (r *authenticationUserRepo) FindUserByUsername(context.Context, string) (*U
|
||||
return r.user, nil
|
||||
}
|
||||
|
||||
func (*authenticationUserRepo) HasAuthorityMenu(context.Context, uint, string) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
type authenticationSecurityRepo struct{ SecurityRepo }
|
||||
|
||||
func (*authenticationSecurityRepo) SecurityConfig(context.Context) (*SecurityConfig, error) {
|
||||
@@ -48,11 +52,42 @@ func (*authenticationIssuer) IssueToken(*User, uint, bool, time.Duration) (*Issu
|
||||
return &IssuedToken{Value: "token", ExpiresAt: time.Now().Add(time.Hour), TTL: time.Hour}, nil
|
||||
}
|
||||
|
||||
func (*authenticationIssuer) ReissueToken(claims *AuthClaims, _ uint) (*IssuedToken, error) {
|
||||
return &IssuedToken{Value: "token", ExpiresAt: claims.ExpiresAt, TTL: time.Until(claims.ExpiresAt)}, nil
|
||||
}
|
||||
|
||||
type authenticationAudit struct {
|
||||
AuditRecordRepo
|
||||
logins []*LoginLog
|
||||
}
|
||||
|
||||
type switchAuthorityUserRepo struct {
|
||||
UserRepo
|
||||
setUserID, setAuthorityID uint
|
||||
findCalls int
|
||||
}
|
||||
|
||||
func (r *switchAuthorityUserRepo) SetUserAuthority(_ context.Context, userID, authorityID uint) error {
|
||||
r.setUserID, r.setAuthorityID = userID, authorityID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *switchAuthorityUserRepo) FindUserByID(context.Context, uint) (*User, error) {
|
||||
r.findCalls++
|
||||
return &User{NickName: "changed-in-database"}, nil
|
||||
}
|
||||
|
||||
type switchAuthorityIssuer struct {
|
||||
TokenIssuer
|
||||
claims *AuthClaims
|
||||
authorityID uint
|
||||
}
|
||||
|
||||
func (i *switchAuthorityIssuer) ReissueToken(claims *AuthClaims, authorityID uint) (*IssuedToken, error) {
|
||||
i.claims, i.authorityID = claims, authorityID
|
||||
return &IssuedToken{Value: "switched", ExpiresAt: claims.ExpiresAt, TTL: time.Until(claims.ExpiresAt)}, nil
|
||||
}
|
||||
|
||||
func (a *authenticationAudit) RecordLogin(_ context.Context, value *LoginLog) error {
|
||||
a.logins = append(a.logins, value)
|
||||
return nil
|
||||
@@ -77,3 +112,28 @@ func TestLoginRecordsSuccessBeforeMultipointCacheFailure(t *testing.T) {
|
||||
t.Fatalf("expected successful login audit before cache failure, got %+v", audit.logins)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwitchAuthorityReissuesCurrentClaimsWithoutReloadingUser(t *testing.T) {
|
||||
repo := &switchAuthorityUserRepo{}
|
||||
issuer := &switchAuthorityIssuer{}
|
||||
uc := NewAuthenticationUsecase(NewUserUsecase(repo), nil, issuer, nil)
|
||||
expiresAt := time.Now().Add(time.Hour).Truncate(time.Second)
|
||||
claims := &AuthClaims{ID: 7, UUID: "uuid", Username: "admin", NickName: "token-nickname", AuthorityID: 888, MustChangePwd: true, ExpiresAt: expiresAt}
|
||||
|
||||
result, err := uc.SwitchAuthority(context.Background(), claims, 999)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if repo.setUserID != 7 || repo.setAuthorityID != 999 {
|
||||
t.Fatalf("stored authority switch = user:%d authority:%d", repo.setUserID, repo.setAuthorityID)
|
||||
}
|
||||
if repo.findCalls != 0 {
|
||||
t.Fatalf("switch reloaded user %d times", repo.findCalls)
|
||||
}
|
||||
if issuer.claims != claims || issuer.authorityID != 999 {
|
||||
t.Fatalf("reissue input = claims:%p authority:%d", issuer.claims, issuer.authorityID)
|
||||
}
|
||||
if result.User.NickName != "token-nickname" || result.User.AuthorityID != 999 || !result.NeedChangePassword || !result.ExpiresAt.Equal(expiresAt) {
|
||||
t.Fatalf("switch result = %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ type Authority struct {
|
||||
DataScope int
|
||||
DefaultRouter string
|
||||
Children []*Authority
|
||||
Menus []*Menu
|
||||
}
|
||||
|
||||
type AuthorityUsecase struct{ AuthorityAccessRepo }
|
||||
|
||||
@@ -37,6 +37,7 @@ type DictionaryDetailRepo interface {
|
||||
FindDictionaryDetail(context.Context, uint) (*DictionaryDetail, error)
|
||||
ListDictionaryDetails(context.Context, int, int, DictionaryDetailFilter) ([]*DictionaryDetail, int64, error)
|
||||
DictionaryDetailTree(context.Context, uint, string) ([]*DictionaryDetail, error)
|
||||
DictionaryDetailsByParent(context.Context, uint, *uint, bool) ([]*DictionaryDetail, error)
|
||||
}
|
||||
|
||||
type Dictionary struct {
|
||||
@@ -78,38 +79,5 @@ func (uc *DictionaryUsecase) ImportDictionary(ctx context.Context, dictionary *D
|
||||
}
|
||||
|
||||
func (uc *DictionaryUsecase) DictionaryDetailsByParent(ctx context.Context, dictionaryID uint, parentID *uint, includeChildren bool) ([]*DictionaryDetail, error) {
|
||||
items, _, err := uc.ListDictionaryDetails(ctx, 0, 0, DictionaryDetailFilter{DictionaryID: dictionaryID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
direct := make([]*DictionaryDetail, 0)
|
||||
for _, item := range items {
|
||||
if (parentID == nil && item.ParentID == nil) || (parentID != nil && item.ParentID != nil && *item.ParentID == *parentID) {
|
||||
direct = append(direct, item)
|
||||
}
|
||||
}
|
||||
if !includeChildren {
|
||||
return direct, nil
|
||||
}
|
||||
children := make(map[uint][]*DictionaryDetail)
|
||||
for _, item := range items {
|
||||
item.Children = []*DictionaryDetail{}
|
||||
if item.ParentID != nil {
|
||||
children[*item.ParentID] = append(children[*item.ParentID], item)
|
||||
}
|
||||
}
|
||||
var attach func(*DictionaryDetail)
|
||||
attach = func(item *DictionaryDetail) {
|
||||
item.Children = children[item.ID]
|
||||
if item.Children == nil {
|
||||
item.Children = []*DictionaryDetail{}
|
||||
}
|
||||
for _, child := range item.Children {
|
||||
attach(child)
|
||||
}
|
||||
}
|
||||
for _, item := range direct {
|
||||
attach(item)
|
||||
}
|
||||
return direct, nil
|
||||
return uc.DictionaryRepo.DictionaryDetailsByParent(ctx, dictionaryID, parentID, includeChildren)
|
||||
}
|
||||
|
||||
@@ -82,6 +82,8 @@ type AuthClaims struct {
|
||||
BufferTime time.Duration
|
||||
MustChangePwd bool
|
||||
Issuer string
|
||||
Audience []string
|
||||
NotBefore time.Time
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
@@ -96,5 +98,6 @@ var (
|
||||
|
||||
type TokenIssuer interface {
|
||||
IssueToken(*User, uint, bool, time.Duration) (*IssuedToken, error)
|
||||
ReissueToken(*AuthClaims, uint) (*IssuedToken, error)
|
||||
ParseToken(string) (*AuthClaims, error)
|
||||
}
|
||||
|
||||
+10
-5
@@ -132,10 +132,11 @@ func (uc *MediaUsecase) ListStorage(ctx context.Context, prefix, cursor string,
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
// Multipart chunks are staged on the local filesystem, so listOssFiles
|
||||
// never exposes them. Kra's storage abstraction may stage them through
|
||||
// the selected backend; hide the internal prefix to preserve the API.
|
||||
chunkRoot := ".chunks"
|
||||
// Multipart chunks are an internal staging detail. The compatible local
|
||||
// implementation keeps them outside the configured object-storage root, so
|
||||
// the public listing must not expose them even when the selected backend is
|
||||
// also used for staging.
|
||||
chunkRoot := "uploads/chunks"
|
||||
if uc.settings != nil {
|
||||
if configured := strings.Trim(uc.settings.MediaSettings().ChunkDir, "/\\ "); configured != "" {
|
||||
chunkRoot = configured
|
||||
@@ -145,7 +146,11 @@ func (uc *MediaUsecase) ListStorage(ctx context.Context, prefix, cursor string,
|
||||
result := make([]*StoredFile, 0, limit)
|
||||
nextCursor := cursor
|
||||
for {
|
||||
items, next, more, err := uc.files.List(ctx, prefix, nextCursor, limit)
|
||||
// Ask only for the remaining visible capacity. Otherwise a page that is
|
||||
// partially filtered could fill the response halfway through the next
|
||||
// storage page while returning that page's end cursor, skipping objects.
|
||||
remaining := limit - len(result)
|
||||
items, next, more, err := uc.files.List(ctx, prefix, nextCursor, remaining)
|
||||
if err != nil {
|
||||
return nil, "", false, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type listStorage struct {
|
||||
items []*StoredFile
|
||||
limits []int
|
||||
}
|
||||
|
||||
func (*listStorage) Put(context.Context, string, io.Reader) (*StoredFile, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (*listStorage) Open(context.Context, string) (io.ReadCloser, error) {
|
||||
return io.NopCloser(strings.NewReader("")), nil
|
||||
}
|
||||
func (*listStorage) Delete(context.Context, string) error { return nil }
|
||||
func (*listStorage) Compose(context.Context, []string, string) (*StoredFile, string, error) {
|
||||
return nil, "", nil
|
||||
}
|
||||
func (*listStorage) DeletePrefix(context.Context, string) error { return nil }
|
||||
func (s *listStorage) List(_ context.Context, _ string, cursor string, limit int) ([]*StoredFile, string, bool, error) {
|
||||
s.limits = append(s.limits, limit)
|
||||
start := 0
|
||||
for start < len(s.items) && s.items[start].Path <= cursor {
|
||||
start++
|
||||
}
|
||||
end := start + limit
|
||||
if end > len(s.items) {
|
||||
end = len(s.items)
|
||||
}
|
||||
page := s.items[start:end]
|
||||
more := end < len(s.items)
|
||||
next := ""
|
||||
if len(page) > 0 {
|
||||
next = page[len(page)-1].Path
|
||||
}
|
||||
return page, next, more, nil
|
||||
}
|
||||
|
||||
func TestListStorageFiltersChunksWithoutSkippingVisibleObjects(t *testing.T) {
|
||||
storage := &listStorage{items: []*StoredFile{
|
||||
{Path: "uploads/chunks/1/00000000"},
|
||||
{Path: "uploads/chunks/1/00000001"},
|
||||
{Path: "visible/a"},
|
||||
{Path: "visible/b"},
|
||||
{Path: "visible/c"},
|
||||
{Path: "visible/d"},
|
||||
{Path: "visible/e"},
|
||||
}}
|
||||
uc := NewMediaUsecase(nil, storage, nil)
|
||||
|
||||
first, cursor, more, err := uc.ListStorage(context.Background(), "", "", 3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := []string{first[0].Path, first[1].Path, first[2].Path}; strings.Join(got, ",") != "visible/a,visible/b,visible/c" {
|
||||
t.Fatalf("first page = %v", got)
|
||||
}
|
||||
if cursor != "visible/c" || !more {
|
||||
t.Fatalf("first cursor/more = %q/%v", cursor, more)
|
||||
}
|
||||
if len(storage.limits) != 2 || storage.limits[0] != 3 || storage.limits[1] != 2 {
|
||||
t.Fatalf("storage limits = %v, want [3 2]", storage.limits)
|
||||
}
|
||||
|
||||
second, cursor, more, err := uc.ListStorage(context.Background(), "", cursor, 3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := []string{second[0].Path, second[1].Path}; strings.Join(got, ",") != "visible/d,visible/e" {
|
||||
t.Fatalf("second page = %v", got)
|
||||
}
|
||||
if cursor != "visible/e" || more {
|
||||
t.Fatalf("second cursor/more = %q/%v", cursor, more)
|
||||
}
|
||||
}
|
||||
+9
-24
@@ -59,6 +59,11 @@ type MenuRepo interface {
|
||||
List(context.Context) ([]*Menu, error)
|
||||
SetAuthorityMenus(context.Context, uint, []uint) error
|
||||
AuthorityMenuIDs(context.Context, uint) ([]uint, error)
|
||||
// ListAuthorityMenus returns the base menus assigned to the target
|
||||
// authority. Unlike List, it must not apply the current actor's strict
|
||||
// menu filter: the administration endpoint is explicitly querying another
|
||||
// role's assignments.
|
||||
ListAuthorityMenus(context.Context, uint) ([]*Menu, error)
|
||||
MenuRoleIDs(context.Context, uint) ([]uint, error)
|
||||
DefaultRouterRoleIDs(context.Context, uint) ([]uint, error)
|
||||
SetMenuRoles(context.Context, uint, []uint) error
|
||||
@@ -79,10 +84,10 @@ func (uc *MenuUsecase) Tree(ctx context.Context) ([]*Menu, error) {
|
||||
}
|
||||
byID := make(map[uint]*Menu, len(items))
|
||||
for _, item := range items {
|
||||
item.Children = []*Menu{}
|
||||
item.Children = nil
|
||||
byID[item.ID] = item
|
||||
}
|
||||
roots := make([]*Menu, 0)
|
||||
var roots []*Menu
|
||||
for _, item := range items {
|
||||
if parent := byID[item.ParentID]; parent != nil {
|
||||
parent.Children = append(parent.Children, item)
|
||||
@@ -114,31 +119,11 @@ func (uc *MenuUsecase) SetAuthorityMenus(ctx context.Context, authorityID uint,
|
||||
}
|
||||
|
||||
func (uc *MenuUsecase) AuthorityMenus(ctx context.Context, authorityID uint) ([]*Menu, error) {
|
||||
ids, err := uc.repo.AuthorityMenuIDs(ctx, authorityID)
|
||||
items, err := uc.repo.ListAuthorityMenus(ctx, authorityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items, err := uc.repo.List(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
selected := make(map[uint]struct{}, len(ids))
|
||||
for _, id := range ids {
|
||||
selected[id] = struct{}{}
|
||||
}
|
||||
result := make([]*Menu, 0, len(ids))
|
||||
for _, item := range items {
|
||||
if _, ok := selected[item.ID]; ok {
|
||||
// The authority-menu endpoint returns menu relations only. Keep its
|
||||
// existing contract: button details are loaded by the dedicated
|
||||
// authority-button endpoints. The compatible query does not preload either
|
||||
// relation here, so parameters must remain absent as well.
|
||||
item.Buttons = nil
|
||||
item.Parameters = nil
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (uc *MenuUsecase) RoleIDs(ctx context.Context, menuID uint) ([]uint, error) {
|
||||
|
||||
@@ -20,8 +20,9 @@ type SystemParameter struct {
|
||||
type ParameterRepo interface {
|
||||
CreateParameter(context.Context, *SystemParameter) error
|
||||
UpdateParameter(context.Context, *SystemParameter) error
|
||||
DeleteParameters(context.Context, []uint) error
|
||||
FindParameter(context.Context, uint, string) (*SystemParameter, error)
|
||||
DeleteParameters(context.Context, []string) error
|
||||
FindParameterByID(context.Context, string) (*SystemParameter, error)
|
||||
FindParameterByKey(context.Context, string) (*SystemParameter, error)
|
||||
ListParameters(context.Context, int, int, *SystemParameter) ([]*SystemParameter, int64, error)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ type PermissionRepo interface {
|
||||
AuthorityButtonIDs(context.Context, uint) ([]uint, error)
|
||||
SelectedButtons(context.Context, uint, uint) ([]uint, error)
|
||||
SetSelectedButtons(context.Context, uint, uint, []uint) error
|
||||
CanRemoveButton(context.Context, uint) (bool, error)
|
||||
CanRemoveButton(context.Context, string) (bool, error)
|
||||
}
|
||||
|
||||
type PermissionUsecase struct{ PermissionRepo }
|
||||
|
||||
@@ -99,7 +99,7 @@ func (uc *SecurityUsecase) Current(ctx context.Context) (*SecurityConfig, error)
|
||||
uc.mu.RUnlock()
|
||||
value, err := uc.repo.SecurityConfig(ctx)
|
||||
if err != nil {
|
||||
// GVA returns its default value together with the database-not-ready
|
||||
// Return the default value together with the database-not-ready
|
||||
// error. Callers such as login/runtime policy consumers intentionally
|
||||
// ignore the error and continue with that default, while the HTTP
|
||||
// settings endpoint still reports the failure.
|
||||
@@ -115,6 +115,13 @@ func (uc *SecurityUsecase) Current(ctx context.Context) (*SecurityConfig, error)
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// Security returns the persisted row for the administration endpoint. Runtime
|
||||
// policy consumers use Current so their hot path remains cached, while the
|
||||
// settings page mirrors the reference behavior and always observes storage.
|
||||
func (uc *SecurityUsecase) Security(ctx context.Context) (*SecurityConfig, error) {
|
||||
return uc.repo.SecurityConfig(ctx)
|
||||
}
|
||||
|
||||
func (uc *SecurityUsecase) ValidatePassword(value *SecurityConfig, password string) error {
|
||||
if value.PwdMinLength > 0 && utf8.RuneCountInString(password) < value.PwdMinLength {
|
||||
return fmt.Errorf("密码长度不能少于 %d 位", value.PwdMinLength)
|
||||
|
||||
@@ -81,3 +81,32 @@ func TestUpdateSecurityDoesNotBackfillWithoutDisabledToEnabledTransition(t *test
|
||||
t.Fatalf("call order = %v, want [get save]", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecurityReadsPersistedValueWhileCurrentUsesCache(t *testing.T) {
|
||||
repo := &securityUpdateRepo{current: &SecurityConfig{ID: 1, LimitCount: 10}}
|
||||
uc := NewSecurityUsecase(repo, nil, nil, nil)
|
||||
|
||||
cached, err := uc.Current(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cached.LimitCount != 10 {
|
||||
t.Fatalf("initial cached limit = %d", cached.LimitCount)
|
||||
}
|
||||
repo.current.LimitCount = 20
|
||||
|
||||
current, err := uc.Current(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if current.LimitCount != 10 {
|
||||
t.Fatalf("Current() bypassed cache: %d", current.LimitCount)
|
||||
}
|
||||
fresh, err := uc.Security(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fresh.LimitCount != 20 {
|
||||
t.Fatalf("Security() returned stale value: %d", fresh.LimitCount)
|
||||
}
|
||||
}
|
||||
|
||||
+21
-28
@@ -38,6 +38,8 @@ type UserListFilter struct {
|
||||
type UserRepo interface {
|
||||
FindUserByUsername(context.Context, string) (*User, error)
|
||||
FindUserByID(context.Context, uint) (*User, error)
|
||||
FindUserByUUID(context.Context, string) (*User, error)
|
||||
HasAuthorityMenu(context.Context, uint, string) (bool, error)
|
||||
MenusByAuthority(context.Context, uint) ([]*Menu, error)
|
||||
ListUsers(context.Context, int, int, *UserListFilter) ([]*User, int64, error)
|
||||
CreateUser(context.Context, *User) (*User, error)
|
||||
@@ -77,23 +79,29 @@ func (uc *UserUsecase) User(ctx context.Context, id uint) (*User, error) {
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (uc *UserUsecase) fallbackDefaultRouter(ctx context.Context, user *User) {
|
||||
if user == nil || user.Authority.DefaultRouter == "" {
|
||||
return
|
||||
}
|
||||
menus, err := uc.repo.MenusByAuthority(ctx, user.AuthorityID)
|
||||
if err != nil || !menuNameExists(menus, user.Authority.DefaultRouter) {
|
||||
user.Authority.DefaultRouter = "404"
|
||||
// UserByUUID resolves the identity from the UUID embedded in the JWT rather
|
||||
// than from the mutable numeric ID used by administrative records.
|
||||
func (uc *UserUsecase) UserByUUID(ctx context.Context, uuid string) (*User, error) {
|
||||
user, err := uc.repo.FindUserByUUID(ctx, uuid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = uc.repo.FillDepartmentNamePaths(ctx, user)
|
||||
uc.fallbackDefaultRouter(ctx, user)
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func menuNameExists(menus []*Menu, name string) bool {
|
||||
for _, menu := range menus {
|
||||
if menu.Name == name || menuNameExists(menu.Children, name) {
|
||||
return true
|
||||
}
|
||||
func (uc *UserUsecase) fallbackDefaultRouter(ctx context.Context, user *User) {
|
||||
if user == nil {
|
||||
return
|
||||
}
|
||||
hasMenu, err := uc.repo.HasAuthorityMenu(ctx, user.AuthorityID, user.Authority.DefaultRouter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !hasMenu {
|
||||
user.Authority.DefaultRouter = "404"
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (uc *UserUsecase) Menus(ctx context.Context, authorityID uint) ([]*Menu, error) {
|
||||
@@ -104,10 +112,6 @@ func (uc *UserUsecase) ListUsers(ctx context.Context, page, pageSize int, filter
|
||||
return uc.repo.ListUsers(ctx, page, pageSize, filter)
|
||||
}
|
||||
func (uc *UserUsecase) CreateUser(ctx context.Context, user *User, authorityIDs []uint) (*User, error) {
|
||||
if user.AuthorityID == 0 && len(authorityIDs) > 0 {
|
||||
user.AuthorityID = authorityIDs[0]
|
||||
}
|
||||
authorityIDs = includeAuthority(authorityIDs, user.AuthorityID)
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -132,17 +136,6 @@ func (uc *UserUsecase) UpdateSelfUser(ctx context.Context, user *User) error {
|
||||
return uc.repo.UpdateSelfUser(ctx, user)
|
||||
}
|
||||
|
||||
func includeAuthority(ids []uint, primary uint) []uint {
|
||||
if primary == 0 {
|
||||
return ids
|
||||
}
|
||||
for _, id := range ids {
|
||||
if id == primary {
|
||||
return ids
|
||||
}
|
||||
}
|
||||
return append(ids, primary)
|
||||
}
|
||||
func (uc *UserUsecase) DeleteUser(ctx context.Context, id uint) error {
|
||||
return uc.repo.DeleteUser(ctx, id)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type defaultRouterUserRepo struct {
|
||||
UserRepo
|
||||
hasMenu bool
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *defaultRouterUserRepo) HasAuthorityMenu(context.Context, uint, string) (bool, error) {
|
||||
return r.hasMenu, r.err
|
||||
}
|
||||
|
||||
func TestFallbackDefaultRouterMatchesMenuLookupOutcome(t *testing.T) {
|
||||
user := &User{AuthorityID: 888, Authority: Authority{DefaultRouter: "dashboard"}}
|
||||
NewUserUsecase(&defaultRouterUserRepo{hasMenu: false}).fallbackDefaultRouter(context.Background(), user)
|
||||
if user.Authority.DefaultRouter != "404" {
|
||||
t.Fatalf("missing default route = %q", user.Authority.DefaultRouter)
|
||||
}
|
||||
|
||||
user.Authority.DefaultRouter = "dashboard"
|
||||
NewUserUsecase(&defaultRouterUserRepo{err: errors.New("database unavailable")}).fallbackDefaultRouter(context.Background(), user)
|
||||
if user.Authority.DefaultRouter != "dashboard" {
|
||||
t.Fatalf("lookup error changed default route to %q", user.Authority.DefaultRouter)
|
||||
}
|
||||
|
||||
user.Authority.DefaultRouter = ""
|
||||
NewUserUsecase(&defaultRouterUserRepo{}).fallbackDefaultRouter(context.Background(), user)
|
||||
if user.Authority.DefaultRouter != "404" {
|
||||
t.Fatalf("empty missing default route = %q", user.Authority.DefaultRouter)
|
||||
}
|
||||
}
|
||||
@@ -30,10 +30,10 @@ type Version struct {
|
||||
ID uint
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Name string
|
||||
Code string
|
||||
Description string
|
||||
Data string
|
||||
Name *string
|
||||
Code *string
|
||||
Description *string
|
||||
Data *string
|
||||
}
|
||||
type VersionBundle struct {
|
||||
Name string
|
||||
|
||||
Reference in New Issue
Block a user