feat: register the Users admin screen with the privileged-member guard

- permissions (the four Winter codes and manage_privileged_groups),
  the user navigation item and the embedded admin YAML
- Users controller: list with deactivated users, row states, the
  groups, created date and activated filters, and the form's name,
  surname and email
- a member of a group in golem15.user.privileged_groups (default
  admin) keeps their email, password and row unless the administrator
  holds golem15.users.manage_privileged_groups; the refusal is a 403
  that saves nothing
This commit is contained in:
Jakub Zych
2026-10-05 13:01:18 +02:00
parent e22f7660f3
commit b410a7fea9
17 changed files with 739 additions and 2 deletions

36
admin.go Normal file
View File

@@ -0,0 +1,36 @@
package user
import (
"embed"
"io/fs"
"git.golem15.com/golem15/sm-user-plugin/controllers"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/pact"
)
// adminFS holds the admin YAML and partials, file by file: the controllers
// directory also holds files that are not admin assets.
//
//go:embed controllers/users/config_list.yaml controllers/users/config_form.yaml controllers/users/config_filter.yaml models/user/fields.yaml models/user/columns.yaml
var adminFS embed.FS
func (p *Plugin) AdminFS() fs.FS { return adminFS }
// AdminControllers hands the controllers a lazy accessor of the booted app,
// so they read the database handle and the config at request time.
func (p *Plugin) AdminControllers() []pact.AdminController {
return controllers.AdminControllers(func() *backpack.App {
if p == nil {
return nil
}
return p.app
})
}
var (
_ pact.AdminAssets = (*Plugin)(nil)
_ pact.HasAdminControllers = (*Plugin)(nil)
_ pact.HasPermissions = (*Plugin)(nil)
_ pact.HasNavigation = (*Plugin)(nil)
)

31
admin_navigation.go Normal file
View File

@@ -0,0 +1,31 @@
package user
import (
"git.golem15.com/golem15/sm-user-plugin/controllers"
"git.golem15.com/golem15/summercms/modules/pact"
)
// Navigation is the plugin's Winter registerNavigation() entry: the main item
// user (order 555) with its side menu. Controller names an admin controller
// id in place of a backend URL, and icons are lucide names.
func (*Plugin) Navigation() []pact.NavigationItem {
return []pact.NavigationItem{
{
Code: "user",
Label: "golem15.user::lang.users.menu_label",
Icon: "user",
Permissions: []string{"golem15.users.*"},
Order: 555,
Controller: "golem15.user.users",
SideMenu: []pact.NavigationItem{
{
Code: "users",
Label: "golem15.user::lang.users.menu_label",
Icon: "user",
Permissions: []string{controllers.PermissionAccessUsers},
Controller: "golem15.user.users",
},
},
},
}
}

49
admin_permissions.go Normal file
View File

@@ -0,0 +1,49 @@
package user
import (
"git.golem15.com/golem15/sm-user-plugin/classes"
"git.golem15.com/golem15/sm-user-plugin/controllers"
"git.golem15.com/golem15/summercms/modules/pact"
)
const userPermissionTab = "golem15.user::lang.plugin.tab"
// Permissions is the plugin's Winter registerPermissions() catalog plus one
// code of its own. The four PHP codes default to the developer role;
// impersonate_user is registered so imported backend roles that name it stay
// valid, although no screen uses it. manage_privileged_groups has no default
// role: only a superuser, or a role that was granted it explicitly, may
// change the email or password of a privileged-group member or delete one.
func (*Plugin) Permissions() []pact.Permission {
return []pact.Permission{
{
Code: controllers.PermissionAccessUsers,
Tab: userPermissionTab,
Label: "golem15.user::lang.plugin.access_users",
Roles: []string{"developer"},
},
{
Code: "golem15.users.access_groups",
Tab: userPermissionTab,
Label: "golem15.user::lang.plugin.access_groups",
Roles: []string{"developer"},
},
{
Code: "golem15.users.access_settings",
Tab: userPermissionTab,
Label: "golem15.user::lang.plugin.access_settings",
Roles: []string{"developer"},
},
{
Code: "golem15.users.impersonate_user",
Tab: userPermissionTab,
Label: "golem15.user::lang.plugin.impersonate_user",
Roles: []string{"developer"},
},
{
Code: classes.PermissionManagePrivilegedGroups,
Tab: userPermissionTab,
Label: "golem15.user::lang.plugin.manage_privileged_groups",
},
}
}

