Skip to main content

handlers

import "github.com/Ask-Atlas/AskAtlas/api/internal/handlers"

Package handlers contains the HTTP handlers and routes for the API endpoints.

Index

type ClerkHandler

ClerkHandler manages incoming HTTP webhook requests from Clerk.

type ClerkHandler struct {
// contains filtered or unexported fields
}

func NewClerkWebhookHandler

func NewClerkWebhookHandler(clerkService ClerkService) *ClerkHandler

NewClerkWebhookHandler creates a new ClerkHandler with the given service.

func (*ClerkHandler) Webhook

func (ch *ClerkHandler) Webhook(w http.ResponseWriter, r *http.Request)

Webhook processes the incoming Clerk webhook request, parsing and dispatching the event.

type ClerkService

ClerkService defines the interface for processing Clerk webhook payloads.

type ClerkService interface {
HandleWebhookEvent(ctx context.Context, payload clerk.Event) error
}

type CompositeHandler

CompositeHandler combines multiple handler structs into a single value that satisfies the generated api.ServerInterface.

type CompositeHandler struct {
*FileHandler
*GrantHandler
*SchoolsHandler
*CoursesHandler
*StudyGuideHandler
*QuizzesHandler
}

func NewCompositeHandler

func NewCompositeHandler(fh *FileHandler, gh *GrantHandler, sh *SchoolsHandler, ch *CoursesHandler, sgh *StudyGuideHandler, qh *QuizzesHandler) *CompositeHandler

NewCompositeHandler creates a handler that delegates to FileHandler, GrantHandler, SchoolsHandler, CoursesHandler, StudyGuideHandler, and QuizzesHandler.

type CourseService

CourseService defines the application logic required by the CoursesHandler.

type CourseService interface {
ListCourses(ctx context.Context, params courses.ListCoursesParams) (courses.ListCoursesResult, error)
GetCourse(ctx context.Context, params courses.GetCourseParams) (courses.CourseDetail, error)
JoinSection(ctx context.Context, params courses.JoinSectionParams) (courses.Membership, error)
LeaveSection(ctx context.Context, params courses.LeaveSectionParams) error
ListMyEnrollments(ctx context.Context, params courses.ListMyEnrollmentsParams) ([]courses.Enrollment, error)
CheckMembership(ctx context.Context, params courses.CheckMembershipParams) (courses.MembershipCheck, error)
ListSectionMembers(ctx context.Context, params courses.ListSectionMembersParams) (courses.ListSectionMembersResult, error)
}

type CoursesHandler

CoursesHandler manages incoming HTTP requests relating to course operations.

type CoursesHandler struct {
// contains filtered or unexported fields
}

func NewCoursesHandler

func NewCoursesHandler(service CourseService) *CoursesHandler

NewCoursesHandler creates a new CoursesHandler backed by the given CourseService.

func (*CoursesHandler) CheckMembership

func (h *CoursesHandler) CheckMembership(w http.ResponseWriter, r *http.Request, courseId openapi_types.UUID, sectionId openapi_types.UUID)

CheckMembership handles GET /courses/{course_id}/sections/{section_id}/members/me. Always returns 200 -- non-membership is enrolled=false with null role/joined_at, NOT 404. 404 is reserved for missing course/section (so the frontend can distinguish "not enrolled" from "section deleted").

func (*CoursesHandler) GetCourse

func (h *CoursesHandler) GetCourse(w http.ResponseWriter, r *http.Request, courseID openapi_types.UUID)

GetCourse handles GET /courses/{course_id}, returning a single course with its embedded school summary and inline sections array.

func (*CoursesHandler) JoinSection

func (h *CoursesHandler) JoinSection(w http.ResponseWriter, r *http.Request, courseId openapi_types.UUID, sectionId openapi_types.UUID)

JoinSection handles POST /courses/{course_id}/sections/{section_id}/members. The viewer joins the section as a 'student'. Any *fields* in the request body are intentionally ignored -- the role is enforced by the service + SQL layer to prevent privilege escalation via {"role": "instructor"} -- but malformed JSON is still rejected with 400 per the spec, so we shallow-parse the body when one is provided.

func (*CoursesHandler) LeaveSection

func (h *CoursesHandler) LeaveSection(w http.ResponseWriter, r *http.Request, courseId openapi_types.UUID, sectionId openapi_types.UUID)

LeaveSection handles DELETE /courses/{course_id}/sections/{section_id}/members/me. The viewer leaves the section. The /me path segment makes self-only scope explicit; there is no path parameter for the user being removed.

