feat(backend): bootstrap Go module, config, and data plane
Introduce configuration validation, Postgres migrations, Redis/storage adapters, and the seed sample video.
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Role string
|
||||
|
||||
const (
|
||||
RoleStudent Role = "student"
|
||||
RoleMentor Role = "mentor"
|
||||
RoleAdmin Role = "admin"
|
||||
)
|
||||
|
||||
func (r Role) String() string { return string(r) }
|
||||
|
||||
// IsStaff reports whether the role is exempt from student heart/lockout limits.
|
||||
func (r Role) IsStaff() bool {
|
||||
return r == RoleAdmin || r == RoleMentor
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID int64 `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
PasswordHash string `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Role Role `json:"role"`
|
||||
XP int `json:"xp"`
|
||||
Hearts int `json:"hearts"`
|
||||
HeartsUpdatedAt time.Time `json:"heartsUpdatedAt"`
|
||||
StreakCurrent int `json:"streakCurrent"`
|
||||
StreakLongest int `json:"streakLongest"`
|
||||
LastActivityDate *time.Time `json:"lastActivityDate,omitempty"`
|
||||
IsLocked bool `json:"isLocked"`
|
||||
LockedAt *time.Time `json:"lockedAt,omitempty"`
|
||||
UnlockedBy *int64 `json:"unlockedBy,omitempty"`
|
||||
IsActive bool `json:"isActive"`
|
||||
AvatarVariant string `json:"avatarVariant"`
|
||||
AvatarPalette string `json:"avatarPalette"`
|
||||
AvatarPhoto string `json:"avatarPhoto,omitempty"`
|
||||
Phone string `json:"phone"`
|
||||
TelegramID *int64 `json:"-"`
|
||||
TelegramUsername string `json:"-"`
|
||||
SecurityQuestion string `json:"securityQuestion,omitempty"`
|
||||
SecurityAnswerHash string `json:"-"`
|
||||
HasSecurityAnswer bool `json:"hasSecurityAnswer"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type Certificate struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"userId"`
|
||||
PublicID string `json:"publicId"`
|
||||
FullName string `json:"fullName"`
|
||||
IssuedAt time.Time `json:"issuedAt"`
|
||||
RevokedAt *time.Time `json:"revokedAt,omitempty"`
|
||||
}
|
||||
|
||||
type PhysicalOrderStatus string
|
||||
|
||||
const (
|
||||
PhysicalRequested PhysicalOrderStatus = "requested"
|
||||
PhysicalPaid PhysicalOrderStatus = "paid"
|
||||
PhysicalShipped PhysicalOrderStatus = "shipped"
|
||||
PhysicalCancelled PhysicalOrderStatus = "cancelled"
|
||||
)
|
||||
|
||||
type CertificatePhysicalOrder struct {
|
||||
ID int64 `json:"id"`
|
||||
CertificateID int64 `json:"certificateId"`
|
||||
UserID int64 `json:"userId"`
|
||||
Status PhysicalOrderStatus `json:"status"`
|
||||
Note string `json:"note"`
|
||||
RecipientName string `json:"recipientName"`
|
||||
Phone string `json:"phone"`
|
||||
Address string `json:"address"`
|
||||
City string `json:"city"`
|
||||
PostalCode string `json:"postalCode"`
|
||||
TrackingCode string `json:"trackingCode"`
|
||||
WindowEndsAt time.Time `json:"windowEndsAt"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
UserName string `json:"userName,omitempty"`
|
||||
PublicID string `json:"publicId,omitempty"`
|
||||
}
|
||||
|
||||
type UnlockRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"userId"`
|
||||
Note string `json:"note"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
ResolvedAt *time.Time `json:"resolvedAt,omitempty"`
|
||||
UserName string `json:"userName,omitempty"`
|
||||
}
|
||||
|
||||
type NotificationPrefs struct {
|
||||
UserID int64 `json:"userId"`
|
||||
Progress bool `json:"progress"`
|
||||
Gamification bool `json:"gamification"`
|
||||
Mentor bool `json:"mentor"`
|
||||
Event bool `json:"event"`
|
||||
}
|
||||
|
||||
type Chapter struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Icon string `json:"icon"`
|
||||
SortOrder int `json:"sortOrder"`
|
||||
}
|
||||
|
||||
type Lesson struct {
|
||||
ID int64 `json:"id"`
|
||||
ChapterID int64 `json:"chapterId"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
VideoKey string `json:"videoKey"`
|
||||
DurationSeconds int `json:"durationSeconds"`
|
||||
CompletionThresholdPct int `json:"completionThresholdPct"`
|
||||
RequiresLessonID *int64 `json:"requiresLessonId,omitempty"`
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
SortOrder int `json:"sortOrder"`
|
||||
XPReward int `json:"xpReward"`
|
||||
IsActive bool `json:"isActive"`
|
||||
}
|
||||
|
||||
type MCQQuestion struct {
|
||||
ID int64 `json:"id"`
|
||||
LessonID int64 `json:"lessonId"`
|
||||
Position int `json:"position"`
|
||||
Question string `json:"question"`
|
||||
Options []string `json:"options"`
|
||||
AnswerIndex int `json:"answerIndex"`
|
||||
Explanation string `json:"explanation"`
|
||||
}
|
||||
|
||||
type LessonProgress struct {
|
||||
UserID int64 `json:"userId"`
|
||||
LessonID int64 `json:"lessonId"`
|
||||
WatchedSeconds float64 `json:"watchedSeconds"`
|
||||
WatchedPct float64 `json:"watchedPct"`
|
||||
LastPosition float64 `json:"lastPosition"`
|
||||
QuizUnlocked bool `json:"quizUnlocked"`
|
||||
PassedQuiz bool `json:"passedQuiz"`
|
||||
QuizCompletedAt *time.Time `json:"quizCompletedAt,omitempty"`
|
||||
}
|
||||
|
||||
type WatchSession struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"userId"`
|
||||
LessonID int64 `json:"lessonId"`
|
||||
StartedAt time.Time `json:"startedAt"`
|
||||
LastHeartbeatAt time.Time `json:"lastHeartbeatAt"`
|
||||
LastPosition float64 `json:"lastPosition"`
|
||||
}
|
||||
|
||||
type QuizQuestionView struct {
|
||||
ID int64 `json:"id"`
|
||||
Position int `json:"position"`
|
||||
Question string `json:"question"`
|
||||
Options []string `json:"options"`
|
||||
}
|
||||
|
||||
type QuizAttempt struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"userId"`
|
||||
LessonID int64 `json:"lessonId"`
|
||||
Answers []int `json:"-"`
|
||||
CorrectCount int `json:"correctCount"`
|
||||
Total int `json:"total"`
|
||||
ScorePct float64 `json:"scorePct"`
|
||||
Status string `json:"status"`
|
||||
HeartsLost int `json:"heartsLost"`
|
||||
PassedAt *time.Time `json:"passedAt,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
EventType string `json:"eventType"`
|
||||
ExternalURL string `json:"externalUrl"`
|
||||
StartsAt time.Time `json:"startsAt"`
|
||||
EndsAt time.Time `json:"endsAt"`
|
||||
IsActive bool `json:"isActive"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
CreatedBy *int64 `json:"-"`
|
||||
Rsvped bool `json:"rsvped,omitempty"`
|
||||
}
|
||||
|
||||
// Challenge is a timed XP goal (typically monthly) for students.
|
||||
type Challenge struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
TargetXP int `json:"targetXp"`
|
||||
StartsAt time.Time `json:"startsAt"`
|
||||
EndsAt time.Time `json:"endsAt"`
|
||||
IsActive bool `json:"isActive"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
CreatedBy *int64 `json:"-"`
|
||||
// Progress fields for the requesting student (optional).
|
||||
MyXP int `json:"myXp,omitempty"`
|
||||
Completed bool `json:"completed,omitempty"`
|
||||
}
|
||||
|
||||
type ChatMessage struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"userId"`
|
||||
MentorID int64 `json:"mentorId"`
|
||||
SenderRole Role `json:"senderRole"`
|
||||
Body string `json:"body"`
|
||||
ReadAt *time.Time `json:"readAt,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
ReplyTo *int64 `json:"replyTo,omitempty"`
|
||||
PinnedAt *time.Time `json:"pinnedAt,omitempty"`
|
||||
Pinned bool `json:"pinned"`
|
||||
Attachment *Attachment `json:"attachment,omitempty"`
|
||||
Reactions []ChatReaction `json:"reactions,omitempty"`
|
||||
}
|
||||
|
||||
type ChatReaction struct {
|
||||
Emoji string `json:"emoji"`
|
||||
Count int `json:"count"`
|
||||
Mine bool `json:"mine"`
|
||||
}
|
||||
|
||||
type Attachment struct {
|
||||
Type string `json:"type"` // image | video | audio | file
|
||||
URL string `json:"url"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"userId"`
|
||||
Category string `json:"category"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Route string `json:"route"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
ReadAt *time.Time `json:"readAt,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type InviteStatus string
|
||||
|
||||
const (
|
||||
InviteActive InviteStatus = "active"
|
||||
InvitePaused InviteStatus = "paused"
|
||||
InviteRevoked InviteStatus = "revoked"
|
||||
InviteExhausted InviteStatus = "exhausted"
|
||||
)
|
||||
|
||||
// RegistrationInvite is a capacity-limited membership registration link.
|
||||
type RegistrationInvite struct {
|
||||
ID int64 `json:"id"`
|
||||
Label string `json:"label"`
|
||||
MaxUses int `json:"maxUses"`
|
||||
UsedCount int `json:"usedCount"`
|
||||
Remaining int `json:"remaining"`
|
||||
Status InviteStatus `json:"status"`
|
||||
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
|
||||
CreatedBy *int64 `json:"createdBy,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
// Token is the raw invite token; only populated when decrypting for admin URL display.
|
||||
Token string `json:"token,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// ChatExportToken is a time-limited, single-use download link for the project chat archive.
|
||||
type ChatExportToken struct {
|
||||
ID int64 `json:"id"`
|
||||
Label string `json:"label"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
RevokedAt *time.Time `json:"revokedAt,omitempty"`
|
||||
UsedAt *time.Time `json:"usedAt,omitempty"`
|
||||
CreatedBy *int64 `json:"createdBy,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
|
||||
Active bool `json:"active"`
|
||||
// Token/URL are populated for admin display after decrypt.
|
||||
Token string `json:"token,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
type WhitelistStatus string
|
||||
|
||||
const (
|
||||
WhitelistAvailable WhitelistStatus = "available"
|
||||
WhitelistConsumed WhitelistStatus = "consumed"
|
||||
)
|
||||
|
||||
// RegistrationPhoneWhitelist is a one-shot phone allowed for public registration.
|
||||
type RegistrationPhoneWhitelist struct {
|
||||
ID int64 `json:"id"`
|
||||
Phone string `json:"phone"`
|
||||
Status WhitelistStatus `json:"status"`
|
||||
ConsumedAt *time.Time `json:"consumedAt,omitempty"`
|
||||
ConsumedUserID *int64 `json:"consumedUserId,omitempty"`
|
||||
CreatedBy *int64 `json:"createdBy,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
// RegistrationPhoneBlacklist blocks a phone from all registration paths.
|
||||
type RegistrationPhoneBlacklist struct {
|
||||
ID int64 `json:"id"`
|
||||
Phone string `json:"phone"`
|
||||
CreatedBy *int64 `json:"createdBy,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
// RegistrationBlacklistAttempt records a failed registration try with a blacklisted phone.
|
||||
type RegistrationBlacklistAttempt struct {
|
||||
ID int64 `json:"id"`
|
||||
Phone string `json:"phone"`
|
||||
Path string `json:"path"` // public | invite
|
||||
InviteID *int64 `json:"inviteId,omitempty"`
|
||||
IP string `json:"ip"`
|
||||
UserAgent string `json:"userAgent,omitempty"`
|
||||
AttemptedUsername string `json:"attemptedUsername,omitempty"`
|
||||
AttemptedEmail string `json:"attemptedEmail,omitempty"`
|
||||
AttemptedAt time.Time `json:"attemptedAt"`
|
||||
}
|
||||
Reference in New Issue
Block a user