48
classes/admin_actions.go Normal file
View File

@@ -0,0 +1,48 @@
package classes
import (
"context"
"fmt"
"time"
"git.golem15.com/golem15/sm-user-plugin/models"
"gorm.io/gorm"
)
// ThrottleStates reads the ban and suspension state of userIDs from
// user_throttle in one query. A user is banned when any of their rows is
// banned, and suspended when a row is suspended and its suspended_at lies
// inside the suspension window (golem15.user.throttle.suspension_minutes,
// read from ctx as ContextWithThrottle stores it). It is a pure read: an
// expired suspension is not lifted here.
func ThrottleStates(ctx context.Context, db *gorm.DB, userIDs []uint) (banned, suspended map[uint]bool, err error) {
banned, suspended = map[uint]bool{}, map[uint]bool{}
if db == nil {
return nil, nil, fmt.Errorf("golem15.user: gorm handle is missing")
}
if len(userIDs) == 0 {
return banned, suspended, nil
}
minutes := throttleFrom(ctx).minutes
if minutes <= 0 {
minutes = 15
}
window := time.Duration(minutes) * time.Minute
var rows []models.Throttle
err = db.Session(&gorm.Session{NewDB: true}).WithContext(ctx).
Where("user_id IN ? AND (is_banned OR is_suspended)", userIDs).
Find(&rows).Error
if err != nil {
return nil, nil, err
}
now := time.Now()
for _, row := range rows {
if row.IsBanned {
banned[row.UserID] = true
}
if row.IsSuspended && row.SuspendedAt != nil && now.Before(row.SuspendedAt.Add(window)) {
suspended[row.UserID] = true
}
}
return banned, suspended, nil
}

118
classes/privileged.go Normal file
View File

@@ -0,0 +1,118 @@
package classes
import (
"context"
"fmt"
"strings"
"git.golem15.com/golem15/summercms/modules/compass"
"gorm.io/gorm"
)
// PermissionManagePrivilegedGroups is the backend permission an administrator
// needs, on top of golem15.users.access_users, to change the email or the
// password of a member of a privileged group, or to delete such a user for
// good.
const PermissionManagePrivilegedGroups = "golem15.users.manage_privileged_groups"
// privilegedGroupsKey is the config list of privileged group codes.
const privilegedGroupsKey = "golem15.user.privileged_groups"
// defaultPrivilegedGroup is the only privileged code when the config names
// none: the site-admin group.
const defaultPrivilegedGroup = "admin"
// PrivilegedGroupCodes returns the group codes whose members are protected,
// read from golem15.user.privileged_groups on every call so a config change
// applies without a restart. A nil config or a missing key gives the single
// code admin. Blank entries are dropped; a key that is present and empty
// means that no group is privileged.
func PrivilegedGroupCodes(cfg *compass.Config) []string {
if cfg == nil {
return []string{defaultPrivilegedGroup}
}
raw, ok := cfg.Lookup(privilegedGroupsKey)
if !ok {
return []string{defaultPrivilegedGroup}
}
var entries []string
switch v := raw.(type) {
case []string:
entries = v
case []any:
for _, item := range v {
if s, ok := item.(string); ok {
entries = append(entries, s)
}
}
case string:
// An environment override arrives as one comma-separated string.
entries = strings.Split(v, ",")
}
codes := make([]string, 0, len(entries))
for _, entry := range entries {
if code := strings.TrimSpace(entry); code != "" {
codes = append(codes, code)
}
}
return codes
}
// IsPrivilegedCode reports whether a group's code is in codes. Codes compare
// case-sensitively, as HasGroupCode does, and a group without a code is
// never privileged.
func IsPrivilegedCode(codes []string, code *string) bool {
if code == nil || *code == "" {
return false
}
for _, privileged := range codes {
if privileged == *code {
return true
}
}
return false
}
// PrivilegedGroupIDs returns the ids of the user_groups rows whose code is in
// codes, ordered by id.
func PrivilegedGroupIDs(ctx context.Context, db *gorm.DB, codes []string) ([]uint, error) {
if db == nil {
return nil, fmt.Errorf("golem15.user: gorm handle is missing")
}
ids := []uint{}
if len(codes) == 0 {
return ids, nil
}
err := db.Session(&gorm.Session{NewDB: true}).WithContext(ctx).
Table("user_groups").
Where("code IN ?", codes).
Order("id").
Pluck("id", &ids).Error
if err != nil {
return nil, err
}
return ids, nil
}
// IsPrivilegedMember reports whether the user belongs to at least one group
// whose code is in codes. A user in no group, and a group without a code,
// give false. A deactivated user's membership counts: it survives the soft
// delete.
func IsPrivilegedMember(ctx context.Context, db *gorm.DB, codes []string, userID uint) (bool, error) {
if db == nil {
return false, fmt.Errorf("golem15.user: gorm handle is missing")
}
if len(codes) == 0 || userID == 0 {
return false, nil
}
held, err := UserGroupCodes(ctx, db.Session(&gorm.Session{NewDB: true}), userID)
if err != nil {
return false, err
}
for i := range held {
if IsPrivilegedCode(codes, &held[i]) {
return true, nil
}
}
return false, nil
}