func (*CoursesHandler) ListCourses

func (h *CoursesHandler) ListCourses(w http.ResponseWriter, r *http.Request, params api.ListCoursesParams)

ListCourses handles GET /courses, returning a paginated list of courses (with embedded school summaries) optionally filtered by school_id, department, or a q search term.

func (*CoursesHandler) ListMyEnrollments

func (h *CoursesHandler) ListMyEnrollments(w http.ResponseWriter, r *http.Request, params api.ListMyEnrollmentsParams)

ListMyEnrollments handles GET /me/courses, returning every section the authenticated viewer is enrolled in. Filters on term + role come from the query string; the openapi layer enforces role enum membership and term maxLength so the service path-validation is defense in depth.

func (*CoursesHandler) ListSectionMembers

func (h *CoursesHandler) ListSectionMembers(w http.ResponseWriter, r *http.Request, courseId openapi_types.UUID, sectionId openapi_types.UUID, params api.ListSectionMembersParams)

ListSectionMembers handles GET /courses/{course_id}/sections/{section_id}/members. Any authenticated user can list -- no membership check on the caller (course pages are public within the app). The response payload is the privacy floor: 5 fields, no email, no clerk_id.

type FileHandler

FileHandler manages incoming HTTP requests relating to File operations.

type FileHandler struct {
// contains filtered or unexported fields
}

func NewFileHandler

func NewFileHandler(service FileService, publisher files.QStashPublisher) *FileHandler

NewFileHandler creates a new FileHandler backed by the given FileService.

func (*FileHandler) CreateFile

func (h *FileHandler) CreateFile(w http.ResponseWriter, r *http.Request)

CreateFile handles requests to create a new file reference and get a presigned upload URL.

func (*FileHandler) DeleteFile

func (h *FileHandler) DeleteFile(w http.ResponseWriter, r *http.Request, fileId openapi_types.UUID)

DeleteFile handles requests to delete a single file by its unique identifier.

func (*FileHandler) GetFile

func (h *FileHandler) GetFile(w http.ResponseWriter, r *http.Request, fileId openapi_types.UUID)

GetFile handles requests to retrieve a single file by its unique identifier.

func (*FileHandler) ListFiles

func (h *FileHandler) ListFiles(w http.ResponseWriter, r *http.Request, params api.ListFilesParams)

ListFiles handles requests to retrieve a paginated list of files accessible to the viewer.

func (*FileHandler) UpdateFile

func (h *FileHandler) UpdateFile(w http.ResponseWriter, r *http.Request, fileId openapi_types.UUID)

UpdateFile handles requests to rename a file.

type FileService

FileService defines the application logic required by the FileHandler.

type FileService interface {
CreateFile(ctx context.Context, params files.CreateFileParams) (files.CreateFileResult, error)
GetFile(ctx context.Context, params files.GetFileParams) (files.File, error)
ListFiles(ctx context.Context, params files.ListFilesParams) ([]files.File, *string, error)
DeleteFile(ctx context.Context, params files.DeleteFileParams, publisher files.QStashPublisher) error
UpdateFile(ctx context.Context, params files.UpdateFileParams) (files.File, error)
}

type GrantHandler

GrantHandler manages incoming HTTP requests relating to file grant operations.

type GrantHandler struct {
// contains filtered or unexported fields
}

func NewGrantHandler

func NewGrantHandler(service GrantService) *GrantHandler

NewGrantHandler creates a new GrantHandler backed by the given GrantService.

func (*GrantHandler) CreateGrant

func (h *GrantHandler) CreateGrant(w http.ResponseWriter, r *http.Request, fileId openapi_types.UUID)

CreateGrant handles requests to create a permission grant on a file.

func (*GrantHandler) RevokeGrant

func (h *GrantHandler) RevokeGrant(w http.ResponseWriter, r *http.Request, fileId openapi_types.UUID)

RevokeGrant handles requests to revoke a permission grant on a file.

type GrantService

GrantService defines the application logic required by the GrantHandler.

type GrantService interface {
CreateGrant(ctx context.Context, params files.CreateGrantParams) (files.Grant, error)
RevokeGrant(ctx context.Context, params files.RevokeGrantParams) error
}

type JobHandler

JobHandler handles async job requests from QStash.

type JobHandler struct {
// contains filtered or unexported fields
}

func NewJobHandler

func NewJobHandler(s3 *s3client.Client, queries *db.Queries) *JobHandler

NewJobHandler creates a JobHandler with S3 and DB clients.

func (*JobHandler) DeleteFileFailedJob

