feat: added API gateway

This commit is contained in:
ITQ
2025-12-17 11:00:50 +03:00
parent daa8c24482
commit 340ae43d22
30 changed files with 2838 additions and 3 deletions
@@ -0,0 +1,69 @@
package handler
import (
"net/http"
"datarush/internal/gw/domain"
"datarush/internal/gw/grpc_client"
"datarush/internal/gw/utils"
)
type AchievementsHandler struct {
achievementsClient *grpc_client.AchievementsClient
}
func NewAchievementsHandler(achievementsClient *grpc_client.AchievementsClient) *AchievementsHandler {
return &AchievementsHandler{achievementsClient: achievementsClient}
}
func (h *AchievementsHandler) ListAchievements(w http.ResponseWriter, r *http.Request) {
achievements, err := h.achievementsClient.ListAchievements(r.Context())
if err != nil {
utils.RespondError(w, domain.NewInternalServerError("failed to list achievements"))
return
}
response := make([]domain.AchievementResponse, len(achievements))
for i, ach := range achievements {
response[i] = *utils.AchievementProtoToHTTP(ach)
}
utils.RespondJSON(w, http.StatusOK, &domain.ListAchievementsResponse{
Achievements: response,
})
}
func (h *AchievementsHandler) GetAchievement(w http.ResponseWriter, r *http.Request) {
achievementID := getPathParam(r, "achievement_id")
achievement, err := h.achievementsClient.GetAchievement(r.Context(), achievementID)
if err != nil {
utils.RespondError(w, domain.NewNotFoundError("achievement not found"))
return
}
utils.RespondJSON(w, http.StatusOK, utils.AchievementProtoToHTTP(achievement))
}
func (h *AchievementsHandler) GetUserAchievements(w http.ResponseWriter, r *http.Request) {
userID := getPathParam(r, "user_id")
achievements, err := h.achievementsClient.GetUserAchievements(r.Context(), userID)
if err != nil {
utils.RespondError(w, domain.NewInternalServerError("failed to get user achievements"))
return
}
response := make([]domain.UserAchievementResponse, len(achievements))
for i, ach := range achievements {
response[i] = domain.UserAchievementResponse{
AchievementID: ach.Achievement.Id,
UserID: userID,
EarnedAt: ach.ReceivedAt.AsTime(),
}
}
utils.RespondJSON(w, http.StatusOK, &domain.ListUserAchievementsResponse{
Achievements: response,
})
}
+69
View File
@@ -0,0 +1,69 @@
package handler
import (
"net/http"
"datarush/internal/gw/domain"
"datarush/internal/gw/grpc_client"
"datarush/internal/gw/utils"
)
type AuthHandler struct {
authClient *grpc_client.AuthClient
userClient *grpc_client.UserClient
}
func NewAuthHandler(authClient *grpc_client.AuthClient, userClient *grpc_client.UserClient) *AuthHandler {
return &AuthHandler{
authClient: authClient,
userClient: userClient,
}
}
func (h *AuthHandler) SignUp(w http.ResponseWriter, r *http.Request) {
var req domain.SignUpRequest
if err := utils.DecodeJSON(r, &req); err != nil {
utils.RespondError(w, err)
return
}
token, err := h.authClient.SignUp(r.Context(), req.Email, req.Username, req.Password)
if err != nil {
utils.RespondError(w, domain.NewInternalServerError("sign up failed"))
return
}
utils.RespondJSON(w, http.StatusCreated, &domain.TokenResponse{Token: token})
}
func (h *AuthHandler) SignIn(w http.ResponseWriter, r *http.Request) {
var req domain.SignInRequest
if err := utils.DecodeJSON(r, &req); err != nil {
utils.RespondError(w, err)
return
}
token, err := h.authClient.SignIn(r.Context(), req.Email, req.Password)
if err != nil {
utils.RespondError(w, domain.NewUnauthorizedError("invalid credentials"))
return
}
utils.RespondJSON(w, http.StatusOK, &domain.TokenResponse{Token: token})
}
func (h *AuthHandler) GetMe(w http.ResponseWriter, r *http.Request) {
userID, err := getUserIDFromContext(r.Context())
if err != nil {
utils.RespondError(w, domain.NewUnauthorizedError("unauthorized"))
return
}
user, err := h.userClient.GetProfile(r.Context(), userID)
if err != nil {
utils.RespondError(w, domain.NewInternalServerError("failed to get user profile"))
return
}
utils.RespondJSON(w, http.StatusOK, utils.UserProtoToHTTP(user))
}
+75
View File
@@ -0,0 +1,75 @@
package handler
import (
"context"
"encoding/json"
"net/http"
"strconv"
"datarush/internal/gw/domain"
"datarush/internal/gw/middleware"
"github.com/gorilla/mux"
)
func getUserIDFromContext(ctx context.Context) (string, error) {
return middleware.GetUserIDFromContext(ctx)
}
func getPathParam(r *http.Request, key string) string {
vars := mux.Vars(r)
return vars[key]
}
func getQueryParam(r *http.Request, key string) string {
return r.URL.Query().Get(key)
}
func getQueryParamInt(r *http.Request, key string, defaultValue int) int {
val := r.URL.Query().Get(key)
if val == "" {
return defaultValue
}
intVal, err := strconv.Atoi(val)
if err != nil {
return defaultValue
}
return intVal
}
func getQueryParamInt32(r *http.Request, key string, defaultValue int32) int32 {
return int32(getQueryParamInt(r, key, int(defaultValue)))
}
func getQueryParamBool(r *http.Request, key string) *bool {
val := r.URL.Query().Get(key)
if val == "" {
return nil
}
boolVal, err := strconv.ParseBool(val)
if err != nil {
return nil
}
return &boolVal
}
type PingHandler struct{}
func NewPingHandler() *PingHandler {
return &PingHandler{}
}
func (h *PingHandler) Ping(w http.ResponseWriter, r *http.Request) {
response := &domain.PingResponse{
Message: "pong",
Status: "ok",
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(response)
}
+158
View File
@@ -0,0 +1,158 @@
package handler
import (
"net/http"
"datarush/internal/gw/domain"
"datarush/internal/gw/grpc_client"
"datarush/internal/gw/utils"
comppb "datarush/pkg/api/competition"
)
type CompetitionHandler struct {
competitionClient *grpc_client.CompetitionClient
userClient *grpc_client.UserClient
}
func NewCompetitionHandler(
competitionClient *grpc_client.CompetitionClient,
userClient *grpc_client.UserClient,
) *CompetitionHandler {
return &CompetitionHandler{
competitionClient: competitionClient,
userClient: userClient,
}
}
func (h *CompetitionHandler) CreateCompetition(w http.ResponseWriter, r *http.Request) {
var req domain.CompetitionRequest
if err := utils.DecodeJSON(r, &req); err != nil {
utils.RespondError(w, err)
return
}
competition := utils.CompetitionHTTPToProto(&req)
resp, err := h.competitionClient.CreateCompetition(r.Context(), competition)
if err != nil {
utils.RespondError(w, domain.NewInternalServerError("failed to create competition"))
return
}
utils.RespondJSON(w, http.StatusCreated, utils.CompetitionProtoToHTTP(resp))
}
func (h *CompetitionHandler) GetCompetition(w http.ResponseWriter, r *http.Request) {
competitionID := getPathParam(r, "competition_id")
competition, err := h.competitionClient.GetCompetition(r.Context(), competitionID)
if err != nil {
utils.RespondError(w, domain.NewNotFoundError("competition not found"))
return
}
utils.RespondJSON(w, http.StatusOK, utils.CompetitionProtoToHTTP(competition))
}
func (h *CompetitionHandler) UpdateCompetition(w http.ResponseWriter, r *http.Request) {
competitionID := getPathParam(r, "competition_id")
var req domain.CompetitionRequest
if err := utils.DecodeJSON(r, &req); err != nil {
utils.RespondError(w, err)
return
}
req.ID = competitionID
competition := utils.CompetitionHTTPToProto(&req)
resp, err := h.competitionClient.EditCompetition(r.Context(), competition)
if err != nil {
utils.RespondError(w, domain.NewInternalServerError("failed to update competition"))
return
}
utils.RespondJSON(w, http.StatusOK, utils.CompetitionProtoToHTTP(resp))
}
func (h *CompetitionHandler) DeleteCompetition(w http.ResponseWriter, r *http.Request) {
competitionID := getPathParam(r, "competition_id")
if err := h.competitionClient.DeleteCompetition(r.Context(), competitionID); err != nil {
utils.RespondError(w, domain.NewInternalServerError("failed to delete competition"))
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *CompetitionHandler) ListCompetitions(w http.ResponseWriter, r *http.Request) {
pageSize := getQueryParamInt32(r, "page_size", 20)
pageToken := getQueryParamInt32(r, "page_token", 0)
state := getQueryParam(r, "state")
searchQuery := getQueryParam(r, "search_query")
isParticipating := getQueryParamBool(r, "is_participating")
req := &comppb.ListCompetitionsRequest{
PageSize: pageSize,
PageToken: pageToken,
}
if state != "" {
s := utils.StringToCompetitionState(state)
req.State = &s
}
if searchQuery != "" {
req.SearchQuery = &searchQuery
}
if isParticipating != nil {
req.IsParticipating = isParticipating
}
resp, err := h.competitionClient.ListCompetitions(r.Context(), req)
if err != nil {
utils.RespondError(w, domain.NewInternalServerError("failed to list competitions"))
return
}
competitions := make([]domain.CompetitionResponse, len(resp.Competitions))
for i, comp := range resp.Competitions {
competitions[i] = *utils.CompetitionProtoToHTTP(comp)
}
utils.RespondJSON(w, http.StatusOK, &domain.ListCompetitionsResponse{
TotalCount: resp.TotalCount,
NextPageToken: resp.NextPageToken,
Competitions: competitions,
})
}
func (h *CompetitionHandler) ChangeCompetitionState(w http.ResponseWriter, r *http.Request) {
competitionID := getPathParam(r, "competition_id")
var req domain.ChangeCompetitionStateRequest
if err := utils.DecodeJSON(r, &req); err != nil {
utils.RespondError(w, err)
return
}
state := utils.StringToCompetitionState(req.State)
competition, err := h.competitionClient.ChangeCompetitionState(r.Context(), competitionID, state)
if err != nil {
utils.RespondError(w, domain.NewInternalServerError("failed to change competition state"))
return
}
utils.RespondJSON(w, http.StatusOK, utils.CompetitionProtoToHTTP(competition))
}
func (h *CompetitionHandler) JoinCompetition(w http.ResponseWriter, r *http.Request) {
competitionID := getPathParam(r, "competition_id")
if err := h.userClient.RegisterForCompetition(r.Context(), competitionID); err != nil {
utils.RespondError(w, domain.NewInternalServerError("failed to join competition"))
return
}
w.WriteHeader(http.StatusNoContent)
}
+77
View File
@@ -0,0 +1,77 @@
package handler
import (
"net/http"
"datarush/internal/gw/domain"
"datarush/internal/gw/grpc_client"
"datarush/internal/gw/utils"
resultspb "datarush/pkg/api/results"
)
type ResultsHandler struct {
resultsClient *grpc_client.ResultsClient
}
func NewResultsHandler(resultsClient *grpc_client.ResultsClient) *ResultsHandler {
return &ResultsHandler{resultsClient: resultsClient}
}
func (h *ResultsHandler) GetCompetitionResults(w http.ResponseWriter, r *http.Request) {
competitionID := getPathParam(r, "competition_id")
pageSize := getQueryParamInt32(r, "page_size", 20)
pageToken := getQueryParamInt32(r, "page_token", 0)
req := &resultspb.GetCompetitionResultsRequest{
CompetitionId: competitionID,
PageSize: pageSize,
PageToken: pageToken,
}
resp, err := h.resultsClient.GetCompetitionResults(r.Context(), req)
if err != nil {
utils.RespondError(w, domain.NewInternalServerError("failed to get competition results"))
return
}
results := make([]domain.UserResultResponse, len(resp.Results))
for i, result := range resp.Results {
results[i] = *utils.UserResultProtoToHTTP(result)
}
utils.RespondJSON(w, http.StatusOK, &domain.CompetitionResultsResponse{
Results: results,
TotalCount: resp.TotalCount,
NextPageToken: resp.NextPageToken,
})
}
func (h *ResultsHandler) GetMyResults(w http.ResponseWriter, r *http.Request) {
userID, err := getUserIDFromContext(r.Context())
if err != nil {
utils.RespondError(w, domain.NewUnauthorizedError("unauthorized"))
return
}
competitionID := getPathParam(r, "competition_id")
result, err := h.resultsClient.GetUserCompetitionResults(r.Context(), competitionID, userID)
if err != nil {
utils.RespondError(w, domain.NewInternalServerError("failed to get user results"))
return
}
utils.RespondJSON(w, http.StatusOK, utils.UserResultProtoToHTTP(result))
}
func (h *ResultsHandler) RecalculateResults(w http.ResponseWriter, r *http.Request) {
competitionID := getPathParam(r, "competition_id")
if err := h.resultsClient.RecalculateResults(r.Context(), competitionID); err != nil {
utils.RespondError(w, domain.NewInternalServerError("failed to recalculate results"))
return
}
w.WriteHeader(http.StatusNoContent)
}
+133
View File
@@ -0,0 +1,133 @@
package handler
import (
"net/http"
"datarush/internal/gw/domain"
"datarush/internal/gw/grpc_client"
"datarush/internal/gw/utils"
reviewpb "datarush/pkg/api/review"
)
type ReviewHandler struct {
reviewClient *grpc_client.ReviewClient
}
func NewReviewHandler(reviewClient *grpc_client.ReviewClient) *ReviewHandler {
return &ReviewHandler{reviewClient: reviewClient}
}
func (h *ReviewHandler) ListSubmissionsForReview(w http.ResponseWriter, r *http.Request) {
token := getPathParam(r, "token")
pageSize := getQueryParamInt32(r, "page_size", 20)
pageToken := getQueryParamInt32(r, "page_token", 0)
statusStr := getQueryParam(r, "status")
req := &reviewpb.ListSubmissionsForReviewRequest{
Token: token,
PageSize: pageSize,
PageToken: pageToken,
}
if statusStr != "" {
status := stringToReviewStatus(statusStr)
req.Status = &status
}
resp, err := h.reviewClient.ListSubmissionsForReview(r.Context(), req)
if err != nil {
utils.RespondError(w, domain.NewUnauthorizedError("invalid review token"))
return
}
submissions := make([]domain.SubmissionSummaryResponse, len(resp.Submissions))
for i, sub := range resp.Submissions {
submissions[i] = *utils.SubmissionSummaryProtoToHTTP(sub)
}
utils.RespondJSON(w, http.StatusOK, &domain.ListSubmissionsForReviewResponse{
TotalCount: resp.TotalCount,
NextPageToken: resp.NextPageToken,
Submissions: submissions,
})
}
func (h *ReviewHandler) GetSubmissionForReview(w http.ResponseWriter, r *http.Request) {
token := getPathParam(r, "token")
submissionID := getPathParam(r, "submission_id")
submission, err := h.reviewClient.GetSubmissionForReview(r.Context(), token, submissionID)
if err != nil {
utils.RespondError(w, domain.NewNotFoundError("submission not found"))
return
}
utils.RespondJSON(w, http.StatusOK, utils.SubmissionForReviewProtoToHTTP(submission))
}
func (h *ReviewHandler) EvaluateSubmission(w http.ResponseWriter, r *http.Request) {
token := getPathParam(r, "token")
submissionID := getPathParam(r, "submission_id")
var reqBody domain.EvaluateSubmissionRequest
if err := utils.DecodeJSON(r, &reqBody); err != nil {
utils.RespondError(w, err)
return
}
marks := make([]*reviewpb.CriteriaMark, len(reqBody.Marks))
for i, m := range reqBody.Marks {
marks[i] = &reviewpb.CriteriaMark{
Slug: m.Slug,
Mark: m.Mark,
}
}
req := &reviewpb.EvaluateSubmissionRequest{
Token: token,
SubmissionId: submissionID,
EarnedPoints: reqBody.EarnedPoints,
ReviewerComment: reqBody.ReviewerComment,
Marks: marks,
}
resp, err := h.reviewClient.EvaluateSubmission(r.Context(), req)
if err != nil {
utils.RespondError(w, domain.NewInternalServerError("failed to evaluate submission"))
return
}
utils.RespondJSON(w, http.StatusOK, &domain.EvaluateSubmissionResponse{
SubmissionID: resp.SubmissionId,
FinalScore: resp.FinalScore,
NewStatus: utils.ReviewStatusToString(resp.NewStatus),
})
}
func (h *ReviewHandler) ReleaseSubmission(w http.ResponseWriter, r *http.Request) {
token := getPathParam(r, "token")
submissionID := getPathParam(r, "submission_id")
if err := h.reviewClient.ReleaseSubmission(r.Context(), token, submissionID); err != nil {
utils.RespondError(w, domain.NewInternalServerError("failed to release submission"))
return
}
w.WriteHeader(http.StatusNoContent)
}
func stringToReviewStatus(s string) reviewpb.ReviewStatus {
switch s {
case "pending":
return reviewpb.ReviewStatus_REVIEW_STATUS_PENDING
case "in_review":
return reviewpb.ReviewStatus_REVIEW_STATUS_IN_REVIEW
case "completed":
return reviewpb.ReviewStatus_REVIEW_STATUS_COMPLETED
case "rejected":
return reviewpb.ReviewStatus_REVIEW_STATUS_REJECTED
default:
return reviewpb.ReviewStatus_REVIEW_STATUS_UNSPECIFIED
}
}
+90
View File
@@ -0,0 +1,90 @@
package handler
import (
"net/http"
"datarush/internal/gw/domain"
"datarush/internal/gw/grpc_client"
"datarush/internal/gw/storage"
"datarush/internal/gw/utils"
)
type SubmissionHandler struct {
submissionClient *grpc_client.SubmissionClient
s3Storage *storage.S3Storage
}
func NewSubmissionHandler(
submissionClient *grpc_client.SubmissionClient,
s3Storage *storage.S3Storage,
) *SubmissionHandler {
return &SubmissionHandler{
submissionClient: submissionClient,
s3Storage: s3Storage,
}
}
func (h *SubmissionHandler) SubmitTask(w http.ResponseWriter, r *http.Request) {
userID, err := getUserIDFromContext(r.Context())
if err != nil {
utils.RespondError(w, domain.NewUnauthorizedError("unauthorized"))
return
}
competitionID := getPathParam(r, "competition_id")
taskID := getPathParam(r, "task_id")
if err := r.ParseMultipartForm(32 << 20); err != nil { // 32 MB max
utils.RespondError(w, domain.NewBadRequestError("failed to parse form"))
return
}
file, header, err := r.FormFile("content")
if err != nil {
utils.RespondError(w, domain.NewBadRequestError("missing or invalid file"))
return
}
defer file.Close()
fileURL, err := h.s3Storage.UploadFile(r.Context(), file, header)
if err != nil {
utils.RespondError(w, domain.NewInternalServerError("failed to upload file"))
return
}
submission, err := h.submissionClient.SubmitTask(r.Context(), userID, competitionID, taskID, fileURL)
if err != nil {
utils.RespondError(w, domain.NewInternalServerError("failed to submit task"))
return
}
utils.RespondJSON(w, http.StatusCreated, &domain.SubmitTaskResponse{
SubmissionID: submission.Id,
})
}
func (h *SubmissionHandler) GetSubmissionHistory(w http.ResponseWriter, r *http.Request) {
userID, err := getUserIDFromContext(r.Context())
if err != nil {
utils.RespondError(w, domain.NewUnauthorizedError("unauthorized"))
return
}
competitionID := getPathParam(r, "competition_id")
taskID := getPathParam(r, "task_id")
submissions, err := h.submissionClient.GetSubmissionsHistory(r.Context(), userID, competitionID, taskID)
if err != nil {
utils.RespondError(w, domain.NewInternalServerError("failed to get submission history"))
return
}
response := make([]domain.SubmissionResponse, len(submissions))
for i, sub := range submissions {
response[i] = *utils.SubmissionProtoToHTTP(sub)
}
utils.RespondJSON(w, http.StatusOK, &domain.SubmissionHistoryResponse{
Submissions: response,
})
}
+99
View File
@@ -0,0 +1,99 @@
package handler
import (
"net/http"
"datarush/internal/gw/domain"
"datarush/internal/gw/grpc_client"
"datarush/internal/gw/utils"
)
type TaskHandler struct {
taskClient *grpc_client.TaskClient
}
func NewTaskHandler(taskClient *grpc_client.TaskClient) *TaskHandler {
return &TaskHandler{taskClient: taskClient}
}
func (h *TaskHandler) CreateTask(w http.ResponseWriter, r *http.Request) {
competitionID := getPathParam(r, "competition_id")
var req domain.TaskRequest
if err := utils.DecodeJSON(r, &req); err != nil {
utils.RespondError(w, err)
return
}
req.CompetitionID = competitionID
task := utils.TaskHTTPToProto(&req)
resp, err := h.taskClient.CreateTask(r.Context(), task)
if err != nil {
utils.RespondError(w, domain.NewInternalServerError("failed to create task"))
return
}
utils.RespondJSON(w, http.StatusCreated, utils.TaskProtoToHTTP(resp))
}
func (h *TaskHandler) GetTask(w http.ResponseWriter, r *http.Request) {
taskID := getPathParam(r, "task_id")
task, err := h.taskClient.GetTask(r.Context(), taskID)
if err != nil {
utils.RespondError(w, domain.NewNotFoundError("task not found"))
return
}
utils.RespondJSON(w, http.StatusOK, utils.TaskProtoToHTTP(task))
}
func (h *TaskHandler) UpdateTask(w http.ResponseWriter, r *http.Request) {
competitionID := getPathParam(r, "competition_id")
taskID := getPathParam(r, "task_id")
var req domain.TaskRequest
if err := utils.DecodeJSON(r, &req); err != nil {
utils.RespondError(w, err)
return
}
req.ID = taskID
req.CompetitionID = competitionID
task := utils.TaskHTTPToProto(&req)
resp, err := h.taskClient.EditTask(r.Context(), task)
if err != nil {
utils.RespondError(w, domain.NewInternalServerError("failed to update task"))
return
}
utils.RespondJSON(w, http.StatusOK, utils.TaskProtoToHTTP(resp))
}
func (h *TaskHandler) DeleteTask(w http.ResponseWriter, r *http.Request) {
taskID := getPathParam(r, "task_id")
if err := h.taskClient.DeleteTask(r.Context(), taskID); err != nil {
utils.RespondError(w, domain.NewInternalServerError("failed to delete task"))
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *TaskHandler) ListTasks(w http.ResponseWriter, r *http.Request) {
competitionID := getPathParam(r, "competition_id")
tasks, err := h.taskClient.ListCompetitionTasks(r.Context(), competitionID)
if err != nil {
utils.RespondError(w, domain.NewInternalServerError("failed to list tasks"))
return
}
response := make([]domain.TaskResponse, len(tasks))
for i, task := range tasks {
response[i] = *utils.TaskProtoToHTTP(task)
}
utils.RespondJSON(w, http.StatusOK, &domain.ListTasksResponse{Tasks: response})
}