You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
561 lines
15 KiB
Go
561 lines
15 KiB
Go
package admin
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/knowfoolery/backend/shared/errors"
|
|
"github.com/knowfoolery/backend/shared/types"
|
|
)
|
|
|
|
type AdminSessionStatus string
|
|
|
|
const (
|
|
AdminSessionStatusActive AdminSessionStatus = "active"
|
|
AdminSessionStatusExpired AdminSessionStatus = "expired"
|
|
AdminSessionStatusRevoked AdminSessionStatus = "revoked"
|
|
AdminSessionStatusSuspended AdminSessionStatus = "suspended"
|
|
)
|
|
|
|
type AdminSession struct {
|
|
id types.AdminSessionID
|
|
adminID types.UserID
|
|
adminName string
|
|
adminEmail string
|
|
roles []AdminRole
|
|
permissions []Permission
|
|
sessionToken string
|
|
refreshToken string
|
|
ipAddress string
|
|
userAgent string
|
|
status AdminSessionStatus
|
|
createdAt time.Time
|
|
lastActiveAt time.Time
|
|
expiresAt time.Time
|
|
metadata map[string]interface{}
|
|
events []AdminSessionEvent
|
|
}
|
|
|
|
type AdminRole string
|
|
|
|
const (
|
|
AdminRoleSuperAdmin AdminRole = "super_admin"
|
|
AdminRoleModerator AdminRole = "moderator"
|
|
AdminRoleQuestionEditor AdminRole = "question_editor"
|
|
AdminRoleUserManager AdminRole = "user_manager"
|
|
AdminRoleReporter AdminRole = "reporter"
|
|
)
|
|
|
|
type Permission string
|
|
|
|
const (
|
|
PermissionManageUsers Permission = "manage_users"
|
|
PermissionManageQuestions Permission = "manage_questions"
|
|
PermissionManageThemes Permission = "manage_themes"
|
|
PermissionViewReports Permission = "view_reports"
|
|
PermissionManageSettings Permission = "manage_settings"
|
|
PermissionManageAdmins Permission = "manage_admins"
|
|
PermissionViewLogs Permission = "view_logs"
|
|
PermissionManageLeaderboards Permission = "manage_leaderboards"
|
|
PermissionModerateContent Permission = "moderate_content"
|
|
PermissionManageSystem Permission = "manage_system"
|
|
)
|
|
|
|
type AdminSessionEvent struct {
|
|
ID string
|
|
Type AdminSessionEventType
|
|
Data map[string]interface{}
|
|
Timestamp time.Time
|
|
}
|
|
|
|
type AdminSessionEventType string
|
|
|
|
const (
|
|
AdminSessionEventCreated AdminSessionEventType = "session_created"
|
|
AdminSessionEventRefreshed AdminSessionEventType = "session_refreshed"
|
|
AdminSessionEventExpired AdminSessionEventType = "session_expired"
|
|
AdminSessionEventRevoked AdminSessionEventType = "session_revoked"
|
|
AdminSessionEventSuspended AdminSessionEventType = "session_suspended"
|
|
AdminSessionEventPermissionAdded AdminSessionEventType = "permission_added"
|
|
AdminSessionEventPermissionRemoved AdminSessionEventType = "permission_removed"
|
|
AdminSessionEventRoleAssigned AdminSessionEventType = "role_assigned"
|
|
AdminSessionEventRoleRevoked AdminSessionEventType = "role_revoked"
|
|
)
|
|
|
|
func NewAdminSession(
|
|
id types.AdminSessionID,
|
|
adminID types.UserID,
|
|
adminName, adminEmail, sessionToken string,
|
|
roles []AdminRole,
|
|
expirationDuration time.Duration,
|
|
) (*AdminSession, error) {
|
|
if id == "" {
|
|
return nil, errors.ErrInvalidSessionID
|
|
}
|
|
if adminID == "" {
|
|
return nil, errors.ErrInvalidPlayerID
|
|
}
|
|
if adminName == "" {
|
|
return nil, errors.ErrInvalidPlayerName("", "admin name cannot be empty")
|
|
}
|
|
if adminEmail == "" {
|
|
return nil, errors.ErrValidationFailed("email", "admin email cannot be empty")
|
|
}
|
|
if sessionToken == "" {
|
|
return nil, errors.ErrValidationFailed("session_token", "session token cannot be empty")
|
|
}
|
|
if expirationDuration <= 0 {
|
|
expirationDuration = time.Hour * 8
|
|
}
|
|
|
|
now := time.Now()
|
|
permissions := calculatePermissionsFromRoles(roles)
|
|
|
|
session := &AdminSession{
|
|
id: id,
|
|
adminID: adminID,
|
|
adminName: adminName,
|
|
adminEmail: adminEmail,
|
|
roles: roles,
|
|
permissions: permissions,
|
|
sessionToken: sessionToken,
|
|
status: AdminSessionStatusActive,
|
|
createdAt: now,
|
|
lastActiveAt: now,
|
|
expiresAt: now.Add(expirationDuration),
|
|
metadata: make(map[string]interface{}),
|
|
events: make([]AdminSessionEvent, 0),
|
|
}
|
|
|
|
session.addEvent(AdminSessionEventCreated, map[string]interface{}{
|
|
"admin_id": adminID,
|
|
"admin_name": adminName,
|
|
"admin_email": adminEmail,
|
|
"roles": roles,
|
|
"expires_at": session.expiresAt,
|
|
})
|
|
|
|
return session, nil
|
|
}
|
|
|
|
func calculatePermissionsFromRoles(roles []AdminRole) []Permission {
|
|
permissionSet := make(map[Permission]bool)
|
|
|
|
for _, role := range roles {
|
|
rolePermissions := getRolePermissions(role)
|
|
for _, perm := range rolePermissions {
|
|
permissionSet[perm] = true
|
|
}
|
|
}
|
|
|
|
permissions := make([]Permission, 0, len(permissionSet))
|
|
for perm := range permissionSet {
|
|
permissions = append(permissions, perm)
|
|
}
|
|
|
|
return permissions
|
|
}
|
|
|
|
func getRolePermissions(role AdminRole) []Permission {
|
|
switch role {
|
|
case AdminRoleSuperAdmin:
|
|
return []Permission{
|
|
PermissionManageUsers, PermissionManageQuestions, PermissionManageThemes,
|
|
PermissionViewReports, PermissionManageSettings, PermissionManageAdmins,
|
|
PermissionViewLogs, PermissionManageLeaderboards, PermissionModerateContent,
|
|
PermissionManageSystem,
|
|
}
|
|
case AdminRoleModerator:
|
|
return []Permission{
|
|
PermissionManageUsers, PermissionViewReports, PermissionModerateContent,
|
|
PermissionViewLogs,
|
|
}
|
|
case AdminRoleQuestionEditor:
|
|
return []Permission{
|
|
PermissionManageQuestions, PermissionManageThemes, PermissionViewReports,
|
|
}
|
|
case AdminRoleUserManager:
|
|
return []Permission{
|
|
PermissionManageUsers, PermissionViewReports, PermissionViewLogs,
|
|
}
|
|
case AdminRoleReporter:
|
|
return []Permission{
|
|
PermissionViewReports, PermissionViewLogs,
|
|
}
|
|
default:
|
|
return []Permission{}
|
|
}
|
|
}
|
|
|
|
func (as *AdminSession) ID() types.AdminSessionID {
|
|
return as.id
|
|
}
|
|
|
|
func (as *AdminSession) AdminID() types.UserID {
|
|
return as.adminID
|
|
}
|
|
|
|
func (as *AdminSession) AdminName() string {
|
|
return as.adminName
|
|
}
|
|
|
|
func (as *AdminSession) AdminEmail() string {
|
|
return as.adminEmail
|
|
}
|
|
|
|
func (as *AdminSession) Roles() []AdminRole {
|
|
result := make([]AdminRole, len(as.roles))
|
|
copy(result, as.roles)
|
|
return result
|
|
}
|
|
|
|
func (as *AdminSession) Permissions() []Permission {
|
|
result := make([]Permission, len(as.permissions))
|
|
copy(result, as.permissions)
|
|
return result
|
|
}
|
|
|
|
func (as *AdminSession) SessionToken() string {
|
|
return as.sessionToken
|
|
}
|
|
|
|
func (as *AdminSession) RefreshToken() string {
|
|
return as.refreshToken
|
|
}
|
|
|
|
func (as *AdminSession) IPAddress() string {
|
|
return as.ipAddress
|
|
}
|
|
|
|
func (as *AdminSession) UserAgent() string {
|
|
return as.userAgent
|
|
}
|
|
|
|
func (as *AdminSession) Status() AdminSessionStatus {
|
|
return as.status
|
|
}
|
|
|
|
func (as *AdminSession) CreatedAt() time.Time {
|
|
return as.createdAt
|
|
}
|
|
|
|
func (as *AdminSession) LastActiveAt() time.Time {
|
|
return as.lastActiveAt
|
|
}
|
|
|
|
func (as *AdminSession) ExpiresAt() time.Time {
|
|
return as.expiresAt
|
|
}
|
|
|
|
func (as *AdminSession) Metadata() map[string]interface{} {
|
|
result := make(map[string]interface{})
|
|
for k, v := range as.metadata {
|
|
result[k] = v
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (as *AdminSession) Events() []AdminSessionEvent {
|
|
result := make([]AdminSessionEvent, len(as.events))
|
|
copy(result, as.events)
|
|
return result
|
|
}
|
|
|
|
func (as *AdminSession) IsActive() bool {
|
|
if as.status != AdminSessionStatusActive {
|
|
return false
|
|
}
|
|
return time.Now().Before(as.expiresAt)
|
|
}
|
|
|
|
func (as *AdminSession) IsExpired() bool {
|
|
return time.Now().After(as.expiresAt) || as.status == AdminSessionStatusExpired
|
|
}
|
|
|
|
func (as *AdminSession) HasPermission(permission Permission) bool {
|
|
for _, perm := range as.permissions {
|
|
if perm == permission {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (as *AdminSession) HasRole(role AdminRole) bool {
|
|
for _, r := range as.roles {
|
|
if r == role {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (as *AdminSession) HasAnyRole(roles ...AdminRole) bool {
|
|
for _, role := range roles {
|
|
if as.HasRole(role) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (as *AdminSession) RefreshSession(newToken string, extensionDuration time.Duration) error {
|
|
if as.status != AdminSessionStatusActive {
|
|
return errors.ErrOperationNotAllowed("refresh_session", "session is not active")
|
|
}
|
|
if as.IsExpired() {
|
|
return errors.ErrSessionExpired(types.GameSessionID(as.id))
|
|
}
|
|
|
|
now := time.Now()
|
|
as.sessionToken = newToken
|
|
as.lastActiveAt = now
|
|
as.expiresAt = now.Add(extensionDuration)
|
|
|
|
as.addEvent(AdminSessionEventRefreshed, map[string]interface{}{
|
|
"new_expires_at": as.expiresAt,
|
|
"extended_by": extensionDuration.String(),
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (as *AdminSession) UpdateActivity(ipAddress, userAgent string) error {
|
|
if as.status != AdminSessionStatusActive {
|
|
return errors.ErrOperationNotAllowed("update_activity", "session is not active")
|
|
}
|
|
|
|
as.lastActiveAt = time.Now()
|
|
if ipAddress != "" {
|
|
as.ipAddress = ipAddress
|
|
}
|
|
if userAgent != "" {
|
|
as.userAgent = userAgent
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (as *AdminSession) RevokeSession(reason string) error {
|
|
if as.status != AdminSessionStatusActive {
|
|
return errors.ErrOperationNotAllowed("revoke_session", "session is not active")
|
|
}
|
|
|
|
as.status = AdminSessionStatusRevoked
|
|
as.addEvent(AdminSessionEventRevoked, map[string]interface{}{
|
|
"reason": reason,
|
|
"revoked_at": time.Now(),
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (as *AdminSession) SuspendSession(reason string, until time.Time) error {
|
|
if as.status != AdminSessionStatusActive {
|
|
return errors.ErrOperationNotAllowed("suspend_session", "session is not active")
|
|
}
|
|
|
|
as.status = AdminSessionStatusSuspended
|
|
as.metadata["suspended_until"] = until
|
|
as.metadata["suspension_reason"] = reason
|
|
|
|
as.addEvent(AdminSessionEventSuspended, map[string]interface{}{
|
|
"reason": reason,
|
|
"suspended_until": until,
|
|
"suspended_at": time.Now(),
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (as *AdminSession) AddRole(role AdminRole) error {
|
|
if as.HasRole(role) {
|
|
return errors.ErrValidationFailed("role", "role already assigned")
|
|
}
|
|
|
|
as.roles = append(as.roles, role)
|
|
as.permissions = calculatePermissionsFromRoles(as.roles)
|
|
|
|
as.addEvent(AdminSessionEventRoleAssigned, map[string]interface{}{
|
|
"role": role,
|
|
"assigned_at": time.Now(),
|
|
"new_permissions": as.permissions,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (as *AdminSession) RemoveRole(role AdminRole) error {
|
|
if !as.HasRole(role) {
|
|
return errors.ErrValidationFailed("role", "role not assigned")
|
|
}
|
|
|
|
newRoles := make([]AdminRole, 0, len(as.roles))
|
|
for _, r := range as.roles {
|
|
if r != role {
|
|
newRoles = append(newRoles, r)
|
|
}
|
|
}
|
|
|
|
as.roles = newRoles
|
|
as.permissions = calculatePermissionsFromRoles(as.roles)
|
|
|
|
as.addEvent(AdminSessionEventRoleRevoked, map[string]interface{}{
|
|
"role": role,
|
|
"revoked_at": time.Now(),
|
|
"new_permissions": as.permissions,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (as *AdminSession) SetRefreshToken(refreshToken string) error {
|
|
if refreshToken == "" {
|
|
return errors.ErrValidationFailed("refresh_token", "refresh token cannot be empty")
|
|
}
|
|
as.refreshToken = refreshToken
|
|
return nil
|
|
}
|
|
|
|
func (as *AdminSession) SetMetadata(key string, value interface{}) error {
|
|
if key == "" {
|
|
return errors.ErrInvalidMetadata
|
|
}
|
|
as.metadata[key] = value
|
|
return nil
|
|
}
|
|
|
|
func (as *AdminSession) RemoveMetadata(key string) {
|
|
delete(as.metadata, key)
|
|
}
|
|
|
|
func (as *AdminSession) GetTimeUntilExpiration() time.Duration {
|
|
return time.Until(as.expiresAt)
|
|
}
|
|
|
|
func (as *AdminSession) GetSessionDuration() time.Duration {
|
|
return time.Since(as.createdAt)
|
|
}
|
|
|
|
func (as *AdminSession) GetIdleDuration() time.Duration {
|
|
return time.Since(as.lastActiveAt)
|
|
}
|
|
|
|
func (as *AdminSession) addEvent(eventType AdminSessionEventType, data map[string]interface{}) {
|
|
event := AdminSessionEvent{
|
|
ID: fmt.Sprintf("%s_%d", eventType, time.Now().UnixNano()),
|
|
Type: eventType,
|
|
Data: data,
|
|
Timestamp: time.Now(),
|
|
}
|
|
as.events = append(as.events, event)
|
|
}
|
|
|
|
func (as *AdminSession) RequirePermission(permission Permission) error {
|
|
if !as.HasPermission(permission) {
|
|
return errors.ErrOperationNotAllowed("access_resource", fmt.Sprintf("missing permission: %s", permission))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (as *AdminSession) RequireRole(role AdminRole) error {
|
|
if !as.HasRole(role) {
|
|
return errors.ErrOperationNotAllowed("access_resource", fmt.Sprintf("missing role: %s", role))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (as *AdminSession) ValidateAccess() error {
|
|
if as.status != AdminSessionStatusActive {
|
|
return errors.ErrOperationNotAllowed("access", "session is not active")
|
|
}
|
|
if as.IsExpired() {
|
|
as.status = AdminSessionStatusExpired
|
|
as.addEvent(AdminSessionEventExpired, map[string]interface{}{
|
|
"expired_at": time.Now(),
|
|
})
|
|
return errors.ErrSessionExpired(types.GameSessionID(as.id))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (as *AdminSession) String() string {
|
|
return fmt.Sprintf("AdminSession{ID: %s, AdminID: %s, Status: %s, ExpiresAt: %v}",
|
|
as.id, as.adminID, as.status, as.expiresAt)
|
|
}
|
|
|
|
type AdminSessionSnapshot struct {
|
|
ID types.AdminSessionID `json:"id"`
|
|
AdminID types.UserID `json:"admin_id"`
|
|
AdminName string `json:"admin_name"`
|
|
AdminEmail string `json:"admin_email"`
|
|
Roles []AdminRole `json:"roles"`
|
|
Permissions []Permission `json:"permissions"`
|
|
SessionToken string `json:"session_token"`
|
|
RefreshToken string `json:"refresh_token"`
|
|
IPAddress string `json:"ip_address"`
|
|
UserAgent string `json:"user_agent"`
|
|
Status AdminSessionStatus `json:"status"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
LastActiveAt time.Time `json:"last_active_at"`
|
|
ExpiresAt time.Time `json:"expires_at"`
|
|
Metadata map[string]interface{} `json:"metadata"`
|
|
Events []AdminSessionEvent `json:"events"`
|
|
}
|
|
|
|
func (as *AdminSession) ToSnapshot() AdminSessionSnapshot {
|
|
return AdminSessionSnapshot{
|
|
ID: as.id,
|
|
AdminID: as.adminID,
|
|
AdminName: as.adminName,
|
|
AdminEmail: as.adminEmail,
|
|
Roles: as.Roles(),
|
|
Permissions: as.Permissions(),
|
|
SessionToken: as.sessionToken,
|
|
RefreshToken: as.refreshToken,
|
|
IPAddress: as.ipAddress,
|
|
UserAgent: as.userAgent,
|
|
Status: as.status,
|
|
CreatedAt: as.createdAt,
|
|
LastActiveAt: as.lastActiveAt,
|
|
ExpiresAt: as.expiresAt,
|
|
Metadata: as.Metadata(),
|
|
Events: as.Events(),
|
|
}
|
|
}
|
|
|
|
func (as *AdminSession) FromSnapshot(snapshot AdminSessionSnapshot) error {
|
|
if snapshot.ID == "" {
|
|
return errors.ErrInvalidSessionID
|
|
}
|
|
if snapshot.AdminID == "" {
|
|
return errors.ErrInvalidPlayerID
|
|
}
|
|
|
|
as.id = snapshot.ID
|
|
as.adminID = snapshot.AdminID
|
|
as.adminName = snapshot.AdminName
|
|
as.adminEmail = snapshot.AdminEmail
|
|
as.roles = make([]AdminRole, len(snapshot.Roles))
|
|
copy(as.roles, snapshot.Roles)
|
|
as.permissions = make([]Permission, len(snapshot.Permissions))
|
|
copy(as.permissions, snapshot.Permissions)
|
|
as.sessionToken = snapshot.SessionToken
|
|
as.refreshToken = snapshot.RefreshToken
|
|
as.ipAddress = snapshot.IPAddress
|
|
as.userAgent = snapshot.UserAgent
|
|
as.status = snapshot.Status
|
|
as.createdAt = snapshot.CreatedAt
|
|
as.lastActiveAt = snapshot.LastActiveAt
|
|
as.expiresAt = snapshot.ExpiresAt
|
|
|
|
as.metadata = make(map[string]interface{})
|
|
for k, v := range snapshot.Metadata {
|
|
as.metadata[k] = v
|
|
}
|
|
|
|
as.events = make([]AdminSessionEvent, len(snapshot.Events))
|
|
copy(as.events, snapshot.Events)
|
|
|
|
return nil
|
|
} |