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

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
}