func (h *JobHandler) DeleteFileFailedJob(w http.ResponseWriter, r *http.Request)

DeleteFileFailedJob handles POST /jobs/delete-file-failed. Called by QStash when the delete-file job fails after all retries.

func (*JobHandler) DeleteFileJob

func (h *JobHandler) DeleteFileJob(w http.ResponseWriter, r *http.Request)

DeleteFileJob handles POST /jobs/delete-file. It deletes the S3 object and marks the file as deleted in the DB.

type QuizService

QuizService defines the application logic required by the QuizzesHandler. Mirrors StudyGuideService: small, defined at the consumer, and mocked via mockery for handler tests.

type QuizService interface {
CreateQuiz(ctx context.Context, params quizzes.CreateQuizParams) (quizzes.QuizDetail, error)
ListQuizzes(ctx context.Context, params quizzes.ListQuizzesParams) ([]quizzes.QuizListItem, error)
DeleteQuiz(ctx context.Context, params quizzes.DeleteQuizParams) error
UpdateQuiz(ctx context.Context, params quizzes.UpdateQuizParams) (quizzes.QuizDetail, error)
}

type QuizzesHandler

QuizzesHandler manages incoming HTTP requests for the quizzes surface. Embedded in CompositeHandler so a single instance satisfies the generated api.ServerInterface.

type QuizzesHandler struct {
// contains filtered or unexported fields
}

func NewQuizzesHandler

func NewQuizzesHandler(service QuizService) *QuizzesHandler

NewQuizzesHandler creates a new QuizzesHandler backed by the given QuizService.

func (*QuizzesHandler) CreateQuiz

func (h *QuizzesHandler) CreateQuiz(w http.ResponseWriter, r *http.Request, studyGuideId openapi_types.UUID)

CreateQuiz handles POST /study-guides/{study_guide_id}/quizzes. The body is decoded into the openapi-generated request type; the service layer applies the cross-field validation (per-type correct_answer typing, MCQ correct-count invariant) and runs the quiz + questions + options inserts inside one transaction. The creator id is always taken from the JWT -- the openapi schema explicitly forbids accepting one in the request body.

func (*QuizzesHandler) DeleteQuiz

func (h *QuizzesHandler) DeleteQuiz(w http.ResponseWriter, r *http.Request, quizId openapi_types.UUID)

DeleteQuiz handles DELETE /quizzes/{quiz_id} (ASK-102). Creator-only -- the service runs the locked SELECT + creator check + soft-delete in a single transaction. 404 covers both 'never existed' and 'already deleted' (idempotent semantics); 403 covers viewer-is-not-creator. Returns 204 with no body on success.

func (*QuizzesHandler) ListQuizzes

func (h *QuizzesHandler) ListQuizzes(w http.ResponseWriter, r *http.Request, studyGuideId openapi_types.UUID)

ListQuizzes handles GET /study-guides/{study_guide_id}/quizzes (ASK-136). Auth-only -- any authenticated user can list. The service runs the live-guide preflight + the list query; a missing or soft-deleted guide surfaces as 404. The response always emits a non-nil quizzes slice so the JSON shape is `[]` (not null) when the guide has no quizzes.

func (*QuizzesHandler) UpdateQuiz

func (h *QuizzesHandler) UpdateQuiz(w http.ResponseWriter, r *http.Request, quizId openapi_types.UUID)

UpdateQuiz handles PATCH /quizzes/{quiz_id} (ASK-153). Decodes the raw body twice: once into a key-presence map so the service can distinguish 'description absent' from 'description explicitly null' (the openapi-generated CreateQuizRequest type uses *string with omitempty, which loses that distinction at the Go-struct level), once into the typed request body for title + description values. Builds UpdateQuizParams from both passes and delegates to service.UpdateQuiz.

Creator-only authz + 404/403 dispatch lives in the service. 200 on success carries the freshly re-hydrated QuizDetail (same wire shape as CreateQuiz) so the frontend can patch its local state without a follow-up GET.

type SchoolService

SchoolService defines the application logic required by the SchoolsHandler.

type SchoolService interface {
ListSchools(ctx context.Context, params schools.ListSchoolsParams) (schools.ListSchoolsResult, error)
GetSchool(ctx context.Context, params schools.GetSchoolParams) (schools.School, error)
}

type SchoolsHandler

SchoolsHandler manages incoming HTTP requests relating to school operations.

type SchoolsHandler struct {
// contains filtered or unexported fields
}

func NewSchoolsHandler

func NewSchoolsHandler(service SchoolService) *SchoolsHandler