View File

@@ -22,3 +22,5 @@ password:
min_length: 8
api_tokens:
prefix: usr_
privileged_groups:
- admin

View File

@@ -0,0 +1,28 @@
package controllers
import (
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/pact"
)
// AdminControllers is the plugin's admin controller list. app returns the
// booted application; it is asked on every call, so a controller never keeps
// a database handle, a config or a mailer of its own.
func AdminControllers(app func() *backpack.App) []pact.AdminController {
return []pact.AdminController{
usersAdminController{app: app},
}
}
func init() {
var _ pact.AdminController = usersAdminController{}
var _ pact.AdminPermissioned = usersAdminController{}
var _ pact.AdminRecordSource = usersAdminController{}
var _ pact.ListExtendQuery = usersAdminController{}
var _ pact.FormExtendQuery = usersAdminController{}
var _ pact.ListRowStates = usersAdminController{}
var _ pact.FormRules = usersAdminController{}
var _ pact.FilterOptions = usersAdminController{}
var _ pact.FormBeforeUpdate = usersAdminController{}
var _ pact.FormBeforeDelete = usersAdminController{}
}

23
controllers/request_db.go Normal file
View File

@@ -0,0 +1,23 @@
package controllers
import (
"context"
"git.golem15.com/golem15/summercms/modules/cabana"
"gorm.io/gorm"
)
// requestDB is the database handle a hook or scope should read through. Inside
// an admin write it is the transaction the write runs in, so the hook sees the
// write's snapshot and does not take a second pool connection while the
// transaction holds row locks. Outside a transaction (the list) it is the
// application pool from the booted app. It is nil when neither exists.
func requestDB(ctx context.Context, pool func() *gorm.DB) *gorm.DB {
if tx, ok := cabana.TxFromContext(ctx); ok {
return tx
}
if pool == nil {
return nil
}
return pool()
}

View File

@@ -0,0 +1,14 @@
scopes:
groups:
label: golem15.user::lang.group.label
modelClass: Golem15\User\Models\UserGroup
nameFrom: name
scope: filterByGroup
created_date:
label: golem15.user::lang.user.created_at
type: daterange
column: created_at
activated:
label: golem15.user::lang.user.status_activated
type: switch
column: is_activated

View File

@@ -0,0 +1,14 @@
name: golem15.user::lang.user.label
form: ~/plugins/golem15/user/models/user/fields.yaml
modelClass: Golem15\User\Models\User
defaultRedirect: golem15/user/users
update:
redirect: golem15/user/users/update/:id
redirectClose: golem15/user/users
messages:
update: backend::lang.messages.form.update
saved: backend::lang.messages.form.saved
deleteConfirm: golem15.user::lang.users.delete_confirm
deleted: backend::lang.messages.form.deleted

View File

@@ -0,0 +1,20 @@
list: ~/plugins/golem15/user/models/user/columns.yaml
modelClass: Golem15\User\Models\User
title: golem15.user::lang.users.list_title
recordUrl: golem15/user/users/update/:id
noRecordsMessage: backend::lang.list.no_records
recordsPerPage: 20
showCheckboxes: true
showSetup: true
filter: config_filter.yaml
toolbar:
buttons: []
search:
prompt: backend::lang.list.search_prompt
messages:
empty: golem15.user::lang.users.list_empty
rowStateDeleted: golem15.user::lang.users.state_deactivated
rowStateNegative: golem15.user::lang.users.state_banned
rowStateDisabled: golem15.user::lang.users.state_not_activated

View File

