2026-10-07 19:40:26 +03:30
package api
import (
"context"
"fmt"
"net/http"
"path/filepath"
"strings"
"time"
"unicode/utf8"
"pargar/backend/internal/models"
"pargar/backend/internal/observability"
)
func ( s * Server ) handleAdminStats ( w http . ResponseWriter , r * http . Request ) {
stats , err := s . store . Stats ( r . Context ())
if err != nil {
writeErr ( w , http . StatusInternalServerError , "بارگذاری آمار ممکن نشد" )
return
}
writeJSON ( w , http . StatusOK , map [ string ] any { "stats" : stats })
}
2026-10-09 19:05:59 +03:30
func ( s * Server ) handleAdminGetUser ( w http . ResponseWriter , r * http . Request ) {
id := routeID ( r , "id" )
if id <= 0 {
writeErr ( w , http . StatusNotFound , "کاربر پیدا نشد" )
return
}
u , err := s . store . GetUserByID ( r . Context (), id )
if err != nil {
writeErr ( w , http . StatusNotFound , "کاربر پیدا نشد" )
return
}
2026-10-10 16:42:35 +03:30
u = s . settleAccount ( r . Context (), u )
2026-10-09 19:05:59 +03:30
writeJSON ( w , http . StatusOK , map [ string ] any { "user" : u })
}
2026-10-07 19:40:26 +03:30
func ( s * Server ) handleAdminListUsers ( w http . ResponseWriter , r * http . Request ) {
query := r . URL . Query (). Get ( "q" )
role := r . URL . Query (). Get ( "role" )
p := parsePageParams ( r )
users , total , err := s . store . ListUsers ( r . Context (), query , role , p . PageSize , p . Offset )
if err != nil {
writeErr ( w , http . StatusInternalServerError , "بارگذاری کاربران ممکن نشد" )
return
}
writePage ( w , users , total , p )
}
func ( s * Server ) handleAdminCreateUser ( w http . ResponseWriter , r * http . Request ) {
var req struct {
Name string `json:"name"`
Email string `json:"email"`
Username string `json:"username"`
Password string `json:"password"`
Role string `json:"role"`
Phone string `json:"phone"`
SecurityQuestion string `json:"securityQuestion"`
SecurityAnswer string `json:"securityAnswer"`
}
if err := bodyJSON ( r , & req ); err != nil {
writeErr ( w , http . StatusBadRequest , "دادهٔ ارسالی نامعتبر است" )
return
}
req . Email = strings . ToLower ( strings . TrimSpace ( req . Email ))
req . Username = strings . ToLower ( strings . TrimSpace ( req . Username ))
name := strings . TrimSpace ( req . Name )
phone := normalizePhone ( req . Phone )
secQ := strings . TrimSpace ( req . SecurityQuestion )
secA := strings . TrimSpace ( req . SecurityAnswer )
if msg := validatePersonName ( name ); msg != "" {
writeErr ( w , http . StatusBadRequest , msg )
return
}
if ! validEmail ( req . Email ) {
writeErr ( w , http . StatusBadRequest , msgEmailInvalid )
return
}
if ! validUsername ( req . Username ) {
writeErr ( w , http . StatusBadRequest , msgUsernameInvalid )
return
}
if ! validPhone ( phone ) {
writeErr ( w , http . StatusBadRequest , msgPhoneInvalid )
return
}
if msg := validatePasswordOnly ( req . Password ); msg != "" {
writeErr ( w , http . StatusBadRequest , msg )
return
}
if msg := validateSecurityQA ( secQ , secA ); msg != "" {
writeErr ( w , http . StatusBadRequest , msg )
return
}
var role models . Role
switch req . Role {
case "" , "student" :
role = models . RoleStudent
case "mentor" :
role = models . RoleMentor
case "admin" :
role = models . RoleAdmin
default :
writeErr ( w , http . StatusBadRequest , "نقش نامعتبر است" )
return
}
if actor := currentUser ( r ); actor . Role != models . RoleAdmin && role != models . RoleStudent {
writeErr ( w , http . StatusForbidden , "منتور فقط میتواند حساب هنرجو بسازد" )
return
}
if _ , err := s . store . GetUserByEmail ( r . Context (), req . Email ); err == nil {
writeErr ( w , http . StatusConflict , "این ایمیل قبلاً ثبت شده است" )
return
}
if _ , err := s . store . GetUserByUsername ( r . Context (), req . Username ); err == nil {
writeErr ( w , http . StatusConflict , "این شناسهٔ کاربری قبلاً گرفته شده است" )
return
}
if phone != "" {
if _ , err := s . store . GetUserByPhone ( r . Context (), phone ); err == nil {
writeErr ( w , http . StatusConflict , "این شماره تلفن قبلاً ثبت شده است" )
return
}
}
hash , err := s . auth . HashPassword ( req . Password )
if err != nil {
writeErr ( w , http . StatusInternalServerError , "پردازش گذرواژه ممکن نشد" )
return
}
answerHash , err := s . auth . HashPassword ( strings . ToLower ( secA ))
if err != nil {
writeErr ( w , http . StatusInternalServerError , "پردازش جواب امنیتی ممکن نشد" )
return
}
user , err := s . store . CreateUser ( r . Context (), name , req . Email , req . Username , hash , phone , secQ , answerHash , role )
if err != nil {
writeErr ( w , http . StatusInternalServerError , "ساخت کاربر ممکن نشد" )
return
}
writeJSON ( w , http . StatusCreated , map [ string ] any { "user" : user })
}
func ( s * Server ) handleAdminLockUser ( w http . ResponseWriter , r * http . Request ) {
actor := currentUser ( r )
id := routeID ( r , "id" )
if id == actor . ID {
writeErr ( w , http . StatusBadRequest , "نمیتوانید حساب خودتان را قفل کنید" )
return
}
if err := s . canActOn ( actor , id ); err != nil {
writeErr ( w , http . StatusForbidden , "دسترسی کافی ندارید" )
return
}
target , err := s . store . GetUserByID ( r . Context (), id )
if err != nil {
writeErr ( w , http . StatusNotFound , "کاربر پیدا نشد" )
return
}
if target . Role . IsStaff () {
writeErr ( w , http . StatusBadRequest , "حساب ادمین و منتور قابل قفل نیست" )
return
}
u , err := s . store . LockUser ( r . Context (), id )
if err != nil {
writeErr ( w , http . StatusNotFound , "کاربر پیدا نشد" )
return
}
observability . Activity ( "account.locked" , "user_id" , id , "actor_id" , actor . ID , "reason" , "staff" )
go func () {
_ = s . notify . Notify ( context . Background (), id , "progress" , "account_locked" ,
"حساب توسط منتور محدود شد" , "حساب شما توسط " + actor . Name + " محدود شد." , "/unwrap?tab=chats" , nil )
}()
writeJSON ( w , http . StatusOK , map [ string ] any { "user" : u })
}
func ( s * Server ) handleAdminUnlockUser ( w http . ResponseWriter , r * http . Request ) {
actor := currentUser ( r )
id := routeID ( r , "id" )
if err := s . canActOn ( actor , id ); err != nil {
writeErr ( w , http . StatusForbidden , "دسترسی کافی ندارید" )
return
}
u , err := s . store . UnlockUser ( r . Context (), id , actor . ID )
if err != nil {
writeErr ( w , http . StatusNotFound , "کاربر پیدا نشد" )
return
}
_ = s . store . ResolveUnlockRequests ( r . Context (), id )
observability . Activity ( "account.unlocked" , "user_id" , id , "actor_id" , actor . ID )
go func () {
_ = s . notify . Notify ( context . Background (), id , "progress" , "account_unlocked" ,
"محدودیت حساب برداشته شد" , "محدودیت حساب شما توسط " + actor . Name + " برداشته شد. ۳ جان شما بازگردانده شد." , "/cap" , nil )
}()
writeJSON ( w , http . StatusOK , map [ string ] any { "user" : u })
}
func ( s * Server ) handleAdminSetRole ( w http . ResponseWriter , r * http . Request ) {
actor := currentUser ( r )
id := routeID ( r , "id" )
if id == actor . ID {
writeErr ( w , http . StatusBadRequest , "نمیتوانید نقش حساب خودتان را تغییر دهید" )
return
}
if err := s . canActOn ( actor , id ); err != nil {
writeErr ( w , http . StatusForbidden , "دسترسی کافی ندارید" )
return
}
var req struct {
Role string `json:"role"`
}
if err := bodyJSON ( r , & req ); err != nil {
writeErr ( w , http . StatusBadRequest , "دادهٔ ارسالی نامعتبر است" )
return
}
var role models . Role
switch req . Role {
case "student" , "mentor" , "admin" :
role = models . Role ( req . Role )
default :
writeErr ( w , http . StatusBadRequest , "نقش نامعتبر است" )
return
}
if actor . Role != models . RoleAdmin && role != models . RoleStudent {
writeErr ( w , http . StatusForbidden , "منتور فقط میتواند نقش هنرجو را تنظیم کند" )
return
}
if _ , err := s . store . Pool (). Exec ( r . Context (), `UPDATE users SET role=$1 WHERE id=$2` , role . String (), id ); err != nil {
writeErr ( w , http . StatusInternalServerError , "تغییر نقش ممکن نشد" )
return
}
writeJSON ( w , http . StatusOK , map [ string ] any { "ok" : true })
}
func ( s * Server ) handleAdminUpdateUser ( w http . ResponseWriter , r * http . Request ) {
actor := currentUser ( r )
id := routeID ( r , "id" )
if err := s . canActOn ( actor , id ); err != nil {
writeErr ( w , http . StatusForbidden , "دسترسی کافی ندارید" )
return
}
target , err := s . store . GetUserByID ( r . Context (), id )
if err != nil {
writeErr ( w , http . StatusNotFound , "کاربر پیدا نشد" )
return
}
var req struct {
Name * string `json:"name"`
Email * string `json:"email"`
Username * string `json:"username"`
Password * string `json:"password"`
Role * string `json:"role"`
Hearts * int `json:"hearts"`
XP * int `json:"xp"`
Phone * string `json:"phone"`
SecurityQuestion * string `json:"securityQuestion"`
SecurityAnswer * string `json:"securityAnswer"`
}
if err := bodyJSON ( r , & req ); err != nil {
writeErr ( w , http . StatusBadRequest , "دادهٔ ارسالی نامعتبر است" )
return
}
name := target . Name
if req . Name != nil {
name = strings . TrimSpace ( * req . Name )
if msg := validatePersonName ( name ); msg != "" {
writeErr ( w , http . StatusBadRequest , msg )
return
}
}
email := target . Email
if req . Email != nil && strings . TrimSpace ( * req . Email ) != "" {
email = strings . ToLower ( strings . TrimSpace ( * req . Email ))
if ! validEmail ( email ) {
writeErr ( w , http . StatusBadRequest , msgEmailInvalid )
return
}
if email != target . Email {
if _ , err := s . store . GetUserByEmail ( r . Context (), email ); err == nil {
writeErr ( w , http . StatusConflict , "این ایمیل قبلاً ثبت شده است" )
return
}
}
}
username := target . Username
if req . Username != nil && strings . TrimSpace ( * req . Username ) != "" {
username = strings . ToLower ( strings . TrimSpace ( * req . Username ))
if ! validUsername ( username ) {
writeErr ( w , http . StatusBadRequest , msgUsernameInvalid )
return
}
if username != target . Username {
if _ , err := s . store . GetUserByUsername ( r . Context (), username ); err == nil {
writeErr ( w , http . StatusConflict , "این شناسهٔ کاربری قبلاً گرفته شده است" )
return
}
}
}
phone := target . Phone
if req . Phone != nil {
phone = normalizePhone ( * req . Phone )
if ! validPhone ( phone ) {
writeErr ( w , http . StatusBadRequest , msgPhoneInvalid )
return
}
if phone != "" && phone != target . Phone {
if _ , err := s . store . GetUserByPhone ( r . Context (), phone ); err == nil {
writeErr ( w , http . StatusConflict , "این شماره تلفن قبلاً ثبت شده است" )
return
}
}
}
hash := ""
if req . Password != nil && * req . Password != "" {
writeErr ( w , http . StatusBadRequest , "برای تغییر رمز از مسیر ریست با سوال امنیتی استفاده کنید" )
return
}
secQ := ""
secAHash := ""
if req . SecurityQuestion != nil {
secQ = strings . TrimSpace ( * req . SecurityQuestion )
if secQ != "" {
qn := utf8 . RuneCountInString ( secQ )
if qn < minSecQLen {
writeErr ( w , http . StatusBadRequest , msgSecQShort )
return
}
if qn > maxSecQLen {
writeErr ( w , http . StatusBadRequest , msgSecQLong )
return
}
}
}
if req . SecurityAnswer != nil && strings . TrimSpace ( * req . SecurityAnswer ) != "" {
if secQ == "" && target . SecurityQuestion == "" {
writeErr ( w , http . StatusBadRequest , "برای تنظیم جواب، سوال امنیتی هم لازم است" )
return
}
if secQ == "" {
secQ = target . SecurityQuestion
}
ans := strings . TrimSpace ( * req . SecurityAnswer )
an := utf8 . RuneCountInString ( ans )
if an < minSecALen {
writeErr ( w , http . StatusBadRequest , msgSecAShort )
return
}
if an > maxSecALen {
writeErr ( w , http . StatusBadRequest , msgSecALong )
return
}
var err error
secAHash , err = s . auth . HashPassword ( strings . ToLower ( ans ))
if err != nil {
writeErr ( w , http . StatusInternalServerError , "پردازش جواب امنیتی ممکن نشد" )
return
}
}
role := target . Role
if req . Role != nil {
switch * req . Role {
case "student" , "mentor" , "admin" :
newRole := models . Role ( * req . Role )
if id == actor . ID && newRole != target . Role {
writeErr ( w , http . StatusBadRequest , "نمیتوانید نقش حساب خودتان را تغییر دهید" )
return
}
if actor . Role != models . RoleAdmin && newRole != models . RoleStudent {
writeErr ( w , http . StatusForbidden , "منتور فقط میتواند نقش هنرجو را تنظیم کند" )
return
}
role = newRole
default :
writeErr ( w , http . StatusBadRequest , "نقش نامعتبر است" )
return
}
}
hearts , xp := target . Hearts , target . XP
if id == actor . ID {
// Self-service via admin panel must not mint XP/hearts.
req . Hearts = nil
req . XP = nil
}
if req . Hearts != nil {
hearts = * req . Hearts
}
if req . XP != nil {
xp = * req . XP
}
user , err := s . store . UpdateUser ( r . Context (), id , name , email , username , hash , phone , secQ , secAHash , role , hearts , xp )
if err != nil {
writeErr ( w , http . StatusInternalServerError , "بهروزرسانی کاربر ممکن نشد" )
return
}
writeJSON ( w , http . StatusOK , map [ string ] any { "user" : user })
}
func ( s * Server ) handleAdminResetPassword ( w http . ResponseWriter , r * http . Request ) {
actor := currentUser ( r )
id := routeID ( r , "id" )
if err := s . canActOn ( actor , id ); err != nil {
writeErr ( w , http . StatusForbidden , "دسترسی کافی ندارید" )
return
}
2026-10-09 19:05:59 +03:30
if _ , err := s . store . GetUserByID ( r . Context (), id ); err != nil {
2026-10-07 19:40:26 +03:30
writeErr ( w , http . StatusNotFound , "کاربر پیدا نشد" )
return
}
var req struct {
2026-10-09 19:05:59 +03:30
NewPassword string `json:"newPassword"`
2026-10-07 19:40:26 +03:30
}
if err := bodyJSON ( r , & req ); err != nil {
writeErr ( w , http . StatusBadRequest , "دادهٔ ارسالی نامعتبر است" )
return
}
if msg := validatePasswordOnly ( req . NewPassword ); msg != "" {
writeErr ( w , http . StatusBadRequest , msg )
return
}
hash , err := s . auth . HashPassword ( req . NewPassword )
if err != nil {
writeErr ( w , http . StatusInternalServerError , "پردازش گذرواژه ممکن نشد" )
return
}
if err := s . store . SetUserPassword ( r . Context (), id , hash ); err != nil {
writeErr ( w , http . StatusInternalServerError , "ریست گذرواژه ممکن نشد" )
return
}
observability . Activity ( "account.password_reset" , "user_id" , id , "actor_id" , actor . ID )
writeJSON ( w , http . StatusOK , map [ string ] any { "ok" : true })
}
func ( s * Server ) handleAdminDeleteUser ( w http . ResponseWriter , r * http . Request ) {
actor := currentUser ( r )
id := routeID ( r , "id" )
if id == actor . ID {
writeErr ( w , http . StatusBadRequest , "نمیتوانید حساب خودتان را حذف کنید" )
return
}
if err := s . canActOn ( actor , id ); err != nil {
writeErr ( w , http . StatusForbidden , "دسترسی کافی ندارید" )
return
}
2026-10-10 16:42:35 +03:30
target , err := s . store . GetUserByID ( r . Context (), id )
if err != nil {
writeErr ( w , http . StatusNotFound , "کاربر پیدا نشد" )
return
}
target = s . settleAccount ( r . Context (), target )
if ! target . IsFrozen && ! target . IsClosed {
writeErr ( w , http . StatusConflict , msgDeleteNotFrozen )
return
}
2026-10-07 19:40:26 +03:30
if err := s . store . DeleteUser ( r . Context (), id ); err != nil {
writeErr ( w , http . StatusInternalServerError , "حذف کاربر ممکن نشد" )
return
}
writeJSON ( w , http . StatusOK , map [ string ] any { "ok" : true })
}
// canActOn verifies a non-admin actor may only mutate student accounts.
func ( s * Server ) canActOn ( actor * models . User , targetID int64 ) error {
if actor . Role == models . RoleAdmin {
return nil
}
target , err := s . store . GetUserByID ( context . Background (), targetID )
if err != nil {
return err
}
if target . Role != models . RoleStudent {
return fmt . Errorf ( "فقط میتوانید روی حساب هنرجویان عمل کنید" )
}
return nil
}
// ---- chapters ----
func ( s * Server ) handleAdminListChapters ( w http . ResponseWriter , r * http . Request ) {
chapters , err := s . store . ListChapters ( r . Context ())
if err != nil {
writeErr ( w , http . StatusInternalServerError , "بارگذاری فصلها ممکن نشد" )
return
}
writeJSON ( w , http . StatusOK , map [ string ] any { "chapters" : chapters })
}
func ( s * Server ) handleAdminCreateChapter ( w http . ResponseWriter , r * http . Request ) {
var c models . Chapter
if err := bodyJSON ( r , & c ); err != nil {
writeErr ( w , http . StatusBadRequest , "دادهٔ ارسالی نامعتبر است" )
return
}
id , err := s . store . CreateChapter ( r . Context (), & c )
if err != nil {
writeErr ( w , http . StatusInternalServerError , "ساخت فصل ممکن نشد" )
return
}
writeJSON ( w , http . StatusCreated , map [ string ] any { "chapter" : c , "id" : id })
}
func ( s * Server ) handleAdminUpdateChapter ( w http . ResponseWriter , r * http . Request ) {
id := routeID ( r , "id" )
var c models . Chapter
if err := bodyJSON ( r , & c ); err != nil {
writeErr ( w , http . StatusBadRequest , "دادهٔ ارسالی نامعتبر است" )
return
}
if err := s . store . UpdateChapter ( r . Context (), id , & c ); err != nil {
writeErr ( w , http . StatusInternalServerError , "بهروزرسانی فصل ممکن نشد" )
return
}
writeJSON ( w , http . StatusOK , map [ string ] any { "ok" : true })
}
func ( s * Server ) handleAdminDeleteChapter ( w http . ResponseWriter , r * http . Request ) {
id := routeID ( r , "id" )
if err := s . store . DeleteChapter ( r . Context (), id ); err != nil {
writeErr ( w , http . StatusInternalServerError , "حذف فصل ممکن نشد" )
return
}
writeJSON ( w , http . StatusOK , map [ string ] any { "ok" : true })
}
// ---- lessons ----
func ( s * Server ) handleAdminListLessons ( w http . ResponseWriter , r * http . Request ) {
lessons , err := s . store . ListLessons ( r . Context (), true )
if err != nil {
writeErr ( w , http . StatusInternalServerError , "بارگذاری درسها ممکن نشد" )
return
}
writeJSON ( w , http . StatusOK , map [ string ] any { "lessons" : lessons })
}
func ( s * Server ) handleAdminCreateLesson ( w http . ResponseWriter , r * http . Request ) {
var l models . Lesson
if err := bodyJSON ( r , & l ); err != nil {
writeErr ( w , http . StatusBadRequest , "دادهٔ ارسالی نامعتبر است" )
return
}
if l . DurationSeconds <= 0 {
writeErr ( w , http . StatusBadRequest , "مدت درس الزامی است" )
return
}
id , err := s . store . CreateLesson ( r . Context (), & l )
if err != nil {
writeErr ( w , http . StatusInternalServerError , "ساخت درس ممکن نشد" )
return
}
writeJSON ( w , http . StatusCreated , map [ string ] any { "lesson" : l , "id" : id })
}
func ( s * Server ) handleAdminUpdateLesson ( w http . ResponseWriter , r * http . Request ) {
id := routeID ( r , "id" )
var l models . Lesson
if err := bodyJSON ( r , & l ); err != nil {
writeErr ( w , http . StatusBadRequest , "دادهٔ ارسالی نامعتبر است" )
return
}
if err := s . store . UpdateLesson ( r . Context (), id , & l ); err != nil {
writeErr ( w , http . StatusInternalServerError , "بهروزرسانی درس ممکن نشد" )
return
}
writeJSON ( w , http . StatusOK , map [ string ] any { "ok" : true })
}
func ( s * Server ) handleAdminDeleteLesson ( w http . ResponseWriter , r * http . Request ) {
id := routeID ( r , "id" )
if err := s . store . DeleteLesson ( r . Context (), id ); err != nil {
writeErr ( w , http . StatusInternalServerError , "حذف درس ممکن نشد" )
return
}
writeJSON ( w , http . StatusOK , map [ string ] any { "ok" : true })
}
2026-10-18 10:14:22 +03:30
func ( s * Server ) handleAdminPublishLesson ( w http . ResponseWriter , r * http . Request ) {
id := routeID ( r , "id" )
var body struct {
Publish bool `json:"publish"`
}
if err := bodyJSON ( r , & body ); err != nil {
writeErr ( w , http . StatusBadRequest , "دادهٔ ارسالی نامعتبر است" )
return
}
if err := s . store . PublishLesson ( r . Context (), id , body . Publish ); err != nil {
writeErr ( w , http . StatusInternalServerError , "بهروزرسانی وضعیت درس ممکن نشد" )
return
}
writeJSON ( w , http . StatusOK , map [ string ] any { "ok" : true })
}
func ( s * Server ) handleAdminPublishChapter ( w http . ResponseWriter , r * http . Request ) {
id := routeID ( r , "id" )
var body struct {
Publish bool `json:"publish"`
}
if err := bodyJSON ( r , & body ); err != nil {
writeErr ( w , http . StatusBadRequest , "دادهٔ ارسالی نامعتبر است" )
return
}
if err := s . store . PublishChapter ( r . Context (), id , body . Publish ); err != nil {
writeErr ( w , http . StatusInternalServerError , "بهروزرسانی وضعیت فصل ممکن نشد" )
return
}
writeJSON ( w , http . StatusOK , map [ string ] any { "ok" : true })
}
2026-10-07 19:40:26 +03:30
// ---- questions ----
func ( s * Server ) handleAdminListQuestions ( w http . ResponseWriter , r * http . Request ) {
lessonID := routeID ( r , "id" )
questions , err := s . store . ListQuestions ( r . Context (), lessonID , true )
if err != nil {
writeErr ( w , http . StatusInternalServerError , "بارگذاری پرسشها ممکن نشد" )
return
}
writeJSON ( w , http . StatusOK , map [ string ] any { "questions" : questions })
}
func ( s * Server ) handleAdminCreateQuestion ( w http . ResponseWriter , r * http . Request ) {
lessonID := routeID ( r , "id" )
var q models . MCQQuestion
if err := bodyJSON ( r , & q ); err != nil {
writeErr ( w , http . StatusBadRequest , "دادهٔ ارسالی نامعتبر است" )
return
}
q . LessonID = lessonID
if len ( q . Options ) < 2 {
writeErr ( w , http . StatusBadRequest , "دستکم ۲ گزینه لازم است" )
return
}
if q . AnswerIndex < 0 || q . AnswerIndex >= len ( q . Options ) {
writeErr ( w , http . StatusBadRequest , "شاخص پاسخ نامعتبر است" )
return
}
id , err := s . store . CreateQuestion ( r . Context (), & q )
if err != nil {
writeErr ( w , http . StatusInternalServerError , "ساخت پرسش ممکن نشد" )
return
}
writeJSON ( w , http . StatusCreated , map [ string ] any { "question" : q , "id" : id })
}
func ( s * Server ) handleAdminUpdateQuestion ( w http . ResponseWriter , r * http . Request ) {
id := routeID ( r , "id" )
var q models . MCQQuestion
if err := bodyJSON ( r , & q ); err != nil {
writeErr ( w , http . StatusBadRequest , "دادهٔ ارسالی نامعتبر است" )
return
}
if len ( q . Options ) < 2 {
writeErr ( w , http . StatusBadRequest , "دستکم ۲ گزینه لازم است" )
return
}
if q . AnswerIndex < 0 || q . AnswerIndex >= len ( q . Options ) {
writeErr ( w , http . StatusBadRequest , "شاخص پاسخ نامعتبر است" )
return
}
if err := s . store . UpdateQuestion ( r . Context (), id , & q ); err != nil {
writeErr ( w , http . StatusInternalServerError , "بهروزرسانی پرسش ممکن نشد" )
return
}
writeJSON ( w , http . StatusOK , map [ string ] any { "ok" : true })
}
func ( s * Server ) handleAdminDeleteQuestion ( w http . ResponseWriter , r * http . Request ) {
id := routeID ( r , "id" )
if err := s . store . DeleteQuestion ( r . Context (), id ); err != nil {
writeErr ( w , http . StatusInternalServerError , "حذف پرسش ممکن نشد" )
return
}
writeJSON ( w , http . StatusOK , map [ string ] any { "ok" : true })
}
// ---- video library ----
func ( s * Server ) handleAdminListVideos ( w http . ResponseWriter , r * http . Request ) {
rows , err := s . store . ListVideoAssets ( r . Context ())
if err != nil {
writeErr ( w , http . StatusInternalServerError , "بارگذاری ویدیوها ممکن نشد" )
return
}
videos := make ([] map [ string ] any , 0 , len ( rows ))
for _ , v := range rows {
videos = append ( videos , map [ string ] any {
"key" : v . Key ,
"mime" : v . Mime ,
"size" : v . SizeBytes ,
"lastModified" : v . CreatedAt ,
})
}
// Also surface keys that exist only on lessons (legacy uploads).
lessons , _ := s . store . ListLessons ( r . Context (), true )
seen := map [ string ] struct {}{}
for _ , v := range videos {
if k , ok := v [ "key" ].( string ); ok {
seen [ k ] = struct {}{}
}
}
for _ , l := range lessons {
if l . VideoKey == "" {
continue
}
if _ , ok := seen [ l . VideoKey ]; ok {
continue
}
seen [ l . VideoKey ] = struct {}{}
videos = append ( videos , map [ string ] any {
"key" : l . VideoKey ,
"mime" : "video/mp4" ,
"size" : 0 ,
})
}
writeJSON ( w , http . StatusOK , map [ string ] any { "videos" : videos })
}
func ( s * Server ) handleAdminDeleteVideo ( w http . ResponseWriter , r * http . Request ) {
key := strings . TrimSpace ( r . URL . Query (). Get ( "key" ))
if key == "" {
writeErr ( w , http . StatusBadRequest , "کلید ویدیو الزامی است" )
return
}
key = filepath . Base ( key )
if err := s . store . DeleteVideoAsset ( r . Context (), key ); err != nil {
writeErr ( w , http . StatusInternalServerError , "حذف ویدیو ممکن نشد" )
return
}
writeJSON ( w , http . StatusOK , map [ string ] any { "ok" : true })
}
func ( s * Server ) handleAdminUploadVideo ( w http . ResponseWriter , r * http . Request ) {
r . Body = http . MaxBytesReader ( w , r . Body , maxAdminVideoBytes + 1 << 20 )
if err := r . ParseMultipartForm ( 32 << 20 ); err != nil {
writeErr ( w , http . StatusRequestEntityTooLarge , "حجم ویدیو باید کمتر از ۲۰۰ مگابایت باشد" )
return
}
file , header , err := r . FormFile ( "file" )
if err != nil {
writeErr ( w , http . StatusBadRequest , "فایل ارسال نشده است" )
return
}
defer file . Close ()
accepted , err := acceptAdminVideo ( header , file )
if err != nil {
if strings . Contains ( err . Error (), "۲۰۰ مگابایت" ) {
writeErrCode ( w , http . StatusRequestEntityTooLarge , CodePayloadTooLarge , err . Error ())
return
}
writeValidationErr ( w , err . Error ())
return
}
// Prefer stable mp4-first naming; keep real extension for playback.
key := fmt . Sprintf ( "lesson-%d%s" , time . Now (). UnixNano (), accepted . Ext )
key = filepath . Base ( key )
url , err := s . storage . Save ( key , file , header . Size )
if err != nil {
writeErr ( w , http . StatusInternalServerError , "ذخیرهٔ ویدیو ممکن نشد" )
return
}
if err := s . store . CreateVideoAsset ( r . Context (), key , accepted . Mime , header . Size ); err != nil {
writeErr ( w , http . StatusInternalServerError , "ثبت ویدیو در کتابخانه ممکن نشد" )
return
}
observability . VideoUploads . Inc ()
if actor := currentUser ( r ); actor != nil {
observability . Activity ( "video.upload" , "user_id" , actor . ID , "key" , key , "size" , header . Size , "mime" , accepted . Mime )
}
writeJSON ( w , http . StatusCreated , map [ string ] any {
"url" : s . signExistingMediaURL ( url ),
"key" : key ,
"size" : header . Size ,
"mime" : accepted . Mime ,
})
}
// ---- events (admin) ----
func ( s * Server ) handleAdminListEvents ( w http . ResponseWriter , r * http . Request ) {
rows , err := s . store . Pool (). Query ( r . Context (), `
SELECT id, title, description, event_type, external_url, starts_at, ends_at, is_active, created_at
FROM events ORDER BY starts_at DESC LIMIT 200` )
if err != nil {
writeErr ( w , http . StatusInternalServerError , "بارگذاری رویدادها ممکن نشد" )
return
}
var events [] models . Event
for rows . Next () {
var e models . Event
if err := rows . Scan ( & e . ID , & e . Title , & e . Description , & e . EventType , & e . ExternalURL , & e . StartsAt , & e . EndsAt , & e . IsActive , & e . CreatedAt ); err != nil {
rows . Close ()
writeErr ( w , http . StatusInternalServerError , "بارگذاری رویدادها ممکن نشد" )
return
}
events = append ( events , e )
}
rows . Close ()
if events == nil {
events = [] models . Event {}
}
writeJSON ( w , http . StatusOK , map [ string ] any { "events" : events })
}
func ( s * Server ) handleAdminCreateEvent ( w http . ResponseWriter , r * http . Request ) {
admin := currentUser ( r )
var e models . Event
if err := bodyJSON ( r , & e ); err != nil {
writeErr ( w , http . StatusBadRequest , "دادهٔ ارسالی نامعتبر است" )
return
}
if msg := validateAdminEvent ( & e , true ); msg != "" {
writeValidationErr ( w , msg )
return
}
e . CreatedBy = & admin . ID
id , err := s . store . CreateEvent ( r . Context (), & e )
if err != nil {
writeInternalErr ( w , "admin.events.create" , err , "ساخت رویداد ممکن نشد" )
return
}
writeJSON ( w , http . StatusCreated , map [ string ] any { "event" : e , "id" : id })
}
func ( s * Server ) handleAdminUpdateEvent ( w http . ResponseWriter , r * http . Request ) {
id := routeID ( r , "id" )
var e models . Event
if err := bodyJSON ( r , & e ); err != nil {
writeErr ( w , http . StatusBadRequest , "دادهٔ ارسالی نامعتبر است" )
return
}
if msg := validateAdminEvent ( & e , false ); msg != "" {
writeValidationErr ( w , msg )
return
}
if err := s . store . UpdateEvent ( r . Context (), id , & e ); err != nil {
writeInternalErr ( w , "admin.events.update" , err , "بهروزرسانی رویداد ممکن نشد" )
return
}
writeJSON ( w , http . StatusOK , map [ string ] any { "ok" : true })
}
func ( s * Server ) handleAdminDeleteEvent ( w http . ResponseWriter , r * http . Request ) {
id := routeID ( r , "id" )
if err := s . store . DeleteEvent ( r . Context (), id ); err != nil {
writeInternalErr ( w , "admin.events.delete" , err , "حذف رویداد ممکن نشد" )
return
}
writeJSON ( w , http . StatusOK , map [ string ] any { "ok" : true })
}
// validateAdminEvent normalizes and checks event fields. create=true defaults isActive to true when unset via zero-value ambiguity is handled by callers sending explicit bool.
func validateAdminEvent ( e * models . Event , create bool ) string {
e . Title = strings . TrimSpace ( e . Title )
e . Description = strings . TrimSpace ( e . Description )
e . ExternalURL = strings . TrimSpace ( e . ExternalURL )
e . EventType = strings . TrimSpace ( e . EventType )
if e . EventType == "" {
e . EventType = "meet"
}
switch e . EventType {
case "meet" , "zoom" , "workshop" :
default :
return "نوع رویداد نامعتبر است"
}
if e . Title == "" || e . StartsAt . IsZero () || e . EndsAt . IsZero () {
return "عنوان، زمان شروع و زمان پایان الزامی هستند"
}
if utf8 . RuneCountInString ( e . Title ) > 200 {
return "عنوان رویداد خیلی طولانی است"
}
if utf8 . RuneCountInString ( e . Description ) > 5000 {
return "توضیح رویداد خیلی طولانی است"
}
if e . EndsAt . Before ( e . StartsAt ) {
return "زمان پایان باید پس از زمان شروع باشد"
}
_ = create
return ""
}