Compare commits
No commits in common. "05cb8715f96e5a2aab2eb0e1cfedbbbfe6db024e" and "b06b8645cfb808f8cad7c9a47f42b45a66881845" have entirely different histories.
05cb8715f9
...
b06b8645cf
|
|
@ -460,7 +460,7 @@ VALUES
|
|||
'Administrative staff managing day-to-day operations.',
|
||||
'active',
|
||||
TRUE,
|
||||
'[*]'::jsonb,
|
||||
'["users.manage", "courses.manage", "settings.manage"]'::jsonb,
|
||||
CURRENT_TIMESTAMP
|
||||
),
|
||||
(
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
ALTER TABLE question_sets
|
||||
DROP COLUMN IF EXISTS intro_video_url;
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
ALTER TABLE question_sets
|
||||
ADD COLUMN intro_video_url TEXT;
|
||||
|
|
@ -44,7 +44,7 @@ WHERE sub_course_id = $1 AND status = 'PUBLISHED'
|
|||
ORDER BY display_order, id;
|
||||
|
||||
-- name: GetSubCoursePracticesForLearningPath :many
|
||||
SELECT id, title, description, persona, status, intro_video_url,
|
||||
SELECT id, title, description, persona, status,
|
||||
(SELECT COUNT(*) FROM question_set_items WHERE set_id = qs.id) AS question_count
|
||||
FROM question_sets qs
|
||||
WHERE qs.owner_type = 'SUB_COURSE' AND qs.owner_id = $1
|
||||
|
|
|
|||
|
|
@ -11,10 +11,9 @@ INSERT INTO question_sets (
|
|||
passing_score,
|
||||
shuffle_questions,
|
||||
status,
|
||||
sub_course_video_id,
|
||||
intro_video_url
|
||||
sub_course_video_id
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, COALESCE($10, false), COALESCE($11, 'DRAFT'), $12, $13)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, COALESCE($10, false), COALESCE($11, 'DRAFT'), $12)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetQuestionSetByID :one
|
||||
|
|
@ -60,10 +59,9 @@ SET
|
|||
passing_score = COALESCE($6, passing_score),
|
||||
shuffle_questions = COALESCE($7, shuffle_questions),
|
||||
status = COALESCE($8, status),
|
||||
intro_video_url = COALESCE($9, intro_video_url),
|
||||
sub_course_video_id = COALESCE($10, sub_course_video_id),
|
||||
sub_course_video_id = COALESCE($9, sub_course_video_id),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $11;
|
||||
WHERE id = $10;
|
||||
|
||||
-- name: ArchiveQuestionSet :exec
|
||||
UPDATE question_sets
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ func (q *Queries) GetFullLearningTree(ctx context.Context) ([]GetFullLearningTre
|
|||
}
|
||||
|
||||
const GetSubCoursePracticesForLearningPath = `-- name: GetSubCoursePracticesForLearningPath :many
|
||||
SELECT id, title, description, persona, status, intro_video_url,
|
||||
SELECT id, title, description, persona, status,
|
||||
(SELECT COUNT(*) FROM question_set_items WHERE set_id = qs.id) AS question_count
|
||||
FROM question_sets qs
|
||||
WHERE qs.owner_type = 'SUB_COURSE' AND qs.owner_id = $1
|
||||
|
|
@ -160,7 +160,6 @@ type GetSubCoursePracticesForLearningPathRow struct {
|
|||
Description pgtype.Text `json:"description"`
|
||||
Persona pgtype.Text `json:"persona"`
|
||||
Status string `json:"status"`
|
||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||
QuestionCount int64 `json:"question_count"`
|
||||
}
|
||||
|
||||
|
|
@ -179,7 +178,6 @@ func (q *Queries) GetSubCoursePracticesForLearningPath(ctx context.Context, owne
|
|||
&i.Description,
|
||||
&i.Persona,
|
||||
&i.Status,
|
||||
&i.IntroVideoUrl,
|
||||
&i.QuestionCount,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -173,7 +173,6 @@ type QuestionSet struct {
|
|||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||
}
|
||||
|
||||
type QuestionSetItem struct {
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ func (q *Queries) GetQuestionSetItems(ctx context.Context, setID int64) ([]GetQu
|
|||
}
|
||||
|
||||
const GetQuestionSetsContainingQuestion = `-- name: GetQuestionSetsContainingQuestion :many
|
||||
SELECT qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.sub_course_video_id, qs.display_order, qs.intro_video_url
|
||||
SELECT qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.sub_course_video_id, qs.display_order
|
||||
FROM question_sets qs
|
||||
JOIN question_set_items qsi ON qsi.set_id = qs.id
|
||||
WHERE qsi.question_id = $1
|
||||
|
|
@ -231,7 +231,6 @@ func (q *Queries) GetQuestionSetsContainingQuestion(ctx context.Context, questio
|
|||
&i.UpdatedAt,
|
||||
&i.SubCourseVideoID,
|
||||
&i.DisplayOrder,
|
||||
&i.IntroVideoUrl,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,11 +64,10 @@ INSERT INTO question_sets (
|
|||
passing_score,
|
||||
shuffle_questions,
|
||||
status,
|
||||
sub_course_video_id,
|
||||
intro_video_url
|
||||
sub_course_video_id
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, COALESCE($10, false), COALESCE($11, 'DRAFT'), $12, $13)
|
||||
RETURNING id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order, intro_video_url
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, COALESCE($10, false), COALESCE($11, 'DRAFT'), $12)
|
||||
RETURNING id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order
|
||||
`
|
||||
|
||||
type CreateQuestionSetParams struct {
|
||||
|
|
@ -84,7 +83,6 @@ type CreateQuestionSetParams struct {
|
|||
Column10 interface{} `json:"column_10"`
|
||||
Column11 interface{} `json:"column_11"`
|
||||
SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"`
|
||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateQuestionSet(ctx context.Context, arg CreateQuestionSetParams) (QuestionSet, error) {
|
||||
|
|
@ -101,7 +99,6 @@ func (q *Queries) CreateQuestionSet(ctx context.Context, arg CreateQuestionSetPa
|
|||
arg.Column10,
|
||||
arg.Column11,
|
||||
arg.SubCourseVideoID,
|
||||
arg.IntroVideoUrl,
|
||||
)
|
||||
var i QuestionSet
|
||||
err := row.Scan(
|
||||
|
|
@ -121,7 +118,6 @@ func (q *Queries) CreateQuestionSet(ctx context.Context, arg CreateQuestionSetPa
|
|||
&i.UpdatedAt,
|
||||
&i.SubCourseVideoID,
|
||||
&i.DisplayOrder,
|
||||
&i.IntroVideoUrl,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -137,7 +133,7 @@ func (q *Queries) DeleteQuestionSet(ctx context.Context, id int64) error {
|
|||
}
|
||||
|
||||
const GetInitialAssessmentSet = `-- name: GetInitialAssessmentSet :one
|
||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order, intro_video_url
|
||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order
|
||||
FROM question_sets
|
||||
WHERE set_type = 'INITIAL_ASSESSMENT'
|
||||
AND status = 'PUBLISHED'
|
||||
|
|
@ -165,13 +161,12 @@ func (q *Queries) GetInitialAssessmentSet(ctx context.Context) (QuestionSet, err
|
|||
&i.UpdatedAt,
|
||||
&i.SubCourseVideoID,
|
||||
&i.DisplayOrder,
|
||||
&i.IntroVideoUrl,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetPublishedQuestionSetsByOwner = `-- name: GetPublishedQuestionSetsByOwner :many
|
||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order, intro_video_url
|
||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order
|
||||
FROM question_sets
|
||||
WHERE owner_type = $1
|
||||
AND owner_id = $2
|
||||
|
|
@ -210,7 +205,6 @@ func (q *Queries) GetPublishedQuestionSetsByOwner(ctx context.Context, arg GetPu
|
|||
&i.UpdatedAt,
|
||||
&i.SubCourseVideoID,
|
||||
&i.DisplayOrder,
|
||||
&i.IntroVideoUrl,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -223,7 +217,7 @@ func (q *Queries) GetPublishedQuestionSetsByOwner(ctx context.Context, arg GetPu
|
|||
}
|
||||
|
||||
const GetQuestionSetByID = `-- name: GetQuestionSetByID :one
|
||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order, intro_video_url
|
||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order
|
||||
FROM question_sets
|
||||
WHERE id = $1
|
||||
`
|
||||
|
|
@ -248,13 +242,12 @@ func (q *Queries) GetQuestionSetByID(ctx context.Context, id int64) (QuestionSet
|
|||
&i.UpdatedAt,
|
||||
&i.SubCourseVideoID,
|
||||
&i.DisplayOrder,
|
||||
&i.IntroVideoUrl,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetQuestionSetsByOwner = `-- name: GetQuestionSetsByOwner :many
|
||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order, intro_video_url
|
||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order
|
||||
FROM question_sets
|
||||
WHERE owner_type = $1
|
||||
AND owner_id = $2
|
||||
|
|
@ -293,7 +286,6 @@ func (q *Queries) GetQuestionSetsByOwner(ctx context.Context, arg GetQuestionSet
|
|||
&i.UpdatedAt,
|
||||
&i.SubCourseVideoID,
|
||||
&i.DisplayOrder,
|
||||
&i.IntroVideoUrl,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -308,7 +300,7 @@ func (q *Queries) GetQuestionSetsByOwner(ctx context.Context, arg GetQuestionSet
|
|||
const GetQuestionSetsByType = `-- name: GetQuestionSetsByType :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.sub_course_video_id, qs.display_order, qs.intro_video_url
|
||||
qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.sub_course_video_id, qs.display_order
|
||||
FROM question_sets qs
|
||||
WHERE set_type = $1
|
||||
AND status != 'ARCHIVED'
|
||||
|
|
@ -341,7 +333,6 @@ type GetQuestionSetsByTypeRow struct {
|
|||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"`
|
||||
DisplayOrder int32 `json:"display_order"`
|
||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetQuestionSetsByType(ctx context.Context, arg GetQuestionSetsByTypeParams) ([]GetQuestionSetsByTypeRow, error) {
|
||||
|
|
@ -371,7 +362,6 @@ func (q *Queries) GetQuestionSetsByType(ctx context.Context, arg GetQuestionSets
|
|||
&i.UpdatedAt,
|
||||
&i.SubCourseVideoID,
|
||||
&i.DisplayOrder,
|
||||
&i.IntroVideoUrl,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -384,7 +374,7 @@ func (q *Queries) GetQuestionSetsByType(ctx context.Context, arg GetQuestionSets
|
|||
}
|
||||
|
||||
const GetSubCourseInitialAssessmentSet = `-- name: GetSubCourseInitialAssessmentSet :one
|
||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order, intro_video_url
|
||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order
|
||||
FROM question_sets
|
||||
WHERE set_type = 'INITIAL_ASSESSMENT'
|
||||
AND owner_type = 'SUB_COURSE'
|
||||
|
|
@ -414,7 +404,6 @@ func (q *Queries) GetSubCourseInitialAssessmentSet(ctx context.Context, ownerID
|
|||
&i.UpdatedAt,
|
||||
&i.SubCourseVideoID,
|
||||
&i.DisplayOrder,
|
||||
&i.IntroVideoUrl,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -518,10 +507,9 @@ SET
|
|||
passing_score = COALESCE($6, passing_score),
|
||||
shuffle_questions = COALESCE($7, shuffle_questions),
|
||||
status = COALESCE($8, status),
|
||||
intro_video_url = COALESCE($9, intro_video_url),
|
||||
sub_course_video_id = COALESCE($10, sub_course_video_id),
|
||||
sub_course_video_id = COALESCE($9, sub_course_video_id),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $11
|
||||
WHERE id = $10
|
||||
`
|
||||
|
||||
type UpdateQuestionSetParams struct {
|
||||
|
|
@ -533,7 +521,6 @@ type UpdateQuestionSetParams struct {
|
|||
PassingScore pgtype.Int4 `json:"passing_score"`
|
||||
ShuffleQuestions bool `json:"shuffle_questions"`
|
||||
Status string `json:"status"`
|
||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||
SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
|
@ -548,7 +535,6 @@ func (q *Queries) UpdateQuestionSet(ctx context.Context, arg UpdateQuestionSetPa
|
|||
arg.PassingScore,
|
||||
arg.ShuffleQuestions,
|
||||
arg.Status,
|
||||
arg.IntroVideoUrl,
|
||||
arg.SubCourseVideoID,
|
||||
arg.ID,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
package dbgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
// ExecRaw executes raw SQL against the underlying sqlc DB connection.
|
||||
// Use this sparingly for operational tasks that are not modelled by sqlc queries.
|
||||
func (q *Queries) ExecRaw(ctx context.Context, sql string, args ...interface{}) (pgconn.CommandTag, error) {
|
||||
return q.db.Exec(ctx, sql, args...)
|
||||
}
|
||||
|
|
@ -140,9 +140,6 @@ type Config struct {
|
|||
AccountDeletionPurgeEnabled bool
|
||||
AccountDeletionPurgeInterval time.Duration
|
||||
AccountDeletionPurgeBatchSize int32
|
||||
DBResetReseedEnabled bool
|
||||
DBResetReseedToken string
|
||||
DBSeedDir string
|
||||
}
|
||||
|
||||
func NewConfig() (*Config, error) {
|
||||
|
|
@ -494,36 +491,10 @@ func (c *Config) loadEnv() error {
|
|||
c.MinIO.Enabled = true
|
||||
}
|
||||
c.MinIO.Endpoint = os.Getenv("MINIO_ENDPOINT")
|
||||
// New env var names (preferred)
|
||||
// - MINIO_ROOT_USER
|
||||
// - MINIO_ROOT_PASSWORD
|
||||
// - MINIO_BUCKET_NAME
|
||||
//
|
||||
// Backward compatible fallbacks:
|
||||
// - MINIO_ACCESS_KEY / MINIO_SECRET_KEY / MINIO_BUCKET
|
||||
rootUser := os.Getenv("MINIO_ROOT_USER")
|
||||
rootPass := os.Getenv("MINIO_ROOT_PASSWORD")
|
||||
bucketName := os.Getenv("MINIO_BUCKET_NAME")
|
||||
|
||||
if rootUser != "" {
|
||||
c.MinIO.AccessKey = rootUser
|
||||
} else {
|
||||
c.MinIO.AccessKey = os.Getenv("MINIO_ACCESS_KEY")
|
||||
}
|
||||
|
||||
if rootPass != "" {
|
||||
c.MinIO.SecretKey = rootPass
|
||||
} else {
|
||||
c.MinIO.SecretKey = os.Getenv("MINIO_SECRET_KEY")
|
||||
}
|
||||
|
||||
if bucketName != "" {
|
||||
c.MinIO.Bucket = bucketName
|
||||
} else {
|
||||
c.MinIO.Bucket = os.Getenv("MINIO_BUCKET")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(c.MinIO.Bucket) == "" {
|
||||
c.MinIO.AccessKey = os.Getenv("MINIO_ACCESS_KEY")
|
||||
c.MinIO.SecretKey = os.Getenv("MINIO_SECRET_KEY")
|
||||
c.MinIO.Bucket = os.Getenv("MINIO_BUCKET")
|
||||
if c.MinIO.Bucket == "" {
|
||||
c.MinIO.Bucket = "yimaru"
|
||||
}
|
||||
minioUseSSL := os.Getenv("MINIO_USE_SSL")
|
||||
|
|
@ -565,13 +536,6 @@ func (c *Config) loadEnv() error {
|
|||
}
|
||||
}
|
||||
|
||||
// Dangerous DB reset+reseed endpoint configuration
|
||||
// Enabled by default and does not require .env variables.
|
||||
// Optional token can still be set programmatically if needed.
|
||||
c.DBResetReseedEnabled = true
|
||||
c.DBResetReseedToken = ""
|
||||
c.DBSeedDir = "db/data"
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -130,7 +130,6 @@ type LearningPathPractice struct {
|
|||
Description *string `json:"description,omitempty"`
|
||||
Persona *string `json:"persona,omitempty"`
|
||||
Status string `json:"status"`
|
||||
IntroVideoURL *string `json:"intro_video_url,omitempty"`
|
||||
QuestionCount int64 `json:"question_count"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -104,7 +104,6 @@ type QuestionSet struct {
|
|||
ShuffleQuestions bool
|
||||
Status string
|
||||
SubCourseVideoID *int64
|
||||
IntroVideoURL *string
|
||||
UserPersonas []UserPersona
|
||||
CreatedAt time.Time
|
||||
UpdatedAt *time.Time
|
||||
|
|
@ -171,7 +170,6 @@ type CreateQuestionSetInput struct {
|
|||
ShuffleQuestions *bool
|
||||
Status *string
|
||||
SubCourseVideoID *int64
|
||||
IntroVideoURL *string
|
||||
}
|
||||
|
||||
// UserPersona represents a user acting as a persona in a practice session
|
||||
|
|
|
|||
|
|
@ -154,7 +154,6 @@ func (s *Store) getSubCoursePracticesForPath(ctx context.Context, subCourseID in
|
|||
Description: ptrString(row.Description),
|
||||
Persona: ptrString(row.Persona),
|
||||
Status: row.Status,
|
||||
IntroVideoURL: ptrString(row.IntroVideoUrl),
|
||||
QuestionCount: row.QuestionCount,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,7 +124,6 @@ func questionSetToDomain(qs dbgen.QuestionSet) domain.QuestionSet {
|
|||
ShuffleQuestions: qs.ShuffleQuestions,
|
||||
Status: qs.Status,
|
||||
SubCourseVideoID: fromPgInt8(qs.SubCourseVideoID),
|
||||
IntroVideoURL: fromPgText(qs.IntroVideoUrl),
|
||||
CreatedAt: qs.CreatedAt.Time,
|
||||
UpdatedAt: timePtr(qs.UpdatedAt),
|
||||
}
|
||||
|
|
@ -543,7 +542,6 @@ func (s *Store) CreateQuestionSet(ctx context.Context, input domain.CreateQuesti
|
|||
Column10: shuffleQuestions,
|
||||
Column11: status,
|
||||
SubCourseVideoID: toPgInt8(input.SubCourseVideoID),
|
||||
IntroVideoUrl: toPgText(input.IntroVideoURL),
|
||||
})
|
||||
if err != nil {
|
||||
return domain.QuestionSet{}, err
|
||||
|
|
@ -605,7 +603,6 @@ func (s *Store) GetQuestionSetsByType(ctx context.Context, setType string, limit
|
|||
ShuffleQuestions: r.ShuffleQuestions,
|
||||
Status: r.Status,
|
||||
SubCourseVideoID: fromPgInt8(r.SubCourseVideoID),
|
||||
IntroVideoURL: fromPgText(r.IntroVideoUrl),
|
||||
CreatedAt: r.CreatedAt.Time,
|
||||
UpdatedAt: timePtr(r.UpdatedAt),
|
||||
}
|
||||
|
|
@ -691,7 +688,6 @@ func (s *Store) UpdateQuestionSet(ctx context.Context, id int64, input domain.Cr
|
|||
PassingScore: toPgInt4(input.PassingScore),
|
||||
ShuffleQuestions: shuffleQuestions,
|
||||
Status: status,
|
||||
IntroVideoUrl: toPgText(input.IntroVideoURL),
|
||||
SubCourseVideoID: toPgInt8(input.SubCourseVideoID),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -223,9 +223,6 @@ var AllPermissions = []domain.PermissionSeed{
|
|||
{Key: "rbac.permissions.list", Name: "List Permissions", Description: "List all permissions", GroupName: "RBAC"},
|
||||
{Key: "rbac.permissions.groups", Name: "List Permission Groups", Description: "List permission groups", GroupName: "RBAC"},
|
||||
{Key: "rbac.permissions.sync", Name: "Sync Permissions", Description: "Sync permissions from code", GroupName: "RBAC"},
|
||||
|
||||
// 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"},
|
||||
}
|
||||
|
||||
// DefaultRolePermissions maps each system role to the permission keys it should
|
||||
|
|
@ -309,9 +306,6 @@ var DefaultRolePermissions = map[string][]string{
|
|||
"rbac.roles.list", "rbac.roles.get", "rbac.roles.create", "rbac.roles.update", "rbac.roles.delete",
|
||||
"rbac.roles.set_permissions", "rbac.roles.get_permissions",
|
||||
"rbac.permissions.list", "rbac.permissions.groups", "rbac.permissions.sync",
|
||||
|
||||
// Internal operations
|
||||
"internal.db.reset_reseed",
|
||||
},
|
||||
|
||||
"STUDENT": {
|
||||
|
|
|
|||
|
|
@ -156,28 +156,6 @@ func (h *Handler) UploadMedia(c *fiber.Ctx) error {
|
|||
"audio/aac": true, "audio/webm": true, "video/ogg": true, "video/webm": true,
|
||||
"audio/x-wav": true, "audio/x-m4a": true, "audio/flac": true,
|
||||
}
|
||||
// DetectContentType can return "application/octet-stream" for some files (often via clients
|
||||
// like Postman). If that happens, fall back to extension-based detection.
|
||||
if contentType == "application/octet-stream" {
|
||||
filenameLower := strings.ToLower(fileHeader.Filename)
|
||||
lastDot := strings.LastIndex(filenameLower, ".")
|
||||
ext := ""
|
||||
if lastDot != -1 && lastDot+1 < len(filenameLower) {
|
||||
ext = filenameLower[lastDot+1:]
|
||||
}
|
||||
extMap := map[string]string{
|
||||
"mp3": "audio/mpeg",
|
||||
"wav": "audio/wav",
|
||||
"ogg": "audio/ogg",
|
||||
"m4a": "audio/mp4",
|
||||
"aac": "audio/aac",
|
||||
"webm": "audio/webm",
|
||||
"flac": "audio/flac",
|
||||
}
|
||||
if ct, ok := extMap[ext]; ok {
|
||||
contentType = ct
|
||||
}
|
||||
}
|
||||
if !allowedAudio[contentType] {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid file type",
|
||||
|
|
|
|||
|
|
@ -1,265 +0,0 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"crypto/subtle"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type resetAndReseedReq struct {
|
||||
Confirm string `json:"confirm"`
|
||||
}
|
||||
|
||||
type clearCourseManagementReq struct {
|
||||
Confirm string `json:"confirm"`
|
||||
}
|
||||
|
||||
func extractInsertStatement(sqlContent string, tableName string) (string, bool) {
|
||||
pattern := fmt.Sprintf(`(?is)INSERT\s+INTO\s+%s\b.*?;`, regexp.QuoteMeta(tableName))
|
||||
re := regexp.MustCompile(pattern)
|
||||
statement := strings.TrimSpace(re.FindString(sqlContent))
|
||||
if statement == "" {
|
||||
return "", false
|
||||
}
|
||||
return statement, true
|
||||
}
|
||||
|
||||
func resolveSeedDir(seedDir string) (string, error) {
|
||||
cleanSeedDir := strings.TrimSpace(seedDir)
|
||||
if cleanSeedDir == "" {
|
||||
cleanSeedDir = "db/data"
|
||||
}
|
||||
|
||||
// If absolute, use directly.
|
||||
if filepath.IsAbs(cleanSeedDir) {
|
||||
info, err := os.Stat(cleanSeedDir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return "", fmt.Errorf("seed dir is not a directory: %s", cleanSeedDir)
|
||||
}
|
||||
return cleanSeedDir, nil
|
||||
}
|
||||
|
||||
candidates := make([]string, 0, 5)
|
||||
|
||||
// 1) Relative to current working directory.
|
||||
candidates = append(candidates, filepath.Clean(cleanSeedDir))
|
||||
|
||||
// 2) Relative to executable directory (and parents).
|
||||
if exePath, err := os.Executable(); err == nil {
|
||||
exeDir := filepath.Dir(exePath)
|
||||
candidates = append(candidates,
|
||||
filepath.Join(exeDir, cleanSeedDir),
|
||||
filepath.Join(exeDir, "..", cleanSeedDir),
|
||||
filepath.Join(exeDir, "..", "..", cleanSeedDir),
|
||||
)
|
||||
}
|
||||
|
||||
for _, candidate := range candidates {
|
||||
info, err := os.Stat(candidate)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if info.IsDir() {
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("seed directory not found (tried: %s)", strings.Join(candidates, ", "))
|
||||
}
|
||||
|
||||
// ResetAndReseedDatabase godoc
|
||||
// @Summary Reset and reseed database
|
||||
// @Description Dangerous operation: clears and reseeds only course_categories, courses, and sub_courses from seed SQL files.
|
||||
// @Tags internal
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Seed-Reset-Token header string true "Reset token configured in DB_RESET_RESEED_TOKEN"
|
||||
// @Param body body resetAndReseedReq true "Confirmation payload"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 403 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/internal/db/reset-reseed [post]
|
||||
func (h *Handler) ResetAndReseedDatabase(c *fiber.Ctx) error {
|
||||
if h.Cfg == nil || !h.Cfg.DBResetReseedEnabled {
|
||||
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
||||
Message: "Operation is disabled",
|
||||
Error: "DB_RESET_RESEED_ENABLED must be set to true",
|
||||
})
|
||||
}
|
||||
|
||||
var req resetAndReseedReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid request body",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if strings.TrimSpace(req.Confirm) != "RESET_AND_RESEED" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Confirmation required",
|
||||
Error: `set confirm to "RESET_AND_RESEED"`,
|
||||
})
|
||||
}
|
||||
|
||||
expectedToken := strings.TrimSpace(h.Cfg.DBResetReseedToken)
|
||||
if expectedToken != "" {
|
||||
providedToken := strings.TrimSpace(c.Get("X-Seed-Reset-Token"))
|
||||
if subtle.ConstantTimeCompare([]byte(providedToken), []byte(expectedToken)) != 1 {
|
||||
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid reset token",
|
||||
Error: "missing or invalid X-Seed-Reset-Token",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
seedDir, err := resolveSeedDir(h.Cfg.DBSeedDir)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to resolve seed directory",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
seedCandidates := []string{
|
||||
filepath.Join(seedDir, "007_course_management_seed.sql"),
|
||||
filepath.Join(seedDir, "001_initial_seed_data.sql"),
|
||||
}
|
||||
tableNames := []string{"course_categories", "courses", "sub_courses"}
|
||||
statements := map[string]string{}
|
||||
statementSource := map[string]string{}
|
||||
|
||||
for _, file := range seedCandidates {
|
||||
contentBytes, readErr := os.ReadFile(file)
|
||||
if readErr != nil {
|
||||
continue
|
||||
}
|
||||
content := string(contentBytes)
|
||||
for _, tableName := range tableNames {
|
||||
if _, exists := statements[tableName]; exists {
|
||||
continue
|
||||
}
|
||||
if stmt, ok := extractInsertStatement(content, tableName); ok {
|
||||
statements[tableName] = stmt
|
||||
statementSource[tableName] = file
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, tableName := range tableNames {
|
||||
if _, ok := statements[tableName]; !ok {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Missing required seed statement",
|
||||
Error: fmt.Sprintf("could not find INSERT INTO %s in seed files", tableName),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var sqlBuilder strings.Builder
|
||||
sqlBuilder.WriteString("BEGIN;\n")
|
||||
sqlBuilder.WriteString("TRUNCATE TABLE sub_courses, courses, course_categories RESTART IDENTITY CASCADE;\n")
|
||||
for _, tableName := range tableNames {
|
||||
sqlBuilder.WriteString("\n-- ")
|
||||
sqlBuilder.WriteString(tableName)
|
||||
sqlBuilder.WriteString(" from ")
|
||||
sqlBuilder.WriteString(statementSource[tableName])
|
||||
sqlBuilder.WriteString("\n")
|
||||
sqlBuilder.WriteString(statements[tableName])
|
||||
sqlBuilder.WriteString("\n")
|
||||
}
|
||||
sqlBuilder.WriteString("COMMIT;")
|
||||
|
||||
if _, err := h.analyticsDB.ExecRaw(c.Context(), sqlBuilder.String()); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to reset and reseed database",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Course management hierarchy reset and reseed completed successfully",
|
||||
Data: map[string]interface{}{
|
||||
"seed_dir": seedDir,
|
||||
"tables": tableNames,
|
||||
"sources": statementSource,
|
||||
},
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// ClearCourseManagementData godoc
|
||||
// @Summary Clear course management hierarchy data only
|
||||
// @Description Truncates course_categories, courses, and sub_courses (same scope as reset-reseed) without re-inserting seed SQL.
|
||||
// @Tags internal
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Seed-Reset-Token header string false "Optional token when DB_RESET_RESEED_TOKEN is set"
|
||||
// @Param body body clearCourseManagementReq true "Confirmation payload"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 403 {object} domain.ErrorResponse
|
||||
// @Failure 500 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/internal/db/clear-course-management [post]
|
||||
func (h *Handler) ClearCourseManagementData(c *fiber.Ctx) error {
|
||||
if h.Cfg == nil || !h.Cfg.DBResetReseedEnabled {
|
||||
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
||||
Message: "Operation is disabled",
|
||||
Error: "internal course management maintenance is disabled",
|
||||
})
|
||||
}
|
||||
|
||||
var req clearCourseManagementReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid request body",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if strings.TrimSpace(req.Confirm) != "CLEAR_COURSE_MANAGEMENT" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Confirmation required",
|
||||
Error: `set confirm to "CLEAR_COURSE_MANAGEMENT"`,
|
||||
})
|
||||
}
|
||||
|
||||
expectedToken := strings.TrimSpace(h.Cfg.DBResetReseedToken)
|
||||
if expectedToken != "" {
|
||||
providedToken := strings.TrimSpace(c.Get("X-Seed-Reset-Token"))
|
||||
if subtle.ConstantTimeCompare([]byte(providedToken), []byte(expectedToken)) != 1 {
|
||||
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid reset token",
|
||||
Error: "missing or invalid X-Seed-Reset-Token",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
tableNames := []string{"course_categories", "courses", "sub_courses"}
|
||||
sql := `BEGIN;
|
||||
TRUNCATE TABLE sub_courses, courses, course_categories RESTART IDENTITY CASCADE;
|
||||
COMMIT;`
|
||||
|
||||
if _, err := h.analyticsDB.ExecRaw(c.Context(), sql); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to clear course management data",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Course management hierarchy cleared successfully (no re-seed)",
|
||||
Data: map[string]interface{}{
|
||||
"tables": tableNames,
|
||||
},
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
|
@ -525,7 +525,6 @@ type createQuestionSetReq struct {
|
|||
ShuffleQuestions *bool `json:"shuffle_questions"`
|
||||
Status *string `json:"status"`
|
||||
SubCourseVideoID *int64 `json:"sub_course_video_id"`
|
||||
IntroVideoURL *string `json:"intro_video_url"`
|
||||
}
|
||||
|
||||
type questionSetRes struct {
|
||||
|
|
@ -542,7 +541,6 @@ type questionSetRes struct {
|
|||
ShuffleQuestions bool `json:"shuffle_questions"`
|
||||
Status string `json:"status"`
|
||||
SubCourseVideoID *int64 `json:"sub_course_video_id,omitempty"`
|
||||
IntroVideoURL *string `json:"intro_video_url,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
QuestionCount *int64 `json:"question_count,omitempty"`
|
||||
}
|
||||
|
|
@ -628,7 +626,6 @@ func (h *Handler) CreateQuestionSet(c *fiber.Ctx) error {
|
|||
ShuffleQuestions: req.ShuffleQuestions,
|
||||
Status: req.Status,
|
||||
SubCourseVideoID: req.SubCourseVideoID,
|
||||
IntroVideoURL: req.IntroVideoURL,
|
||||
}
|
||||
|
||||
set, err := h.questionsSvc.CreateQuestionSet(c.Context(), input)
|
||||
|
|
@ -662,7 +659,6 @@ func (h *Handler) CreateQuestionSet(c *fiber.Ctx) error {
|
|||
ShuffleQuestions: set.ShuffleQuestions,
|
||||
Status: set.Status,
|
||||
SubCourseVideoID: set.SubCourseVideoID,
|
||||
IntroVideoURL: set.IntroVideoURL,
|
||||
CreatedAt: set.CreatedAt.String(),
|
||||
},
|
||||
})
|
||||
|
|
@ -715,7 +711,6 @@ func (h *Handler) GetSubCourseEntryAssessmentSet(c *fiber.Ctx) error {
|
|||
ShuffleQuestions: set.ShuffleQuestions,
|
||||
Status: set.Status,
|
||||
SubCourseVideoID: set.SubCourseVideoID,
|
||||
IntroVideoURL: set.IntroVideoURL,
|
||||
CreatedAt: set.CreatedAt.String(),
|
||||
QuestionCount: &count,
|
||||
},
|
||||
|
|
@ -779,7 +774,6 @@ func (h *Handler) GetQuestionSetByID(c *fiber.Ctx) error {
|
|||
ShuffleQuestions: set.ShuffleQuestions,
|
||||
Status: set.Status,
|
||||
SubCourseVideoID: set.SubCourseVideoID,
|
||||
IntroVideoURL: set.IntroVideoURL,
|
||||
CreatedAt: set.CreatedAt.String(),
|
||||
QuestionCount: &count,
|
||||
},
|
||||
|
|
@ -835,7 +829,6 @@ func (h *Handler) GetQuestionSetsByType(c *fiber.Ctx) error {
|
|||
ShuffleQuestions: s.ShuffleQuestions,
|
||||
Status: s.Status,
|
||||
SubCourseVideoID: s.SubCourseVideoID,
|
||||
IntroVideoURL: s.IntroVideoURL,
|
||||
CreatedAt: s.CreatedAt.String(),
|
||||
})
|
||||
}
|
||||
|
|
@ -902,7 +895,6 @@ func (h *Handler) GetQuestionSetsByOwner(c *fiber.Ctx) error {
|
|||
ShuffleQuestions: s.ShuffleQuestions,
|
||||
Status: s.Status,
|
||||
SubCourseVideoID: s.SubCourseVideoID,
|
||||
IntroVideoURL: s.IntroVideoURL,
|
||||
CreatedAt: s.CreatedAt.String(),
|
||||
})
|
||||
}
|
||||
|
|
@ -923,7 +915,6 @@ type updateQuestionSetReq struct {
|
|||
ShuffleQuestions *bool `json:"shuffle_questions"`
|
||||
Status *string `json:"status"`
|
||||
SubCourseVideoID *int64 `json:"sub_course_video_id"`
|
||||
IntroVideoURL *string `json:"intro_video_url"`
|
||||
}
|
||||
|
||||
// UpdateQuestionSet godoc
|
||||
|
|
@ -971,7 +962,6 @@ func (h *Handler) UpdateQuestionSet(c *fiber.Ctx) error {
|
|||
ShuffleQuestions: req.ShuffleQuestions,
|
||||
Status: req.Status,
|
||||
SubCourseVideoID: req.SubCourseVideoID,
|
||||
IntroVideoURL: req.IntroVideoURL,
|
||||
}
|
||||
|
||||
err = h.questionsSvc.UpdateQuestionSet(c.Context(), id, input)
|
||||
|
|
|
|||
|
|
@ -244,8 +244,6 @@ func (a *App) initAppRoutes() {
|
|||
groupV1.Delete("/user/me", a.authMiddleware, a.RequirePermission("users.delete_self"), h.DeleteMyUserAccount)
|
||||
groupV1.Post("/user/me/deletion/cancel", a.authMiddleware, a.RequirePermission("users.cancel_delete_self"), h.CancelMyUserAccountDeletion)
|
||||
groupV1.Post("/internal/users/purge-due-deletions", a.authMiddleware, a.RequirePermission("users.purge_due_deletions"), h.PurgeDueDeletedUsers)
|
||||
groupV1.Post("/internal/db/reset-reseed", a.authMiddleware, a.RequirePermission("internal.db.reset_reseed"), h.ResetAndReseedDatabase)
|
||||
groupV1.Post("/internal/db/clear-course-management", a.authMiddleware, a.RequirePermission("internal.db.reset_reseed"), h.ClearCourseManagementData)
|
||||
groupV1.Get("/user/single/:id", a.authMiddleware, a.RequirePermission("users.get"), h.GetUserByID)
|
||||
groupV1.Delete("/user/delete/:id", a.authMiddleware, a.RequirePermission("users.delete"), h.DeleteUser)
|
||||
groupV1.Post("/user/search", a.authMiddleware, a.RequirePermission("users.search"), h.SearchUserByNameOrPhone)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user