@@ -0,0 +1,239 @@
package controllers
import (
"context"
"errors"
"strconv"
"git.golem15.com/golem15/sm-user-plugin/classes"
"git.golem15.com/golem15/sm-user-plugin/models"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/bouncer"
"git.golem15.com/golem15/summercms/modules/cabana"
"git.golem15.com/golem15/summercms/modules/compass"
"git.golem15.com/golem15/summercms/modules/pact"
"gorm.io/gorm"
)
// PermissionAccessUsers is the backend permission of the Users screen, the
// PHP Users::$requiredPermissions.
const PermissionAccessUsers = "golem15.users.access_users"
// usersLang prefixes the phrase keys of the Users screen.
const usersLang = "golem15.user::lang.users."
// usersAdminController is the Winter Users backend controller: the list with
// its filters and row states, and the form. The screen and every write need
// golem15.users.access_users.
//
// app is resolved per call from the booted plugin. Nothing is cached: the
// database handle, the config and the privileged group list are read when a
// request needs them.
type usersAdminController struct {
app func() *backpack.App
}
func (usersAdminController) ID() string { return "golem15.user.users" }
func (usersAdminController) ModelName() string { return `Golem15\User\Models\User` }
func (usersAdminController) ConfigDir() string { return "controllers/users" }
func (usersAdminController) RequiredPermissions() []string {
return []string{PermissionAccessUsers}
}
func (usersAdminController) NewRecord() any { return &models.User{} }
// application returns the booted app, or nil before boot.
func (c usersAdminController) application() *backpack.App {
if c.app == nil {
return nil
}
return c.app()
}
// db is the application pool, or nil when the app has none.
func (c usersAdminController) db() *gorm.DB {
app := c.application()
if app == nil {
return nil
}
gdb, ok := app.Lookup[*gorm.DB]()
if !ok {
return nil
}
return gdb
}
// config is the application config, or nil before boot.
func (c usersAdminController) config() *compass.Config {
app := c.application()
if app == nil {
return nil
}
return app.Config
}
// ListExtendQuery and FormExtendQuery include deactivated (soft-deleted)
// users, the PHP listExtendQuery and formExtendQuery withTrashed().
func (usersAdminController) ListExtendQuery(_ context.Context, db *gorm.DB) *gorm.DB {
if db == nil {
return db
}
return db.Unscoped()
}
func (usersAdminController) FormExtendQuery(_ context.Context, db *gorm.DB) *gorm.DB {
if db == nil {
return db
}
return db.Unscoped()
}
// ListRowStates is the PHP listInjectRowClass: deleted for a deactivated
// user, negative for a banned one, disabled for one who is not activated.
// The ban state of the whole page comes from one user_throttle query.
func (c usersAdminController) ListRowStates(ctx context.Context, db *gorm.DB, records []any) ([][]pact.RowState, error) {
ids := make([]uint, 0, len(records))
for _, record := range records {
user, ok := record.(*models.User)
if !ok || user == nil {
return nil, errors.New("users admin: list record is not a user")
}
ids = append(ids, user.ID)
}
banned, _, err := classes.ThrottleStates(classes.ContextWithThrottle(ctx, c.application()), db, ids)
if err != nil {
return nil, err
}
out := make([][]pact.RowState, len(records))
for i, record := range records {
user := record.(*models.User)
if user.DeletedAt.Valid {
out[i] = append(out[i], pact.RowStateDeleted)
}
if banned[user.ID] {
out[i] = append(out[i], pact.RowStateNegative)
}
if !user.IsActivated {
out[i] = append(out[i], pact.RowStateDisabled)
}
}
return out, nil
}
// FormRules are the rules of an admin save. They replace User.Rules(), which
// is the register contract and would ask every admin update for a confirmed
// password.
func (usersAdminController) FormRules(_ context.Context, _ string) map[string]string {
return map[string]string{
"email": "required|between:6,255|email|unique:users",
}
}
// FilterOptions serves the choices of the groups filter (scope
// filterByGroup): every user group by name. Any other scope has no choices.
func (c usersAdminController) FilterOptions(scope string) []pact.Option {
if scope != models.FilterByGroup {
return nil
}
db := c.db()
if db == nil {
return nil
}
var groups []models.UserGroup
if err := db.Session(&gorm.Session{NewDB: true}).Order("name ASC, id ASC").Find(&groups).Error; err != nil {
return nil
}
out := make([]pact.Option, 0, len(groups))
for _, group := range groups {
out = append(out, pact.Option{Value: strconv.FormatUint(uint64(group.ID), 10), Label: group.Name})
}
return out
}
// guardPrivilegedMember protects the account of a privileged-group member
// against takeover. It answers nil when the administrator on ctx holds
// golem15.users.manage_privileged_groups, or when the user is not a member of
// a group whose code is in golem15.user.privileged_groups; otherwise it
// refuses with a 403 carrying message and details. Membership is read
// through the write's transaction. A failed query is returned as it is.
func (c usersAdminController) guardPrivilegedMember(ctx context.Context, userID uint, message string, details map[string]any) error {
principal, _ := bouncer.User(ctx)
if cabana.Allows(principal, []string{classes.PermissionManagePrivilegedGroups}) {
return nil
}
db := requestDB(ctx, c.db)
if db == nil {
return errors.New("users admin: gorm handle is missing")
}
privileged, err := classes.IsPrivilegedMember(ctx, db, classes.PrivilegedGroupCodes(c.config()), userID)
if err != nil {
return err
}
if !privileged {
return nil
}
return &cabana.ForbiddenError{Message: message, Details: details}
}
// FormBeforeUpdate starts with the takeover guard: a changed email or a
// submitted password on a member of a privileged group needs
// golem15.users.manage_privileged_groups. Everything else an update does
// comes after it.
func (c usersAdminController) FormBeforeUpdate(ctx context.Context, model any) error {
user, ok := model.(*models.User)
if !ok || user == nil {
return errors.New("users admin: form model is not a user")
}
if err := c.guardCredentials(ctx, user); err != nil {
return err
}
return nil
}
// guardCredentials compares the email the save would write with the stored
// one and looks for a submitted password, then asks guardPrivilegedMember
// for the fields that would change.
func (c usersAdminController) guardCredentials(ctx context.Context, user *models.User) error {
db := requestDB(ctx, c.db)
if db == nil {
return errors.New("users admin: gorm handle is missing")
}
var stored []string
err := db.Session(&gorm.Session{NewDB: true}).WithContext(ctx).Unscoped().
Model(&models.User{}).Where("id = ?", user.ID).Pluck("email", &stored).Error
if err != nil {
return err
}
if len(stored) != 1 {
return errors.New("users admin: the user being updated was not found")
}
const message = usersLang + "privileged_member_forbidden"
details := map[string]any{}
if stored[0] != user.Email {
details["email"] = []string{message}
}
if values, ok := cabana.VirtualFieldsFromContext(ctx); ok {
if password, _ := values["password"].(string); password != "" {
details["password"] = []string{message}
}
}
if len(details) == 0 {
return nil
}
return c.guardPrivilegedMember(ctx, user.ID, message, details)
}
// FormBeforeDelete refuses the delete of a privileged-group member without
// golem15.users.manage_privileged_groups. The framework calls it for the
// form's delete and once per record inside the bulk delete's transaction, so
// one refused record rolls the whole selection back.
func (c usersAdminController) FormBeforeDelete(ctx context.Context, model any) error {
user, ok := model.(*models.User)
if !ok || user == nil {
return errors.New("users admin: form model is not a user")
}
return c.guardPrivilegedMember(ctx, user.ID, usersLang+"privileged_member_delete_forbidden", nil)
}

