Files
Gardomatic/internal/storage/users.go
T
kleiax 904d14b64c
CI / test (push) Canceled after 0s
Initial commit
2026-09-12 22:22:17 +02:00

142 lines
5.1 KiB
Go

package storage
import (
"time"
"gardomatic.kleiax.de/internal/auth"
"gardomatic.kleiax.de/internal/platform/validate"
)
// UserModelInterface persists accounts and resolves token ownership.
type UserModelInterface interface {
Insert(user User) (User, error)
GetByID(id int) (User, error)
GetByEmail(email string) (User, error)
Update(user User) (User, error)
GetForToken(tokenScope, tokenPlaintext string) (User, error)
CreateEmailChange(userID int, email string, ttl time.Duration) (string, error)
ConfirmEmailChange(tokenPlaintext string, userID int) (User, error)
GetAll() ([]User, error)
UpdateRole(userID int, role ApplicationRole) (User, error)
Delete(userID int) error
}
// ApplicationRole groups garden-independent permissions for an account.
type ApplicationRole string
// ApplicationPermission identifies one garden-independent capability.
type ApplicationPermission string
// Built-in application roles and permissions.
const (
ApplicationRoleUser ApplicationRole = "application:user"
ApplicationRoleAdmin ApplicationRole = "application:admin"
ApplicationPermissionGlobalSpeciesWrite ApplicationPermission = "global_species:write"
ApplicationPermissionUsersManage ApplicationPermission = "users:manage"
ApplicationPermissionSettingsWrite ApplicationPermission = "application_settings:write"
ApplicationPermissionRolesManage ApplicationPermission = "roles:manage"
ApplicationPermissionGardensCreate ApplicationPermission = "gardens:create"
)
// Valid reports whether role is assignable to an account.
func (role ApplicationRole) Valid() bool {
return role == ApplicationRoleUser || role == ApplicationRoleAdmin
}
// Can reports whether an application role grants a permission.
func (role ApplicationRole) Can(permission ApplicationPermission) bool {
if permission == ApplicationPermissionGardensCreate {
return role == ApplicationRoleUser || role == ApplicationRoleAdmin
}
return role == ApplicationRoleAdmin && (permission == ApplicationPermissionGlobalSpeciesWrite ||
permission == ApplicationPermissionUsersManage ||
permission == ApplicationPermissionSettingsWrite ||
permission == ApplicationPermissionRolesManage)
}
// Permissions returns the capabilities bundled into the role.
func (role ApplicationRole) Permissions() []ApplicationPermission {
permissions := []ApplicationPermission{}
for _, permission := range []ApplicationPermission{
ApplicationPermissionGlobalSpeciesWrite,
ApplicationPermissionUsersManage,
ApplicationPermissionSettingsWrite,
ApplicationPermissionRolesManage,
ApplicationPermissionGardensCreate,
} {
if role.Can(permission) {
permissions = append(permissions, permission)
}
}
return permissions
}
// AllApplicationPermissions returns a copy of the supported application-level
// permissions.
func AllApplicationPermissions() []ApplicationPermission {
return []ApplicationPermission{
ApplicationPermissionGlobalSpeciesWrite,
ApplicationPermissionUsersManage,
ApplicationPermissionSettingsWrite,
ApplicationPermissionRolesManage,
ApplicationPermissionGardensCreate,
}
}
// ValidApplicationPermission reports whether permission is application-scoped
// or the global wildcard.
func ValidApplicationPermission(permission string) bool {
for _, candidate := range AllApplicationPermissions() {
if string(candidate) == permission {
return true
}
}
return permission == "*"
}
// User is an account that can own or join gardens.
type User struct {
ID int `json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Name string `json:"name"`
Email string `json:"email"`
Color string `json:"color"`
Password auth.Password `json:"-"`
Activated bool `json:"activated"`
Role ApplicationRole `json:"role"`
Permissions []ApplicationPermission `json:"permissions"`
Version int `json:"-"`
}
// Can reports whether the user's resolved permissions grant permission. It
// falls back to the built-in role only when no resolved list is present.
func (user User) Can(permission ApplicationPermission) bool {
if user.Permissions != nil {
for _, granted := range user.Permissions {
if granted == permission || granted == "*" {
return true
}
}
return false
}
return user.Role.Can(permission)
}
// ValidateEmail applies the accepted email-address rules.
func ValidateEmail(v *validate.Validator, email string) {
v.Check(email != "", "email", "must be provided")
v.Check(validate.Matches(email, validate.EmailRX), "email", "must be a valid email address")
}
// ValidateUser applies account and password validation rules.
func ValidateUser(v *validate.Validator, user User) {
v.Check(user.Name != "", "name", "must be provided")
v.Check(len(user.Name) <= 500, "name", "must not be more than 500 bytes long")
ValidateEmail(v, user.Email)
v.Check(user.Color == "" || validate.Matches(user.Color, validate.ColorRX), "color", "must be a valid hex color")
user.Password.Validate(v)
}