NewSchoolsHandler creates a new SchoolsHandler backed by the given SchoolService.

func (*SchoolsHandler) GetSchool

func (h *SchoolsHandler) GetSchool(w http.ResponseWriter, r *http.Request, schoolID openapi_types.UUID)

GetSchool handles GET /schools/{school_id}, returning the school's full metadata or a 404 envelope if the ID does not match any row.

func (*SchoolsHandler) ListSchools

func (h *SchoolsHandler) ListSchools(w http.ResponseWriter, r *http.Request, params api.ListSchoolsParams)

ListSchools handles GET /schools, returning a paginated list of schools optionally filtered by the q search term.

type StudyGuideHandler

StudyGuideHandler manages incoming HTTP requests for the study-guide surface. Embedded in CompositeHandler so a single instance satisfies the generated api.ServerInterface.

type StudyGuideHandler struct {
// contains filtered or unexported fields
}

func NewStudyGuideHandler

func NewStudyGuideHandler(service StudyGuideService) *StudyGuideHandler

NewStudyGuideHandler creates a new StudyGuideHandler backed by the given StudyGuideService.

func (*StudyGuideHandler) AttachFile

func (h *StudyGuideHandler) AttachFile(w http.ResponseWriter, r *http.Request, studyGuideId openapi_types.UUID, fileId openapi_types.UUID)

AttachFile handles POST /study-guides/{id}/files/{file_id}. No body. AttacherID comes from JWT. Service runs ownership + status checks; 201 on success with FileAttachmentResponse, 403/404/409/500 flow through ToHTTPError.

func (*StudyGuideHandler) AttachResource

func (h *StudyGuideHandler) AttachResource(w http.ResponseWriter, r *http.Request, studyGuideId openapi_types.UUID)

AttachResource handles POST /study-guides/{id}/resources. Decodes the body, takes attached_by from JWT, delegates to service. 201 on success with ResourceSummary; 409 on duplicate URL on guide; 404 on missing guide; 400 on validation; 500 on db errors.

func (*StudyGuideHandler) CastStudyGuideVote

func (h *StudyGuideHandler) CastStudyGuideVote(w http.ResponseWriter, r *http.Request, studyGuideId openapi_types.UUID)

CastStudyGuideVote handles POST /study-guides/{id}/votes. Decodes the body, validates that `vote` is one of "up" | "down" at the openapi-validator wrapper, then delegates to the service which upserts and returns the post-mutation vote_score so the UI can patch its local state without a follow-up GET.

func (*StudyGuideHandler) CreateStudyGuide

func (h *StudyGuideHandler) CreateStudyGuide(w http.ResponseWriter, r *http.Request, courseId openapi_types.UUID)

CreateStudyGuide handles POST /courses/{course_id}/study-guides. The body is decoded into the openapi-generated request type; the service layer applies tag normalization (trim + lowercase + dedupe) and runs the AssertCourseExists preflight so a missing course surfaces as 404 instead of an FK-violation 500. The creator id is always taken from the JWT -- the openapi schema explicitly forbids accepting one in the request body to avoid privilege-attribution forging.

func (*StudyGuideHandler) DeleteStudyGuide

func (h *StudyGuideHandler) DeleteStudyGuide(w http.ResponseWriter, r *http.Request, studyGuideId openapi_types.UUID)

DeleteStudyGuide handles DELETE /study-guides/{study_guide_id}. Creator-only -- the service runs the locked SELECT + creator check + soft-delete + child-quiz cascade in a single transaction. 404 covers both 'never existed' and 'already deleted' (idempotent semantics); 403 covers viewer-is-not-creator.

func (*StudyGuideHandler) DetachFile

func (h *StudyGuideHandler) DetachFile(w http.ResponseWriter, r *http.Request, studyGuideId openapi_types.UUID, fileId openapi_types.UUID)

DetachFile handles DELETE /study-guides/{id}/files/{file_id}. 204 on success. Dual-authz (file owner OR guide creator) lives in the service; 403/404 from the service flow through ToHTTPError.

func (*StudyGuideHandler) DetachResource

func (h *StudyGuideHandler) DetachResource(w http.ResponseWriter, r *http.Request, studyGuideId openapi_types.UUID, resourceId openapi_types.UUID)

DetachResource handles DELETE /study-guides/{id}/resources/{resource_id}. 204 on success; 403 when viewer is neither guide creator nor attacher; 404 when guide is missing/deleted or resource isn't attached to this guide.

func (*StudyGuideHandler) GetStudyGuide