View File

@@ -1,3 +1,34 @@
account:
invalid_login: Invalid email or password
no_avatar: Your account has no display picture to remove.
plugin:
tab: Users
access_users: Manage Users
access_groups: Manage User Groups
access_settings: Manage User Settings
impersonate_user: Impersonate Users
manage_privileged_groups: Manage privileged group membership
users:
menu_label: Users
list_title: Manage Users
list_empty: No users match this view. Clear the filters, or add one with New User.
state_deactivated: Deactivated
state_banned: Banned
state_not_activated: Not activated
delete_confirm: Do you really want to delete this user? This user will be permanently removed and unrecoverable.
privileged_member_forbidden: This user belongs to a privileged group. You do not have permission to change their email or password. Nothing was saved.
privileged_member_delete_forbidden: Users who belong to a privileged group cannot be deleted without the permission to manage privileged groups. Nothing was deleted.
user:
label: User
id: ID
name: Name
surname: Surname
email: Email
account: Account
created_at: Registered
last_seen: Last seen
created_ip_address: Created IP Address
last_ip_address: Last IP Address
status_activated: Activated
group:
label: Group

View File

@@ -1,3 +1,34 @@
account:
invalid_login: Nieprawidłowy email lub hasło
no_avatar: Brak avatara
plugin:
tab: Użytkownicy
access_users: Zarządzaj użytkownikami
access_groups: Zarządzaj grupami użytkowników
access_settings: Zarządzaj ustawieniami użytkownika
impersonate_user: Wcielaj się w użytkowników
manage_privileged_groups: Zarządzaj członkostwem w grupach uprzywilejowanych
users:
menu_label: Użytkownicy
list_title: Zarządzaj użytkownikami
list_empty: Brak użytkowników w tym widoku. Wyczyść filtry lub dodaj użytkownika przyciskiem Nowy użytkownik.
state_deactivated: Dezaktywowany
state_banned: Zbanowany
state_not_activated: Nieaktywowany
delete_confirm: Czy na pewno chcesz usunąć tego użytkownika? Zostanie trwale usunięty bez możliwości odzyskania.
privileged_member_forbidden: Ten użytkownik należy do grupy uprzywilejowanej. Nie masz uprawnień do zmiany jego adresu e-mail ani hasła. Nic nie zostało zapisane.
privileged_member_delete_forbidden: Użytkowników należących do grupy uprzywilejowanej nie można usunąć bez uprawnienia do zarządzania grupami uprzywilejowanymi. Nic nie zostało usunięte.
user:
label: Użytkownik
id: ID
name: Imię / nick
surname: Nazwisko
email: Email
account: Konto
created_at: Zarejestrowany
last_seen: Ostatnio widziany
created_ip_address: Stworzono z IP
last_ip_address: Ostatnie IP
status_activated: Aktywowany
group:
label: Grupa

