From 7e61e342925051082152da27132005c463fc62e7 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Tue, 19 May 2026 10:26:25 -0700 Subject: [PATCH] Add OPEN_LEARNER role without LMS sequential gating. Migration 000061 inserts the RBAC role and demo user (openlearner@yimaru.com). STUDENT keeps sequential ApplyAccess and practice ordering; OPEN_LEARNER shares learner permissions and customer flows. Document the role in Swagger and point initial seed SQL at the migration for the demo account. Co-authored-by: Cursor --- db/data/001_initial_seed_data.sql | 1 + .../000061_open_learner_role.down.sql | 5 + db/migrations/000061_open_learner_role.up.sql | 79 ++++++++++++ docs/docs.go | 2 + docs/swagger.json | 2 + docs/swagger.yaml | 2 + internal/domain/notification.go | 4 +- internal/domain/role.go | 18 ++- internal/services/lmsprogress/service.go | 8 +- internal/services/rbac/seeds.go | 122 +++++++++--------- internal/web_server/handlers/auth_handler.go | 2 +- .../web_server/handlers/lesson_handler.go | 2 +- internal/web_server/handlers/questions.go | 4 +- internal/web_server/handlers/user.go | 122 +++++++++--------- internal/web_server/middleware.go | 2 +- 15 files changed, 243 insertions(+), 132 deletions(-) create mode 100644 db/migrations/000061_open_learner_role.down.sql create mode 100644 db/migrations/000061_open_learner_role.up.sql diff --git a/db/data/001_initial_seed_data.sql b/db/data/001_initial_seed_data.sql index 3ec8689..49ed617 100644 --- a/db/data/001_initial_seed_data.sql +++ b/db/data/001_initial_seed_data.sql @@ -4,6 +4,7 @@ CREATE EXTENSION IF NOT EXISTS pgcrypto; -- ====================================================== -- Customer/Learner Users (login via /api/v1/auth/customer-login) -- Credentials: email + password@123 +-- OPEN_LEARNER demo user is seeded by migration 000061_open_learner_role (not here). -- ====================================================== INSERT INTO users ( id, diff --git a/db/migrations/000061_open_learner_role.down.sql b/db/migrations/000061_open_learner_role.down.sql new file mode 100644 index 0000000..94f330b --- /dev/null +++ b/db/migrations/000061_open_learner_role.down.sql @@ -0,0 +1,5 @@ +DELETE FROM users WHERE id = 13 AND email = 'openlearner@yimaru.com'; + +DELETE FROM role_permissions WHERE role_id = (SELECT id FROM roles WHERE name = 'OPEN_LEARNER'); + +DELETE FROM roles WHERE name = 'OPEN_LEARNER'; diff --git a/db/migrations/000061_open_learner_role.up.sql b/db/migrations/000061_open_learner_role.up.sql new file mode 100644 index 0000000..7350306 --- /dev/null +++ b/db/migrations/000061_open_learner_role.up.sql @@ -0,0 +1,79 @@ +-- OPEN_LEARNER: learner role with STUDENT-like RBAC but without LMS sequential prerequisite locks (handled in app code). + +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +INSERT INTO roles (name, description, is_system) VALUES + ( + 'OPEN_LEARNER', + 'Learner with full LMS catalog access without sequential prerequisite locking', + TRUE + ) +ON CONFLICT (name) DO NOTHING; + +-- Demo OPEN_LEARNER (customer-login): openlearner@yimaru.com / password@123 +INSERT INTO users ( + id, + first_name, + last_name, + gender, + birth_day, + email, + phone_number, + role, + password, + age_group, + education_level, + country, + region, + knowledge_level, + nick_name, + occupation, + learning_goal, + language_goal, + language_challange, + favourite_topic, + initial_assessment_completed, + email_verified, + phone_verified, + status, + last_login, + profile_completed, + profile_picture_url, + preferred_language, + created_at, + updated_at +) +VALUES +( + 13, + 'Demo', + 'OpenLearner', + 'Female', + '1999-06-01', + 'openlearner@yimaru.com', + NULL, + 'OPEN_LEARNER', + crypt('password@123', gen_salt('bf'))::bytea, + '25_34', + 'Bachelor', + 'Ethiopia', + 'Addis Ababa', + 'BEGINNER', + 'OpenLearner', + 'Tester', + 'Preview LMS content without sequential locks', + 'English', + 'Grammar', + 'Technology', + FALSE, + TRUE, + FALSE, + 'ACTIVE', + NULL, + FALSE, + NULL, + 'en', + CURRENT_TIMESTAMP, + NULL +) +ON CONFLICT (id) DO NOTHING; diff --git a/docs/docs.go b/docs/docs.go index 321c4f4..cd3a3bc 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -11063,6 +11063,7 @@ const docTemplate = `{ "SUPER_ADMIN", "ADMIN", "STUDENT", + "OPEN_LEARNER", "INSTRUCTOR", "SUPPORT" ], @@ -11070,6 +11071,7 @@ const docTemplate = `{ "RoleSuperAdmin", "RoleAdmin", "RoleStudent", + "RoleOpenLearner", "RoleInstructor", "RoleSupport" ] diff --git a/docs/swagger.json b/docs/swagger.json index 73c0fa0..6f3287b 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -11055,6 +11055,7 @@ "SUPER_ADMIN", "ADMIN", "STUDENT", + "OPEN_LEARNER", "INSTRUCTOR", "SUPPORT" ], @@ -11062,6 +11063,7 @@ "RoleSuperAdmin", "RoleAdmin", "RoleStudent", + "RoleOpenLearner", "RoleInstructor", "RoleSupport" ] diff --git a/docs/swagger.yaml b/docs/swagger.yaml index dcd9188..625e545 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -813,6 +813,7 @@ definitions: - SUPER_ADMIN - ADMIN - STUDENT + - OPEN_LEARNER - INSTRUCTOR - SUPPORT type: string @@ -820,6 +821,7 @@ definitions: - RoleSuperAdmin - RoleAdmin - RoleStudent + - RoleOpenLearner - RoleInstructor - RoleSupport domain.RoleRecord: diff --git a/internal/domain/notification.go b/internal/domain/notification.go index bbd1b22..dc3a660 100644 --- a/internal/domain/notification.go +++ b/internal/domain/notification.go @@ -31,7 +31,7 @@ const ( NOTIFICATION_TYPE_ADMIN_CREATED NotificationType = "admin_created" NOTIFICATION_TYPE_TEAM_MEMBER_CREATED NotificationType = "team_member_created" NOTIFICATION_TYPE_USER_DELETED NotificationType = "user_deleted" - NOTIFICATION_TYPE_SYSTEM_ALERT NotificationType = "system_alert" + NOTIFICATION_TYPE_SYSTEM_ALERT NotificationType = "system_alert" NotificationRecieverSideAdmin NotificationRecieverSide = "admin" NotificationRecieverSideCustomer NotificationRecieverSide = "customer" @@ -137,6 +137,8 @@ func ReceiverFromRole(role Role) NotificationRecieverSide { return NotificationRecieverSideAdmin case RoleStudent: return NotificationRecieverSideCustomer + case RoleOpenLearner: + return NotificationRecieverSideCustomer case RoleInstructor: return NotificationRecieverSideCustomer default: diff --git a/internal/domain/role.go b/internal/domain/role.go index 27ed20c..cc3dd6a 100644 --- a/internal/domain/role.go +++ b/internal/domain/role.go @@ -6,19 +6,31 @@ const ( RoleSuperAdmin Role = "SUPER_ADMIN" RoleAdmin Role = "ADMIN" RoleStudent Role = "STUDENT" - RoleInstructor Role = "INSTRUCTOR" - RoleSupport Role = "SUPPORT" + // RoleOpenLearner can consume LMS content like a learner but without sequential prerequisite locking (step-by-step gates). + RoleOpenLearner Role = "OPEN_LEARNER" + RoleInstructor Role = "INSTRUCTOR" + RoleSupport Role = "SUPPORT" ) func (r Role) IsValid() bool { switch r { - case RoleSuperAdmin, RoleAdmin, RoleStudent, RoleInstructor, RoleSupport: + case RoleSuperAdmin, RoleAdmin, RoleStudent, RoleOpenLearner, RoleInstructor, RoleSupport: return true default: return false } } +// UsesLMSSequentialGating is true when LMS APIs apply sequential prerequisite locks (403 when blocked). +func (r Role) UsesLMSSequentialGating() bool { + return r == RoleStudent +} + +// IsCustomerLearnerRole is true for platform roles that sign in as customers and consume learner-facing LMS APIs. +func (r Role) IsCustomerLearnerRole() bool { + return r == RoleStudent || r == RoleOpenLearner +} + func (r Role) Value() string { return string(r) } diff --git a/internal/services/lmsprogress/service.go b/internal/services/lmsprogress/service.go index 7e1b4b9..9db1ea6 100644 --- a/internal/services/lmsprogress/service.go +++ b/internal/services/lmsprogress/service.go @@ -146,7 +146,7 @@ func (s *Service) CanAccessLesson(ctx context.Context, userID, lessonID int64) ( // ApplyAccessProgram sets p.Access for a learner. Non-learners: clears Access to omit from JSON. func (s *Service) ApplyAccessProgram(ctx context.Context, role domain.Role, userID int64, p *domain.Program) error { - if role != domain.RoleStudent { + if !role.UsesLMSSequentialGating() { p.Access = nil return nil } @@ -172,7 +172,7 @@ func (s *Service) ApplyAccessProgram(ctx context.Context, role domain.Role, user // ApplyAccessCourse sets c.Access for a learner. func (s *Service) ApplyAccessCourse(ctx context.Context, role domain.Role, userID int64, c *domain.Course) error { - if role != domain.RoleStudent { + if !role.UsesLMSSequentialGating() { c.Access = nil return nil } @@ -198,7 +198,7 @@ func (s *Service) ApplyAccessCourse(ctx context.Context, role domain.Role, userI // ApplyAccessModule sets m.Access for a learner. func (s *Service) ApplyAccessModule(ctx context.Context, role domain.Role, userID int64, m *domain.Module) error { - if role != domain.RoleStudent { + if !role.UsesLMSSequentialGating() { m.Access = nil return nil } @@ -224,7 +224,7 @@ func (s *Service) ApplyAccessModule(ctx context.Context, role domain.Role, userI // ApplyAccessLesson sets l.Access for a learner. func (s *Service) ApplyAccessLesson(ctx context.Context, role domain.Role, userID int64, les *domain.Lesson) error { - if role != domain.RoleStudent { + if !role.UsesLMSSequentialGating() { les.Access = nil return nil } diff --git a/internal/services/rbac/seeds.go b/internal/services/rbac/seeds.go index 2d13c01..2f6e79c 100644 --- a/internal/services/rbac/seeds.go +++ b/internal/services/rbac/seeds.go @@ -302,10 +302,71 @@ var AllPermissions = []domain.PermissionSeed{ {Key: "internal.db.reset_reseed", Name: "Reset And Reseed Database", Description: "Dangerous operation: clears all data and re-seeds from SQL files", GroupName: "Internal Operations"}, } +// defaultStudentLearnerPermissions is the learner consumption permission set shared by STUDENT and OPEN_LEARNER. +// LMS sequential prerequisite locking applies only to STUDENT in application handlers. +var defaultStudentLearnerPermissions = []string{ + // Course browsing + "course_categories.list", "course_categories.get", + "courses.get", "courses.list_by_program", + "modules.get", "modules.list_by_course", + "lessons.get", "lessons.list_by_module", "lessons.complete", + "practices.get", "practices.list", + "subcourses.get", "subcourses.list_by_course", "subcourses.list_by_course_list", "subcourses.list_active", + "videos.get", "videos.list_by_subcourse", "videos.list_published", + "learning_tree.get", + + "programs.list", "programs.get", + "exam_prep.catalog_courses.list", "exam_prep.catalog_courses.get", + "exam_prep.units.list", "exam_prep.units.get", + "exam_prep.modules.list", "exam_prep.modules.get", + "exam_prep.lessons.list_by_module", "exam_prep.lessons.get", + "exam_prep.practices.list_by_lesson", "exam_prep.practices.get", + "lms.get_my_progress", + + // Questions (read + attempt) + "questions.list", "questions.search", "questions.get", + "question_sets.list", "question_sets.list_by_owner", "question_sets.get", + "question_set_items.list", + "question_set_personas.list", + + // Subscriptions & Payments (own) + "subscriptions.checkout", "subscriptions.get_mine", "subscriptions.history", + "subscriptions.status", "subscriptions.cancel", "subscriptions.set_auto_renew", + "payments.initiate", "payments.verify", "payments.list_mine", "payments.get", "payments.cancel", + "payments.direct_initiate", "payments.direct_verify_otp", + + // User (self-service) + "users.update_self", "users.delete_self", "users.cancel_delete_self", "users.profile_completed", "users.upload_profile_picture", "users.user_profile", + + // Notifications (own) + "notifications.ws_connect", "notifications.list_mine", "notifications.list_all", + "notifications.mark_read", "notifications.mark_all_read", "notifications.mark_unread", "notifications.mark_all_unread", + "notifications.delete_mine", "notifications.count_unread", + "notifications.test_push", + + // Issues (own) + "issues.create", "issues.list_mine", + + // Devices + "devices.register", "devices.unregister", + + // Progress + "progress.start", "progress.update", "progress.complete", "progress.check_access", "progress.get_course", + + // Sub-course Prerequisites (read) + "subcourse_prerequisites.list", + + // Ratings + "ratings.submit", "ratings.list_by_target", "ratings.summary", "ratings.get_mine", "ratings.list_mine", "ratings.delete", + + // Auth + "auth.logout", +} + // DefaultRolePermissions maps each system role to the permission keys it should // have by default. This preserves the previous middleware behavior: // - ADMIN: everything that was previously OnlyAdminAndAbove + SuperAdminOnly + all authenticated routes -// - STUDENT/INSTRUCTOR/SUPPORT: only self-service endpoints (profile, courses, progress, etc.) +// - STUDENT/OPEN_LEARNER/INSTRUCTOR/SUPPORT: only self-service endpoints (profile, courses, progress, etc.) var DefaultRolePermissions = map[string][]string{ "ADMIN": { // Course Management (full access) @@ -409,64 +470,9 @@ var DefaultRolePermissions = map[string][]string{ "internal.db.reset_reseed", }, - "STUDENT": { - // Course browsing - "course_categories.list", "course_categories.get", - "courses.get", "courses.list_by_program", - "modules.get", "modules.list_by_course", - "lessons.get", "lessons.list_by_module", "lessons.complete", - "practices.get", "practices.list", - "subcourses.get", "subcourses.list_by_course", "subcourses.list_by_course_list", "subcourses.list_active", - "videos.get", "videos.list_by_subcourse", "videos.list_published", - "learning_tree.get", + "STUDENT": defaultStudentLearnerPermissions, - "programs.list", "programs.get", - "exam_prep.catalog_courses.list", "exam_prep.catalog_courses.get", - "exam_prep.units.list", "exam_prep.units.get", - "exam_prep.modules.list", "exam_prep.modules.get", - "exam_prep.lessons.list_by_module", "exam_prep.lessons.get", - "exam_prep.practices.list_by_lesson", "exam_prep.practices.get", - "lms.get_my_progress", - - // Questions (read + attempt) - "questions.list", "questions.search", "questions.get", - "question_sets.list", "question_sets.list_by_owner", "question_sets.get", - "question_set_items.list", - "question_set_personas.list", - - // Subscriptions & Payments (own) - "subscriptions.checkout", "subscriptions.get_mine", "subscriptions.history", - "subscriptions.status", "subscriptions.cancel", "subscriptions.set_auto_renew", - "payments.initiate", "payments.verify", "payments.list_mine", "payments.get", "payments.cancel", - "payments.direct_initiate", "payments.direct_verify_otp", - - // User (self-service) - "users.update_self", "users.delete_self", "users.cancel_delete_self", "users.profile_completed", "users.upload_profile_picture", "users.user_profile", - - // Notifications (own) - "notifications.ws_connect", "notifications.list_mine", "notifications.list_all", - "notifications.mark_read", "notifications.mark_all_read", "notifications.mark_unread", "notifications.mark_all_unread", - "notifications.delete_mine", "notifications.count_unread", - "notifications.test_push", - - // Issues (own) - "issues.create", "issues.list_mine", - - // Devices - "devices.register", "devices.unregister", - - // Progress - "progress.start", "progress.update", "progress.complete", "progress.check_access", "progress.get_course", - - // Sub-course Prerequisites (read) - "subcourse_prerequisites.list", - - // Ratings - "ratings.submit", "ratings.list_by_target", "ratings.summary", "ratings.get_mine", "ratings.list_mine", "ratings.delete", - - // Auth - "auth.logout", - }, + "OPEN_LEARNER": defaultStudentLearnerPermissions, "INSTRUCTOR": { // Course browsing + management diff --git a/internal/web_server/handlers/auth_handler.go b/internal/web_server/handlers/auth_handler.go index 6f2cde7..d5abd88 100644 --- a/internal/web_server/handlers/auth_handler.go +++ b/internal/web_server/handlers/auth_handler.go @@ -357,7 +357,7 @@ func (h *Handler) LoginAdmin(c *fiber.Ctx) error { } } - if successRes.Role == domain.RoleStudent || successRes.Role == domain.RoleInstructor { + if successRes.Role == domain.RoleStudent || successRes.Role == domain.RoleOpenLearner || successRes.Role == domain.RoleInstructor { h.mongoLoggerSvc.Warn("Login attempt: admin login of user", zap.Int("status_code", fiber.StatusForbidden), zap.String("role", string(successRes.Role)), diff --git a/internal/web_server/handlers/lesson_handler.go b/internal/web_server/handlers/lesson_handler.go index 531444f..528a18f 100644 --- a/internal/web_server/handlers/lesson_handler.go +++ b/internal/web_server/handlers/lesson_handler.go @@ -279,7 +279,7 @@ func (h *Handler) CompleteLesson(c *fiber.Ctx) error { } uid := c.Locals("user_id").(int64) role := c.Locals("role").(domain.Role) - if role == domain.RoleStudent { + if role.UsesLMSSequentialGating() { ok, reason, err := h.lmsProgressSvc.CanAccessLesson(c.Context(), uid, id) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ diff --git a/internal/web_server/handlers/questions.go b/internal/web_server/handlers/questions.go index 19a8b1c..2ae425c 100644 --- a/internal/web_server/handlers/questions.go +++ b/internal/web_server/handlers/questions.go @@ -737,7 +737,7 @@ func isSequenceGatedPractice(set domain.QuestionSet) bool { func (h *Handler) enforcePracticeSequenceForStudent(c *fiber.Ctx, set domain.QuestionSet) error { role := c.Locals("role").(domain.Role) - if role != domain.RoleStudent || !isSequenceGatedPractice(set) { + if !role.UsesLMSSequentialGating() || !isSequenceGatedPractice(set) { return nil } if !strings.EqualFold(set.Status, "PUBLISHED") { @@ -1547,7 +1547,7 @@ func (h *Handler) GetQuestionsByPractice(c *fiber.Ctx) error { // @Router /api/v1/progress/practices/{id}/complete [post] func (h *Handler) CompletePractice(c *fiber.Ctx) error { role := c.Locals("role").(domain.Role) - if role != domain.RoleStudent { + if !role.IsCustomerLearnerRole() { return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{ Message: "Only learners can complete practices", }) diff --git a/internal/web_server/handlers/user.go b/internal/web_server/handlers/user.go index 7daa3bc..a1a6de2 100644 --- a/internal/web_server/handlers/user.go +++ b/internal/web_server/handlers/user.go @@ -1470,18 +1470,18 @@ func (h *Handler) GetUserProfile(c *fiber.Ctx) error { } return "" }(), - EducationLevel: user.EducationLevel, - Country: user.Country, - Region: user.Region, - EmailVerified: user.EmailVerified, - PhoneVerified: user.PhoneVerified, - Status: user.Status, - LastLogin: lastLogin, - ProfileCompleted: user.ProfileCompleted, - ProfilePictureURL: user.ProfilePictureURL, - PreferredLanguage: user.PreferredLanguage, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, + EducationLevel: user.EducationLevel, + Country: user.Country, + Region: user.Region, + EmailVerified: user.EmailVerified, + PhoneVerified: user.PhoneVerified, + Status: user.Status, + LastLogin: lastLogin, + ProfileCompleted: user.ProfileCompleted, + ProfilePictureURL: user.ProfilePictureURL, + PreferredLanguage: user.PreferredLanguage, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, SubscriptionStatus: subscriptionStatus, } return response.WriteJSON(c, fiber.StatusOK, "User profile retrieved successfully", res, nil) @@ -1567,22 +1567,22 @@ func (h *Handler) AdminProfile(c *fiber.Ctx) error { FirstName: user.FirstName, LastName: user.LastName, // UserName: user.UserName, - Email: user.Email, - PhoneNumber: user.PhoneNumber, - Role: user.Role, - AgeGroup: user.AgeGroup, - EducationLevel: user.EducationLevel, - Country: user.Country, - Region: user.Region, - EmailVerified: user.EmailVerified, - PhoneVerified: user.PhoneVerified, - Status: user.Status, - LastLogin: lastLogin, - ProfileCompleted: user.ProfileCompleted, - ProfilePictureURL: user.ProfilePictureURL, - PreferredLanguage: user.PreferredLanguage, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, + Email: user.Email, + PhoneNumber: user.PhoneNumber, + Role: user.Role, + AgeGroup: user.AgeGroup, + EducationLevel: user.EducationLevel, + Country: user.Country, + Region: user.Region, + EmailVerified: user.EmailVerified, + PhoneVerified: user.PhoneVerified, + Status: user.Status, + LastLogin: lastLogin, + ProfileCompleted: user.ProfileCompleted, + ProfilePictureURL: user.ProfilePictureURL, + PreferredLanguage: user.PreferredLanguage, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, SubscriptionStatus: subscriptionStatus, } // Ensure birthday is included and formatted @@ -1721,21 +1721,21 @@ func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error { } return "" }(), - PhoneNumber: user.PhoneNumber, - Role: user.Role, - AgeGroup: user.AgeGroup, - EducationLevel: user.EducationLevel, - Country: user.Country, - Region: user.Region, - EmailVerified: user.EmailVerified, - PhoneVerified: user.PhoneVerified, - Status: user.Status, - LastLogin: lastLogin, - ProfileCompleted: user.ProfileCompleted, - ProfilePictureURL: user.ProfilePictureURL, - PreferredLanguage: user.PreferredLanguage, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, + PhoneNumber: user.PhoneNumber, + Role: user.Role, + AgeGroup: user.AgeGroup, + EducationLevel: user.EducationLevel, + Country: user.Country, + Region: user.Region, + EmailVerified: user.EmailVerified, + PhoneVerified: user.PhoneVerified, + Status: user.Status, + LastLogin: lastLogin, + ProfileCompleted: user.ProfileCompleted, + ProfilePictureURL: user.ProfilePictureURL, + PreferredLanguage: user.PreferredLanguage, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, SubscriptionStatus: subStatus, }) } @@ -1828,22 +1828,22 @@ func (h *Handler) GetUserByID(c *fiber.Ctx) error { Occupation: user.Occupation, FavouriteTopic: user.FavouriteTopic, // UserName: user.UserName, - Email: user.Email, - PhoneNumber: user.PhoneNumber, - Role: user.Role, - AgeGroup: user.AgeGroup, - EducationLevel: user.EducationLevel, - Country: user.Country, - Region: user.Region, - EmailVerified: user.EmailVerified, - PhoneVerified: user.PhoneVerified, - Status: user.Status, - LastLogin: lastLogin, - ProfileCompleted: user.ProfileCompleted, - ProfilePictureURL: user.ProfilePictureURL, - PreferredLanguage: user.PreferredLanguage, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, + Email: user.Email, + PhoneNumber: user.PhoneNumber, + Role: user.Role, + AgeGroup: user.AgeGroup, + EducationLevel: user.EducationLevel, + Country: user.Country, + Region: user.Region, + EmailVerified: user.EmailVerified, + PhoneVerified: user.PhoneVerified, + Status: user.Status, + LastLogin: lastLogin, + ProfileCompleted: user.ProfileCompleted, + ProfilePictureURL: user.ProfilePictureURL, + PreferredLanguage: user.PreferredLanguage, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, SubscriptionStatus: subscriptionStatus, } @@ -1933,7 +1933,7 @@ func (h *Handler) DeleteMyUserAccount(c *fiber.Ctx) error { if !ok { return fiber.NewError(fiber.StatusUnauthorized, "Invalid authenticated role") } - if role != domain.RoleStudent { + if !role.IsCustomerLearnerRole() { return fiber.NewError(fiber.StatusForbidden, "Only learners can delete their own account using this endpoint") } @@ -1986,7 +1986,7 @@ func (h *Handler) CancelMyUserAccountDeletion(c *fiber.Ctx) error { if !ok { return fiber.NewError(fiber.StatusUnauthorized, "Invalid authenticated role") } - if role != domain.RoleStudent { + if !role.IsCustomerLearnerRole() { return fiber.NewError(fiber.StatusForbidden, "Only learners can cancel their own account deletion using this endpoint") } diff --git a/internal/web_server/middleware.go b/internal/web_server/middleware.go index 827a722..3071016 100644 --- a/internal/web_server/middleware.go +++ b/internal/web_server/middleware.go @@ -183,7 +183,7 @@ func (a *App) RequireActiveSubscription() fiber.Handler { switch role { case domain.RoleSuperAdmin, domain.RoleAdmin, domain.RoleInstructor, domain.RoleSupport: return c.Next() - case domain.RoleStudent: + case domain.RoleStudent, domain.RoleOpenLearner: userID, ok := c.Locals("user_id").(int64) if !ok || userID == 0 { return fiber.NewError(fiber.StatusUnauthorized, "Unauthorized")