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 <cursoragent@cursor.com>
This commit is contained in:
parent
83db13bed0
commit
7e61e34292
|
|
@ -4,6 +4,7 @@ CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||||
-- ======================================================
|
-- ======================================================
|
||||||
-- Customer/Learner Users (login via /api/v1/auth/customer-login)
|
-- Customer/Learner Users (login via /api/v1/auth/customer-login)
|
||||||
-- Credentials: email + password@123
|
-- Credentials: email + password@123
|
||||||
|
-- OPEN_LEARNER demo user is seeded by migration 000061_open_learner_role (not here).
|
||||||
-- ======================================================
|
-- ======================================================
|
||||||
INSERT INTO users (
|
INSERT INTO users (
|
||||||
id,
|
id,
|
||||||
|
|
|
||||||
5
db/migrations/000061_open_learner_role.down.sql
Normal file
5
db/migrations/000061_open_learner_role.down.sql
Normal file
|
|
@ -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';
|
||||||
79
db/migrations/000061_open_learner_role.up.sql
Normal file
79
db/migrations/000061_open_learner_role.up.sql
Normal file
|
|
@ -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;
|
||||||
|
|
@ -11063,6 +11063,7 @@ const docTemplate = `{
|
||||||
"SUPER_ADMIN",
|
"SUPER_ADMIN",
|
||||||
"ADMIN",
|
"ADMIN",
|
||||||
"STUDENT",
|
"STUDENT",
|
||||||
|
"OPEN_LEARNER",
|
||||||
"INSTRUCTOR",
|
"INSTRUCTOR",
|
||||||
"SUPPORT"
|
"SUPPORT"
|
||||||
],
|
],
|
||||||
|
|
@ -11070,6 +11071,7 @@ const docTemplate = `{
|
||||||
"RoleSuperAdmin",
|
"RoleSuperAdmin",
|
||||||
"RoleAdmin",
|
"RoleAdmin",
|
||||||
"RoleStudent",
|
"RoleStudent",
|
||||||
|
"RoleOpenLearner",
|
||||||
"RoleInstructor",
|
"RoleInstructor",
|
||||||
"RoleSupport"
|
"RoleSupport"
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -11055,6 +11055,7 @@
|
||||||
"SUPER_ADMIN",
|
"SUPER_ADMIN",
|
||||||
"ADMIN",
|
"ADMIN",
|
||||||
"STUDENT",
|
"STUDENT",
|
||||||
|
"OPEN_LEARNER",
|
||||||
"INSTRUCTOR",
|
"INSTRUCTOR",
|
||||||
"SUPPORT"
|
"SUPPORT"
|
||||||
],
|
],
|
||||||
|
|
@ -11062,6 +11063,7 @@
|
||||||
"RoleSuperAdmin",
|
"RoleSuperAdmin",
|
||||||
"RoleAdmin",
|
"RoleAdmin",
|
||||||
"RoleStudent",
|
"RoleStudent",
|
||||||
|
"RoleOpenLearner",
|
||||||
"RoleInstructor",
|
"RoleInstructor",
|
||||||
"RoleSupport"
|
"RoleSupport"
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -813,6 +813,7 @@ definitions:
|
||||||
- SUPER_ADMIN
|
- SUPER_ADMIN
|
||||||
- ADMIN
|
- ADMIN
|
||||||
- STUDENT
|
- STUDENT
|
||||||
|
- OPEN_LEARNER
|
||||||
- INSTRUCTOR
|
- INSTRUCTOR
|
||||||
- SUPPORT
|
- SUPPORT
|
||||||
type: string
|
type: string
|
||||||
|
|
@ -820,6 +821,7 @@ definitions:
|
||||||
- RoleSuperAdmin
|
- RoleSuperAdmin
|
||||||
- RoleAdmin
|
- RoleAdmin
|
||||||
- RoleStudent
|
- RoleStudent
|
||||||
|
- RoleOpenLearner
|
||||||
- RoleInstructor
|
- RoleInstructor
|
||||||
- RoleSupport
|
- RoleSupport
|
||||||
domain.RoleRecord:
|
domain.RoleRecord:
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ const (
|
||||||
NOTIFICATION_TYPE_ADMIN_CREATED NotificationType = "admin_created"
|
NOTIFICATION_TYPE_ADMIN_CREATED NotificationType = "admin_created"
|
||||||
NOTIFICATION_TYPE_TEAM_MEMBER_CREATED NotificationType = "team_member_created"
|
NOTIFICATION_TYPE_TEAM_MEMBER_CREATED NotificationType = "team_member_created"
|
||||||
NOTIFICATION_TYPE_USER_DELETED NotificationType = "user_deleted"
|
NOTIFICATION_TYPE_USER_DELETED NotificationType = "user_deleted"
|
||||||
NOTIFICATION_TYPE_SYSTEM_ALERT NotificationType = "system_alert"
|
NOTIFICATION_TYPE_SYSTEM_ALERT NotificationType = "system_alert"
|
||||||
|
|
||||||
NotificationRecieverSideAdmin NotificationRecieverSide = "admin"
|
NotificationRecieverSideAdmin NotificationRecieverSide = "admin"
|
||||||
NotificationRecieverSideCustomer NotificationRecieverSide = "customer"
|
NotificationRecieverSideCustomer NotificationRecieverSide = "customer"
|
||||||
|
|
@ -137,6 +137,8 @@ func ReceiverFromRole(role Role) NotificationRecieverSide {
|
||||||
return NotificationRecieverSideAdmin
|
return NotificationRecieverSideAdmin
|
||||||
case RoleStudent:
|
case RoleStudent:
|
||||||
return NotificationRecieverSideCustomer
|
return NotificationRecieverSideCustomer
|
||||||
|
case RoleOpenLearner:
|
||||||
|
return NotificationRecieverSideCustomer
|
||||||
case RoleInstructor:
|
case RoleInstructor:
|
||||||
return NotificationRecieverSideCustomer
|
return NotificationRecieverSideCustomer
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -6,19 +6,31 @@ const (
|
||||||
RoleSuperAdmin Role = "SUPER_ADMIN"
|
RoleSuperAdmin Role = "SUPER_ADMIN"
|
||||||
RoleAdmin Role = "ADMIN"
|
RoleAdmin Role = "ADMIN"
|
||||||
RoleStudent Role = "STUDENT"
|
RoleStudent Role = "STUDENT"
|
||||||
RoleInstructor Role = "INSTRUCTOR"
|
// RoleOpenLearner can consume LMS content like a learner but without sequential prerequisite locking (step-by-step gates).
|
||||||
RoleSupport Role = "SUPPORT"
|
RoleOpenLearner Role = "OPEN_LEARNER"
|
||||||
|
RoleInstructor Role = "INSTRUCTOR"
|
||||||
|
RoleSupport Role = "SUPPORT"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r Role) IsValid() bool {
|
func (r Role) IsValid() bool {
|
||||||
switch r {
|
switch r {
|
||||||
case RoleSuperAdmin, RoleAdmin, RoleStudent, RoleInstructor, RoleSupport:
|
case RoleSuperAdmin, RoleAdmin, RoleStudent, RoleOpenLearner, RoleInstructor, RoleSupport:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
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 {
|
func (r Role) Value() string {
|
||||||
return string(r)
|
return string(r)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
// 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 {
|
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
|
p.Access = nil
|
||||||
return 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.
|
// ApplyAccessCourse sets c.Access for a learner.
|
||||||
func (s *Service) ApplyAccessCourse(ctx context.Context, role domain.Role, userID int64, c *domain.Course) error {
|
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
|
c.Access = nil
|
||||||
return 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.
|
// ApplyAccessModule sets m.Access for a learner.
|
||||||
func (s *Service) ApplyAccessModule(ctx context.Context, role domain.Role, userID int64, m *domain.Module) error {
|
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
|
m.Access = nil
|
||||||
return 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.
|
// ApplyAccessLesson sets l.Access for a learner.
|
||||||
func (s *Service) ApplyAccessLesson(ctx context.Context, role domain.Role, userID int64, les *domain.Lesson) error {
|
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
|
les.Access = nil
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"},
|
{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
|
// DefaultRolePermissions maps each system role to the permission keys it should
|
||||||
// have by default. This preserves the previous middleware behavior:
|
// have by default. This preserves the previous middleware behavior:
|
||||||
// - ADMIN: everything that was previously OnlyAdminAndAbove + SuperAdminOnly + all authenticated routes
|
// - 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{
|
var DefaultRolePermissions = map[string][]string{
|
||||||
"ADMIN": {
|
"ADMIN": {
|
||||||
// Course Management (full access)
|
// Course Management (full access)
|
||||||
|
|
@ -409,64 +470,9 @@ var DefaultRolePermissions = map[string][]string{
|
||||||
"internal.db.reset_reseed",
|
"internal.db.reset_reseed",
|
||||||
},
|
},
|
||||||
|
|
||||||
"STUDENT": {
|
"STUDENT": defaultStudentLearnerPermissions,
|
||||||
// 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",
|
"OPEN_LEARNER": defaultStudentLearnerPermissions,
|
||||||
"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",
|
|
||||||
},
|
|
||||||
|
|
||||||
"INSTRUCTOR": {
|
"INSTRUCTOR": {
|
||||||
// Course browsing + management
|
// Course browsing + management
|
||||||
|
|
|
||||||
|
|
@ -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",
|
h.mongoLoggerSvc.Warn("Login attempt: admin login of user",
|
||||||
zap.Int("status_code", fiber.StatusForbidden),
|
zap.Int("status_code", fiber.StatusForbidden),
|
||||||
zap.String("role", string(successRes.Role)),
|
zap.String("role", string(successRes.Role)),
|
||||||
|
|
|
||||||
|
|
@ -279,7 +279,7 @@ func (h *Handler) CompleteLesson(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
uid := c.Locals("user_id").(int64)
|
uid := c.Locals("user_id").(int64)
|
||||||
role := c.Locals("role").(domain.Role)
|
role := c.Locals("role").(domain.Role)
|
||||||
if role == domain.RoleStudent {
|
if role.UsesLMSSequentialGating() {
|
||||||
ok, reason, err := h.lmsProgressSvc.CanAccessLesson(c.Context(), uid, id)
|
ok, reason, err := h.lmsProgressSvc.CanAccessLesson(c.Context(), uid, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
|
|
||||||
|
|
@ -737,7 +737,7 @@ func isSequenceGatedPractice(set domain.QuestionSet) bool {
|
||||||
|
|
||||||
func (h *Handler) enforcePracticeSequenceForStudent(c *fiber.Ctx, set domain.QuestionSet) error {
|
func (h *Handler) enforcePracticeSequenceForStudent(c *fiber.Ctx, set domain.QuestionSet) error {
|
||||||
role := c.Locals("role").(domain.Role)
|
role := c.Locals("role").(domain.Role)
|
||||||
if role != domain.RoleStudent || !isSequenceGatedPractice(set) {
|
if !role.UsesLMSSequentialGating() || !isSequenceGatedPractice(set) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if !strings.EqualFold(set.Status, "PUBLISHED") {
|
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]
|
// @Router /api/v1/progress/practices/{id}/complete [post]
|
||||||
func (h *Handler) CompletePractice(c *fiber.Ctx) error {
|
func (h *Handler) CompletePractice(c *fiber.Ctx) error {
|
||||||
role := c.Locals("role").(domain.Role)
|
role := c.Locals("role").(domain.Role)
|
||||||
if role != domain.RoleStudent {
|
if !role.IsCustomerLearnerRole() {
|
||||||
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
||||||
Message: "Only learners can complete practices",
|
Message: "Only learners can complete practices",
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1470,18 +1470,18 @@ func (h *Handler) GetUserProfile(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}(),
|
}(),
|
||||||
EducationLevel: user.EducationLevel,
|
EducationLevel: user.EducationLevel,
|
||||||
Country: user.Country,
|
Country: user.Country,
|
||||||
Region: user.Region,
|
Region: user.Region,
|
||||||
EmailVerified: user.EmailVerified,
|
EmailVerified: user.EmailVerified,
|
||||||
PhoneVerified: user.PhoneVerified,
|
PhoneVerified: user.PhoneVerified,
|
||||||
Status: user.Status,
|
Status: user.Status,
|
||||||
LastLogin: lastLogin,
|
LastLogin: lastLogin,
|
||||||
ProfileCompleted: user.ProfileCompleted,
|
ProfileCompleted: user.ProfileCompleted,
|
||||||
ProfilePictureURL: user.ProfilePictureURL,
|
ProfilePictureURL: user.ProfilePictureURL,
|
||||||
PreferredLanguage: user.PreferredLanguage,
|
PreferredLanguage: user.PreferredLanguage,
|
||||||
CreatedAt: user.CreatedAt,
|
CreatedAt: user.CreatedAt,
|
||||||
UpdatedAt: user.UpdatedAt,
|
UpdatedAt: user.UpdatedAt,
|
||||||
SubscriptionStatus: subscriptionStatus,
|
SubscriptionStatus: subscriptionStatus,
|
||||||
}
|
}
|
||||||
return response.WriteJSON(c, fiber.StatusOK, "User profile retrieved successfully", res, nil)
|
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,
|
FirstName: user.FirstName,
|
||||||
LastName: user.LastName,
|
LastName: user.LastName,
|
||||||
// UserName: user.UserName,
|
// UserName: user.UserName,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
PhoneNumber: user.PhoneNumber,
|
PhoneNumber: user.PhoneNumber,
|
||||||
Role: user.Role,
|
Role: user.Role,
|
||||||
AgeGroup: user.AgeGroup,
|
AgeGroup: user.AgeGroup,
|
||||||
EducationLevel: user.EducationLevel,
|
EducationLevel: user.EducationLevel,
|
||||||
Country: user.Country,
|
Country: user.Country,
|
||||||
Region: user.Region,
|
Region: user.Region,
|
||||||
EmailVerified: user.EmailVerified,
|
EmailVerified: user.EmailVerified,
|
||||||
PhoneVerified: user.PhoneVerified,
|
PhoneVerified: user.PhoneVerified,
|
||||||
Status: user.Status,
|
Status: user.Status,
|
||||||
LastLogin: lastLogin,
|
LastLogin: lastLogin,
|
||||||
ProfileCompleted: user.ProfileCompleted,
|
ProfileCompleted: user.ProfileCompleted,
|
||||||
ProfilePictureURL: user.ProfilePictureURL,
|
ProfilePictureURL: user.ProfilePictureURL,
|
||||||
PreferredLanguage: user.PreferredLanguage,
|
PreferredLanguage: user.PreferredLanguage,
|
||||||
CreatedAt: user.CreatedAt,
|
CreatedAt: user.CreatedAt,
|
||||||
UpdatedAt: user.UpdatedAt,
|
UpdatedAt: user.UpdatedAt,
|
||||||
SubscriptionStatus: subscriptionStatus,
|
SubscriptionStatus: subscriptionStatus,
|
||||||
}
|
}
|
||||||
// Ensure birthday is included and formatted
|
// Ensure birthday is included and formatted
|
||||||
|
|
@ -1721,21 +1721,21 @@ func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}(),
|
}(),
|
||||||
PhoneNumber: user.PhoneNumber,
|
PhoneNumber: user.PhoneNumber,
|
||||||
Role: user.Role,
|
Role: user.Role,
|
||||||
AgeGroup: user.AgeGroup,
|
AgeGroup: user.AgeGroup,
|
||||||
EducationLevel: user.EducationLevel,
|
EducationLevel: user.EducationLevel,
|
||||||
Country: user.Country,
|
Country: user.Country,
|
||||||
Region: user.Region,
|
Region: user.Region,
|
||||||
EmailVerified: user.EmailVerified,
|
EmailVerified: user.EmailVerified,
|
||||||
PhoneVerified: user.PhoneVerified,
|
PhoneVerified: user.PhoneVerified,
|
||||||
Status: user.Status,
|
Status: user.Status,
|
||||||
LastLogin: lastLogin,
|
LastLogin: lastLogin,
|
||||||
ProfileCompleted: user.ProfileCompleted,
|
ProfileCompleted: user.ProfileCompleted,
|
||||||
ProfilePictureURL: user.ProfilePictureURL,
|
ProfilePictureURL: user.ProfilePictureURL,
|
||||||
PreferredLanguage: user.PreferredLanguage,
|
PreferredLanguage: user.PreferredLanguage,
|
||||||
CreatedAt: user.CreatedAt,
|
CreatedAt: user.CreatedAt,
|
||||||
UpdatedAt: user.UpdatedAt,
|
UpdatedAt: user.UpdatedAt,
|
||||||
SubscriptionStatus: subStatus,
|
SubscriptionStatus: subStatus,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -1828,22 +1828,22 @@ func (h *Handler) GetUserByID(c *fiber.Ctx) error {
|
||||||
Occupation: user.Occupation,
|
Occupation: user.Occupation,
|
||||||
FavouriteTopic: user.FavouriteTopic,
|
FavouriteTopic: user.FavouriteTopic,
|
||||||
// UserName: user.UserName,
|
// UserName: user.UserName,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
PhoneNumber: user.PhoneNumber,
|
PhoneNumber: user.PhoneNumber,
|
||||||
Role: user.Role,
|
Role: user.Role,
|
||||||
AgeGroup: user.AgeGroup,
|
AgeGroup: user.AgeGroup,
|
||||||
EducationLevel: user.EducationLevel,
|
EducationLevel: user.EducationLevel,
|
||||||
Country: user.Country,
|
Country: user.Country,
|
||||||
Region: user.Region,
|
Region: user.Region,
|
||||||
EmailVerified: user.EmailVerified,
|
EmailVerified: user.EmailVerified,
|
||||||
PhoneVerified: user.PhoneVerified,
|
PhoneVerified: user.PhoneVerified,
|
||||||
Status: user.Status,
|
Status: user.Status,
|
||||||
LastLogin: lastLogin,
|
LastLogin: lastLogin,
|
||||||
ProfileCompleted: user.ProfileCompleted,
|
ProfileCompleted: user.ProfileCompleted,
|
||||||
ProfilePictureURL: user.ProfilePictureURL,
|
ProfilePictureURL: user.ProfilePictureURL,
|
||||||
PreferredLanguage: user.PreferredLanguage,
|
PreferredLanguage: user.PreferredLanguage,
|
||||||
CreatedAt: user.CreatedAt,
|
CreatedAt: user.CreatedAt,
|
||||||
UpdatedAt: user.UpdatedAt,
|
UpdatedAt: user.UpdatedAt,
|
||||||
SubscriptionStatus: subscriptionStatus,
|
SubscriptionStatus: subscriptionStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1933,7 +1933,7 @@ func (h *Handler) DeleteMyUserAccount(c *fiber.Ctx) error {
|
||||||
if !ok {
|
if !ok {
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "Invalid authenticated role")
|
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")
|
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 {
|
if !ok {
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "Invalid authenticated role")
|
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")
|
return fiber.NewError(fiber.StatusForbidden, "Only learners can cancel their own account deletion using this endpoint")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,7 @@ func (a *App) RequireActiveSubscription() fiber.Handler {
|
||||||
switch role {
|
switch role {
|
||||||
case domain.RoleSuperAdmin, domain.RoleAdmin, domain.RoleInstructor, domain.RoleSupport:
|
case domain.RoleSuperAdmin, domain.RoleAdmin, domain.RoleInstructor, domain.RoleSupport:
|
||||||
return c.Next()
|
return c.Next()
|
||||||
case domain.RoleStudent:
|
case domain.RoleStudent, domain.RoleOpenLearner:
|
||||||
userID, ok := c.Locals("user_id").(int64)
|
userID, ok := c.Locals("user_id").(int64)
|
||||||
if !ok || userID == 0 {
|
if !ok || userID == 0 {
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "Unauthorized")
|
return fiber.NewError(fiber.StatusUnauthorized, "Unauthorized")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user