28
models/user/columns.yaml Normal file
View File

@@ -0,0 +1,28 @@
columns:
id:
label: golem15.user::lang.user.id
invisible: true
name:
label: golem15.user::lang.user.name
searchable: true
surname:
label: golem15.user::lang.user.surname
searchable: true
invisible: true
email:
label: golem15.user::lang.user.email
searchable: true
created_at:
label: golem15.user::lang.user.created_at
type: datetime
last_seen:
label: golem15.user::lang.user.last_seen
type: datetime
created_ip_address:
label: golem15.user::lang.user.created_ip_address
searchable: true
invisible: true
last_ip_address:
label: golem15.user::lang.user.last_ip_address
searchable: true
invisible: true

16
models/user/fields.yaml Normal file
View File

@@ -0,0 +1,16 @@
fields:
name:
label: golem15.user::lang.user.name
type: text
span: left
tab: golem15.user::lang.user.account
surname:
label: golem15.user::lang.user.surname
type: text
span: right
tab: golem15.user::lang.user.account
email:
label: golem15.user::lang.user.email
type: text
span: full
tab: golem15.user::lang.user.account

View File

@@ -34,7 +34,9 @@ import (
)
const (
sessionSecret = "summercms-test-only-hs256-secret"
sessionSecret = "summercms-test-only-hs256-secret"
// adminHarnessSecret signs backend tokens in tests that mount the admin API.
adminHarnessSecret = "summercms-test-only-admin-hs256-secret"
invalidLoginBody = `{"error":true,"message":"Invalid email or password"}`
prvHash = "a867434cbc213adfbe78a02bed7082a6bd99c883"
)
@@ -43,7 +45,10 @@ var (
sessionOnce sync.Once
sessionGDB *gorm.DB
sessionSQL *sql.DB
sessionErr error
// sessionDSN is the container's connection string; the admin harness
// opens its own database in the same container with it.
sessionDSN string
sessionErr error
)
func TestLogin(t *testing.T) {
@@ -507,6 +512,9 @@ func sessionConfig(t *testing.T) *compass.Config {
Environ: []string{
"SUMMER_ENV=development",
"SUMMER_GOLEM15__USER__JWT__SECRET=" + sessionSecret,
// The plugin registers admin controllers, so assembling its
// routes mounts the admin API, which needs its own secret.
"SUMMER_ADMIN__JWT__SECRET=" + adminHarnessSecret,
},
})
if err != nil {
@@ -538,6 +546,7 @@ func sessionDB(t *testing.T) (*gorm.DB, *sql.DB) {
sessionErr = err
return
}
sessionDSN = dsn
sqlDB, err := sql.Open("pgx", dsn)
if err != nil {
sessionErr = err