func (h *StudyGuideHandler) GetStudyGuide(w http.ResponseWriter, r *http.Request, studyGuideId openapi_types.UUID)

GetStudyGuide handles GET /study-guides/{study_guide_id}. Pure read per the design decision to keep GET idempotent + safe (HTTP semantics); view tracking is intentionally absent and will ship as a separate POST endpoint in a future ticket.

func (*StudyGuideHandler) ListStudyGuides

func (h *StudyGuideHandler) ListStudyGuides(w http.ResponseWriter, r *http.Request, courseId openapi_types.UUID, params api.ListStudyGuidesParams)

ListStudyGuides handles GET /courses/{course_id}/study-guides. Runs the AssertCourseExists preflight first so a missing course surfaces as a tailored 404 'Course not found' (rather than an empty 200 that would be indistinguishable from 'course exists but has no guides'). Malformed cursors are rejected at the handler with a 400 before the service is reached.

func (*StudyGuideHandler) RecommendStudyGuide

func (h *StudyGuideHandler) RecommendStudyGuide(w http.ResponseWriter, r *http.Request, studyGuideId openapi_types.UUID)

RecommendStudyGuide handles POST /study-guides/{id}/recommendations. No request body. Service returns 403 / 404 / 409 directly via typed AppErrors; handler just maps + emits 201 with the recommendation payload on success.

func (*StudyGuideHandler) RemoveStudyGuideRecommendation

func (h *StudyGuideHandler) RemoveStudyGuideRecommendation(w http.ResponseWriter, r *http.Request, studyGuideId openapi_types.UUID)

RemoveStudyGuideRecommendation handles DELETE /study-guides/{id}/recommendations. Same role gate as POST. 204 on success; 403/404 from the service flow through ToHTTPError.

func (*StudyGuideHandler) RemoveStudyGuideVote

func (h *StudyGuideHandler) RemoveStudyGuideVote(w http.ResponseWriter, r *http.Request, studyGuideId openapi_types.UUID)

RemoveStudyGuideVote handles DELETE /study-guides/{id}/votes. 404 covers both "guide missing/deleted" and "no existing vote" -- the service already collapses both cases to apperrors.ErrNotFound.

func (*StudyGuideHandler) UpdateStudyGuide

func (h *StudyGuideHandler) UpdateStudyGuide(w http.ResponseWriter, r *http.Request, studyGuideId openapi_types.UUID)

UpdateStudyGuide handles PATCH /study-guides/{study_guide_id}. Decodes the partial-update body into pointer-typed params (so 'absent' is distinct from 'empty'), pulls viewer id from JWT, delegates to service.UpdateStudyGuide which gates on creator-only + returns the freshly re-hydrated detail. 200 on success; 400/403/404 flow through ToHTTPError unchanged.

type StudyGuideService

StudyGuideService defines the application logic required by the StudyGuideHandler. Mirrors CourseService: small, defined at the consumer, and mocked via mockery for handler tests.

type StudyGuideService interface {
ListStudyGuides(ctx context.Context, params studyguides.ListStudyGuidesParams) (studyguides.ListStudyGuidesResult, error)
AssertCourseExists(ctx context.Context, courseID uuid.UUID) error
GetStudyGuide(ctx context.Context, params studyguides.GetStudyGuideParams) (studyguides.StudyGuideDetail, error)
CreateStudyGuide(ctx context.Context, params studyguides.CreateStudyGuideParams) (studyguides.StudyGuideDetail, error)
UpdateStudyGuide(ctx context.Context, params studyguides.UpdateStudyGuideParams) (studyguides.StudyGuideDetail, error)
DeleteStudyGuide(ctx context.Context, params studyguides.DeleteStudyGuideParams) error
CastVote(ctx context.Context, params studyguides.CastVoteParams) (studyguides.CastVoteResult, error)
RemoveVote(ctx context.Context, params studyguides.RemoveVoteParams) error
RecommendStudyGuide(ctx context.Context, params studyguides.RecommendStudyGuideParams) (studyguides.Recommendation, error)
RemoveRecommendation(ctx context.Context, params studyguides.RemoveRecommendationParams) error
AttachResource(ctx context.Context, params studyguides.AttachResourceParams) (studyguides.Resource, error)
DetachResource(ctx context.Context, params studyguides.DetachResourceParams) error
AttachFile(ctx context.Context, params studyguides.AttachFileParams) (studyguides.FileAttachment, error)
DetachFile(ctx context.Context, params studyguides.DetachFileParams) error
}

Generated by gomarkdoc