diff --git a/.gitignore b/.gitignore index 234dd5e..9d9a7a0 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ go.work.sum .idea/ .vscode/ *.~ +*~ # Database files api/*.db diff --git a/api/api.go b/api/api.go index c5e336c..0f52c1b 100644 --- a/api/api.go +++ b/api/api.go @@ -2,9 +2,11 @@ package api import ( "fmt" + "html/template" "log" "net/http" + "lidsol.org/papeador/auth" "lidsol.org/papeador/store" ) @@ -12,27 +14,39 @@ type ApiContext struct { Store store.Store } +var templates = template.Must(template.ParseGlob("templates/*.html")) + func API(s store.Store, port int) { apiCtx := ApiContext{ Store: s, } mux := http.NewServeMux() - mux.HandleFunc("/program", methodHandler("POST", apiCtx.submitProgram)) - mux.HandleFunc("/users", methodHandler("POST", apiCtx.createUser)) - mux.HandleFunc("/contests", methodHandler("POST", apiCtx.createContest)) - mux.HandleFunc("/problems", methodHandler("POST", apiCtx.createProblem)) - mux.HandleFunc("/program", methodHandler("POST", apiCtx.SubmitProgram)) - log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", port), mux)) -} + mux.HandleFunc("GET /", apiCtx.getContests) -func methodHandler(method string, h http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if r.Method != method { - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - return - } - h(w, r) - } + mux.HandleFunc("POST /users/login", apiCtx.loginUser) + mux.HandleFunc("GET /login", apiCtx.loginUserView) + mux.HandleFunc("GET /logout", apiCtx.logoutUser) + + mux.HandleFunc("POST /users/create", apiCtx.createUser) + // mux.HandleFunc("GET /users", apiCtx.createUserView) + mux.HandleFunc("GET /users/{id}", apiCtx.getUserByID) + + mux.HandleFunc("POST /contests/new", auth.RequireAuth(apiCtx.createContest)) + mux.HandleFunc("GET /new-contest", auth.RequireAuth(apiCtx.createContestView)) + mux.HandleFunc("GET /contests", apiCtx.getContests) + mux.HandleFunc("GET /contests/{id}", apiCtx.getContestByID) + + mux.HandleFunc("POST /contests/{id}/problems/new", auth.RequireAuth(apiCtx.createProblem)) + mux.HandleFunc("GET /contests/{id}/new-problem", auth.RequireAuth(apiCtx.createProblemView)) + mux.HandleFunc("GET /contests/{contestID}/problems/{problemID}", apiCtx.getProblemByID) + mux.HandleFunc("GET /contests/{contestID}/problems/{problemID}/pdf", apiCtx.getProblemStatementByID) + + mux.HandleFunc("POST /contests/{constestID}/problems/{problemID}/submit", auth.RequireAuth(apiCtx.submitProgram)) + // mux.HandleFunc("GET /contests/{constestID}/problems/{problemID}/submit/{submitID}", auth.RequireAuth(apiCtx.getSubmissionByID)) + mux.HandleFunc("GET /contests/{constestID}/problems/{problemID}/submit", auth.RequireAuth(apiCtx.getSubmissions)) + // mux.HandleFunc("GET /contests/{constestID}/problems/{problemID}/last-submit", auth.RequireAuth(apiCtx.getLastSubmission)) + + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), mux)) } diff --git a/api/contest.go b/api/contest.go index c7f1a3c..c14e5d1 100644 --- a/api/contest.go +++ b/api/contest.go @@ -1,21 +1,49 @@ package api import ( - "encoding/json" + "fmt" "log" "net/http" + "strconv" "lidsol.org/papeador/store" ) +type ContestRequestContent struct { + store.Contest + Username string `json:"username"` + JWT string `json:"jwt"` +} + func (api *ApiContext) createContest(w http.ResponseWriter, r *http.Request) { var in store.Contest - if err := json.NewDecoder(r.Body).Decode(&in); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + + in.ContestName = r.FormValue("contest-name") + in.StartDate = r.FormValue("start-date") + in.EndDate = r.FormValue("end-date") + + cookieUsername, err := r.Cookie("username") + username := cookieUsername.Value + + // We shouldn't ever get here, handle it anyways + if err != nil { + log.Println("Sin cookie") + http.Error(w, "No hay sesión iniciada", http.StatusNotFound) + return + } + + id, err := api.Store.GetUserID(r.Context(), username) + + // We shouldn't ever get here, handle it anyways + if err != nil { + log.Println("Usuario no existe") + http.Error(w, "Este usuario no existe", http.StatusNotFound) return } - if err := api.Store.CreateContest(r.Context(), &in); err != nil { + in.OrganizerID = int64(id) + + if err = api.Store.CreateContest(r.Context(), &in); err != nil { if err == store.ErrAlreadyExists { http.Error(w, "El nombre del contest ya esta registrado", http.StatusConflict) log.Println("El nombre del contest ya esta registrado") @@ -26,7 +54,74 @@ func (api *ApiContext) createContest(w http.ResponseWriter, r *http.Request) { return } - w.Header().Set("Content-Type", "application/json") + path := fmt.Sprintf("/contests/%v", in.ContestID) + w.Header().Set("HX-Redirect", path) + w.WriteHeader(http.StatusOK) +} + +func (api *ApiContext) createContestView(w http.ResponseWriter, r *http.Request) { + templates.ExecuteTemplate(w, "createContest.html", nil) +} + +func (api *ApiContext) getContests(w http.ResponseWriter, r *http.Request) { + type contestsInfo struct { + Contests []store.Contest + Username string + } + + var info contestsInfo + contests, err := api.Store.GetContests(r.Context()) + info.Contests = contests + + cookieUsername, err := r.Cookie("username") + if err == nil { + username := cookieUsername.Value + info.Username = username + } + + if err != nil { + log.Println("ERROR", err) + w.WriteHeader(http.StatusNotFound) + templates.ExecuteTemplate(w, "404.html", &info) + return + } + + w.Header().Set("Content-Type", "text/html") w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(in) + templates.ExecuteTemplate(w, "contests.html", &info) +} + +func (api *ApiContext) getContestByID(w http.ResponseWriter, r *http.Request) { + idStr := r.PathValue("id") + id, _ := strconv.Atoi(idStr) + + type contestInfo struct { + store.Contest + Problems []store.Problem + Username string + } + + c, err := api.Store.GetContestByID(r.Context(), id) + info := contestInfo{Contest: c} + + cookieUsername, err := r.Cookie("username") + if err == nil { + username := cookieUsername.Value + info.Username = username + } + + w.Header().Set("Content-Type", "text/html") + if err != nil { + log.Println("ERRROR", err) + w.WriteHeader(http.StatusNotFound) + templates.ExecuteTemplate(w, "404.html", &info) + return + } + + problems, err := api.Store.GetContestProblems(r.Context(), int(info.ContestID)) + info.Problems = problems + log.Println("info", info) + + w.WriteHeader(http.StatusOK) + templates.ExecuteTemplate(w, "contest.html", &info) } diff --git a/api/login.go b/api/login.go new file mode 100644 index 0000000..5afd826 --- /dev/null +++ b/api/login.go @@ -0,0 +1,65 @@ +package api + +import ( + "log" + "net/http" + + "lidsol.org/papeador/security" + "lidsol.org/papeador/store" +) + +func (api *ApiContext) login(w http.ResponseWriter, r *http.Request) { + var in store.User + r.ParseForm() + + in.Email = r.FormValue("email") + in.Username = r.FormValue("username") + in.Password = r.FormValue("password") + + if (in.Username == "" && in.Email == "") || (in.Password == "") { + http.Error(w, "Usuario y contraseña son requeridos", http.StatusBadRequest) + log.Println("Campos requeridos vacíos") + return + } + + if err := api.Store.Login(r.Context(), &in); err != nil { + if err == store.ErrNotFound { + http.Error(w, "El usuario ingresado no existe", http.StatusUnauthorized) + log.Println("El usuario ingresado no existe") + return + } else if err == security.ErrInvalidPassword { + http.Error(w, "La contraseña ingresada es incorrecta", http.StatusUnauthorized) + log.Println("La contraseña ingresada es incorrecta") + return + } + + http.Error(w, err.Error(), http.StatusInternalServerError) + log.Println(err) + return + } + log.Println("SE PUDO INICIAR SESIÓN") + http.SetCookie(w, &http.Cookie{ + Name: "session", + Value: in.JWT, + Path: "/", + }) + http.SetCookie(w, &http.Cookie{ + Name: "username", + Value: in.Username, + Path: "/", + }) + w.Header().Set("HX-Redirect", "/") + w.WriteHeader(http.StatusOK) + +} +func (api *ApiContext) createLoginView(w http.ResponseWriter, r *http.Request) { + type prueba struct { + Title string + } + a := prueba{Title: "Iniciar sesión"} + if err := templates.ExecuteTemplate(w, "login.html", &a); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + log.Println("Error ejecutando template login.html:", err) + } +} + diff --git a/api/problem.go b/api/problem.go index faa15ca..0b017b7 100644 --- a/api/problem.go +++ b/api/problem.go @@ -1,20 +1,123 @@ package api import ( - "encoding/json" + "fmt" + "io" "log" "net/http" + "strconv" "lidsol.org/papeador/store" ) +func getRequestFileContents(r *http.Request, formField string) ([]byte, error) { + // Retrieve the file from the form data + file, _, err := r.FormFile(formField) + if err != nil { + return nil, err + } + defer file.Close() + + // Read file contents into []byte + fileBytes, err := io.ReadAll(file) + if err != nil { + return nil, err + } + + return fileBytes, nil +} + +func getFileGroup(r *http.Request, formField string) ([][]byte, error) { + files := r.MultipartForm.File[formField] + + output := make([][]byte, 0) + for _, fileHeader := range files { + file, _ := fileHeader.Open() + defer file.Close() + content, err := io.ReadAll(file) + if err != nil { + return nil, err + } + + output = append(output, content) + } + + return output, nil +} + func (api *ApiContext) createProblem(w http.ResponseWriter, r *http.Request) { var in store.Problem - if err := json.NewDecoder(r.Body).Decode(&in); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + + contestIDStr := r.PathValue("id") + log.Println("ID", contestIDStr) + contestID, err := strconv.Atoi(contestIDStr) + if err != nil { + http.Error(w, "Error en ruta", http.StatusBadRequest) + log.Println("Error", err) + return + } + contestID64 := int64(contestID) + + in.ContestID = &contestID64 + in.ProblemName = r.FormValue("problem-name") + + timelimitStr := r.FormValue("time-limit") + timelimit, err := strconv.Atoi(timelimitStr) + if err != nil { + http.Error(w, "Error en ruta", http.StatusBadRequest) + return + } + + err = r.ParseMultipartForm(8 << 20) + if err != nil { + http.Error(w, "Los archivos deben ser, como máximo, de 8 MiB", http.StatusBadRequest) + log.Println("Error", err) + return + } + + description, err := getRequestFileContents(r, "description") + if err != nil { + http.Error(w, "Error al leer el enunciado", http.StatusInternalServerError) + log.Println("Error", err) + return + } + in.Description = description + + inputs, err := getFileGroup(r, "inputs") + if err != nil { + http.Error(w, "Error al leer los archivos de entrada", http.StatusInternalServerError) + log.Println("Error", err) + return + } + + outputs, err := getFileGroup(r, "outputs") + if err != nil { + http.Error(w, "Error al leer los archivos de salida", http.StatusInternalServerError) + log.Println("Error", err) + return + } + + cookieUsername, err := r.Cookie("username") + username := cookieUsername.Value + + // We shouldn't ever get here, handle it anyways + if err != nil { + log.Println("Sin cookie") + http.Error(w, "No hay sesión iniciada", http.StatusNotFound) + return + } + + id, err := api.Store.GetUserID(r.Context(), username) + + // We shouldn't ever get here, handle it anyways + if err != nil { + log.Println("Usuario no existe") + http.Error(w, "Este usuario no existe", http.StatusNotFound) return } + in.CreatorID = int64(id) + if err := api.Store.CreateProblem(r.Context(), &in); err != nil { if err == store.ErrNotFound { http.Error(w, "El concurso no existe", http.StatusConflict) @@ -26,7 +129,105 @@ func (api *ApiContext) createProblem(w http.ResponseWriter, r *http.Request) { return } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(in) + for k, _ := range inputs { + var t store.TestCase + t.GivenInput = inputs[k] + t.ExpectedOut = outputs[k] + t.NumTestCase = int64(k) + t.TimeLimit = int64(timelimit) + t.ProblemID = in.ProblemID + + if err = api.Store.CreateTestCase(r.Context(), &t); err != nil { + http.Error(w, "No se pudo crear el caso de prueba", http.StatusInternalServerError) + log.Println("error", err) + return + } + } + + path := fmt.Sprintf("/contests/%v/problems/%v", *in.ContestID, in.ProblemID) + w.Header().Set("HX-Redirect", path) + w.WriteHeader(http.StatusOK) +} + +func (api *ApiContext) createProblemView(w http.ResponseWriter, r *http.Request) { + idStr := r.PathValue("id") + type probleminfo struct { + ID string + } + + info := probleminfo{ID: idStr} + templates.ExecuteTemplate(w, "createProblem.html", &info) +} + +func (api *ApiContext) getProblemByID(w http.ResponseWriter, r *http.Request) { + contestIDStr := r.PathValue("contestID") + contestID, err := strconv.Atoi(contestIDStr) + if err != nil { + http.Error(w, "Error en ruta", http.StatusBadRequest) + log.Println("Error", err) + return + } + + problemIDStr := r.PathValue("problemID") + problemID, err := strconv.Atoi(problemIDStr) + if err != nil { + http.Error(w, "Error en ruta", http.StatusBadRequest) + log.Println("Error", err) + return + } + + problem, err := api.Store.GetProblemByIDs(r.Context(), contestID, problemID) + if err != nil { + http.Error(w, "Error al buscar problema", http.StatusBadRequest) + log.Println("Error", err) + return + } + problem.SampleInputStr = string(problem.SampleInput) + problem.SampleOutStr = string(problem.SampleOut) + + type pageInfo struct { + store.Problem + ContestID int + Username string + } + + info := pageInfo{Problem: *problem, ContestID: contestID} + + + cookieUsername, err := r.Cookie("username") + if err == nil { + username := cookieUsername.Value + info.Username = username + } + + templates.ExecuteTemplate(w, "problem.html", &info) +} + +func (api *ApiContext) getProblemStatementByID(w http.ResponseWriter, r *http.Request) { + contestIDStr := r.PathValue("contestID") + contestID, err := strconv.Atoi(contestIDStr) + if err != nil { + http.Error(w, "Error en ruta", http.StatusBadRequest) + log.Println("Error", err) + return + } + + problemIDStr := r.PathValue("problemID") + problemID, err := strconv.Atoi(problemIDStr) + if err != nil { + http.Error(w, "Error en ruta", http.StatusBadRequest) + log.Println("Error", err) + return + } + + problem, err := api.Store.GetProblemByIDs(r.Context(), contestID, problemID) + if err != nil { + http.Error(w, "Error al buscar problema", http.StatusBadRequest) + log.Println("Error", err) + return + } + + w.Header().Set("Content-Type", "application/pdf") + w.WriteHeader(http.StatusOK) + w.Write(problem.Description) } diff --git a/api/submit.go b/api/submit.go index 5321c25..8b6e224 100644 --- a/api/submit.go +++ b/api/submit.go @@ -91,6 +91,35 @@ func (api *ApiContext) submitProgram(w http.ResponseWriter, r *http.Request) { // Metelo de nuevo *judge.WorkerQueueP <- worker + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(resMap) } + +func (api *ApiContext) getSubmissionByID(w http.ResponseWriter, r *http.Request) { + contestId := r.PathValue("contestID") + problemId := r.PathValue("problemID") + submitId := r.PathValue("submitID") + + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusCreated) + w.Write([]byte(fmt.Sprintf("

Not implemented: %v, %v, %v...

", contestId, problemId, submitId))) +} + +func (api *ApiContext) getSubmissions(w http.ResponseWriter, r *http.Request) { + contestId := r.PathValue("contestID") + problemId := r.PathValue("problemID") + + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusCreated) + w.Write([]byte(fmt.Sprintf("

Not implemented: %v, %v...

", contestId, problemId))) +} + +func (api *ApiContext) getLastSubmission(w http.ResponseWriter, r *http.Request) { + contestId := r.PathValue("contestID") + problemId := r.PathValue("problemID") + + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusCreated) + w.Write([]byte(fmt.Sprintf("

Not implemented: %v, %v...

", contestId, problemId))) +} diff --git a/api/user.go b/api/user.go index 22a0bd3..8eb2885 100644 --- a/api/user.go +++ b/api/user.go @@ -1,32 +1,130 @@ package api import ( - "encoding/json" + "fmt" "log" "net/http" + "lidsol.org/papeador/security" "lidsol.org/papeador/store" ) func (api *ApiContext) createUser(w http.ResponseWriter, r *http.Request) { var in store.User + r.ParseForm() + log.Println("CREATING USER") - if err := json.NewDecoder(r.Body).Decode(&in); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } + in.Email = r.FormValue("email") + in.Username = r.FormValue("username") + in.Password = r.FormValue("password") if err := api.Store.CreateUser(r.Context(), &in); err != nil { if err == store.ErrAlreadyExists { http.Error(w, "El usuario ya está registrado", http.StatusConflict) log.Println("El usuario ya está registrado") return + } else if err == security.ErrInvalidUsername { + http.Error(w, "El nombre de usuario solo puede contener caracteres, número y guiones", http.StatusUnprocessableEntity) + log.Println("La nombre de usuario que se intentó registrar es inválido") + return + } else if err == security.ErrInvalidPassword { + http.Error(w, "La contraseña debe contener al menos de 12 a 64 caracteres, una mayúscula, una minúscula, un número y un caracter especial, sin espacios", http.StatusUnprocessableEntity) + log.Println("La contraseña que se intentó registrar es insegura") + return + } else if err == security.ErrInvalidEmail { + http.Error(w, "El correo que se intenta registrar es inválido", http.StatusUnprocessableEntity) + log.Println("El correo que se intentó registrar es inválido") + return + } + http.Error(w, err.Error(), http.StatusInternalServerError) + log.Println("Error", err) + return + } + + http.SetCookie(w, &http.Cookie{ + Name: "session", + Value: in.JWT, + Path: "/", + }) + http.SetCookie(w, &http.Cookie{ + Name: "username", + Value: in.Username, + Path: "/", + }) + + w.Header().Set("HX-Redirect", "/") + w.WriteHeader(http.StatusOK) +} + +func (api *ApiContext) createUserView(w http.ResponseWriter, r *http.Request) { + type prueba struct { + Title string + } + a := prueba{Title: ""} + templates.ExecuteTemplate(w, "createUser.html", &a) +} + +func (api *ApiContext) loginUser(w http.ResponseWriter, r *http.Request) { + var in store.User + r.ParseForm() + + in.Username = r.FormValue("username") + in.Password = r.FormValue("password") + + if err := api.Store.Login(r.Context(), &in); err != nil { + if err == security.ErrInvalidCredentials { + http.Error(w, "Datos inválidos", http.StatusForbidden) + log.Println("Contraseña incorrecta") + return + } else if err == store.ErrNotFound { + http.Error(w, "Datos inválidos", http.StatusNotFound) + log.Println("Usuario invalido") + return } http.Error(w, err.Error(), http.StatusInternalServerError) - log.Println(err) + log.Println("Error", err) return } - w.Header().Set("Content-Type", "application/json") + + http.SetCookie(w, &http.Cookie{ + Name: "session", + Value: in.JWT, + Path: "/", + }) + http.SetCookie(w, &http.Cookie{ + Name: "username", + Value: in.Username, + Path: "/", + }) + + w.Header().Set("HX-Redirect", "/") + w.WriteHeader(http.StatusOK) +} + +func (api *ApiContext) logoutUser(w http.ResponseWriter, r *http.Request) { + cookie := http.Cookie{ + Name: "username", + Value: "", + MaxAge: 0, + } + http.SetCookie(w, &cookie) + + w.Header().Set("HX-Redirect", "/") + // w.WriteHeader(http.StatusOK) +} + +func (api *ApiContext) loginUserView(w http.ResponseWriter, r *http.Request) { + type prueba struct { + Title string + } + a := prueba{Title: ""} + templates.ExecuteTemplate(w, "login.html", &a) +} + +func (api *ApiContext) getUserByID(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + + w.Header().Set("Content-Type", "text/html") w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(in) -} \ No newline at end of file + w.Write([]byte(fmt.Sprintf("

Not implemented: %v...

", id))) +} diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 0000000..b9cf114 --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,39 @@ +package auth + +import ( + "net/http" + + "lidsol.org/papeador/security" +) + +func IsAuthenticated(r *http.Request) bool { + cookieJWT, err := r.Cookie("jwt") + if err != nil { + return false + } + cookieUsername, err := r.Cookie("username") + if err != nil { + return false + } + + ok, err := security.ValidateJWT(cookieJWT.Value, cookieUsername.Value) + if err != nil { + return false + } + if !ok { + return false + } + + return true +} + +func RequireAuth(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if !IsAuthenticated(r) { + w.Header().Set("HX-Redirect", "/login") + w.WriteHeader(http.StatusOK) + return + } + next(w, r) + } +} diff --git a/go.mod b/go.mod index 23159c7..05e2be6 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module lidsol.org/papeador -go 1.23.3 +go 1.24.0 toolchain go1.24.3 @@ -9,6 +9,7 @@ require ( github.com/containers/podman/v5 v5.6.0 github.com/cristalhq/jwt/v5 v5.4.0 github.com/docker/docker v28.3.2+incompatible + golang.org/x/crypto v0.43.0 modernc.org/sqlite v1.38.2 ) @@ -119,13 +120,12 @@ require ( go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/crypto v0.40.0 // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect - golang.org/x/net v0.42.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.34.0 // indirect - golang.org/x/term v0.33.0 // indirect - golang.org/x/text v0.27.0 // indirect + golang.org/x/net v0.45.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/term v0.36.0 // indirect + golang.org/x/text v0.30.0 // indirect golang.org/x/time v0.11.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect diff --git a/go.sum b/go.sum index 0e240cf..0d7cbdd 100644 --- a/go.sum +++ b/go.sum @@ -372,8 +372,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= -golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= @@ -387,8 +387,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= -golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -405,8 +405,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= +golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -419,8 +419,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -441,8 +441,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -452,8 +452,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= -golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -463,8 +463,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -479,8 +479,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= -golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/manifest.scm b/manifest.scm index c7b1882..d4bb5dc 100644 --- a/manifest.scm +++ b/manifest.scm @@ -1 +1 @@ -(specifications->manifest (list "sqlite" "go")) +(specifications->manifest (list "sqlite" "go" "podman" "pkg-config" "btrfs-progs-static" "gpgme" "go-github-com-containerd-btrfs-v2" "bat")) diff --git a/papeador.go b/papeador.go index 8f28976..ad52ab7 100644 --- a/papeador.go +++ b/papeador.go @@ -7,7 +7,6 @@ import ( "database/sql" "fmt" "log" - "net/http" "os/exec" "lidsol.org/papeador/api" diff --git a/schema.sql b/schema.sql index 8c2bdcd..dc89191 100644 --- a/schema.sql +++ b/schema.sql @@ -4,6 +4,7 @@ create table if not exists user ( user_id integer not null primary key autoincrement, username text not null unique, passhash text not null, + passsalt text not null, email text not null unique ); @@ -13,15 +14,22 @@ create table if not exists status ( ); create table if not exists contest ( - contest_id integer not null primary key autoincrement, - contest_name text not null unique - -- Fecha, organizador, + contest_id integer not null primary key autoincrement, + contest_name text not null unique, + start_date text not null, + end_date text not null, + organizer_id integer not null, + constraint fk_organizer + foreign key (organizer_id) + references user(user_id) ); +-- Not useful right now create table if not exists contest_has_problem ( contest_has_problem_id integer not null primary key autoincrement, contest_id integer not null, problem_id integer not null, + score integer not null, constraint fk_contest foreign key (contest_id) references contest(contest_id), @@ -34,6 +42,7 @@ create table if not exists problem ( problem_id integer not null primary key autoincrement, creator_id integer not null, problem_name text not null, + base_score integer not null, description blob not null, constraint fk_creator foreign key (creator_id) @@ -44,6 +53,7 @@ create table if not exists test_case ( test_case_id integer not null primary key autoincrement, problem_id integer not null, num_test_case integer not null, + time_limit integer not null, expected_out blob not null, given_input blob not null, constraint fk_problem @@ -55,6 +65,8 @@ create table if not exists submission ( submission_id integer not null primary key autoincrement, user_id integer not null, status_id integer not null, + score integer not null, + date text not null, problem_id integer not null, constraint fk_user foreign key (user_id) @@ -68,7 +80,7 @@ create table if not exists submission ( ); create table if not exists test_case_status ( - test_case_statu_id integer not null primary key autoincrement, + test_case_status_id integer not null primary key autoincrement, submission_id integer not null, test_case_id integer not null, status_id integer not null, diff --git a/security/ensure.go b/security/ensure.go new file mode 100644 index 0000000..2f38adb --- /dev/null +++ b/security/ensure.go @@ -0,0 +1,60 @@ +package security + +import ( + "net/mail" + "regexp" + "strings" + "errors" +) + +var ( + lower = regexp.MustCompile(`[a-z]`) + upper = regexp.MustCompile(`[A-Z]`) + digit = regexp.MustCompile(`\d`) + special = regexp.MustCompile(`[!@#\$%\^&\*\(\)\-\_\=\+\[\]\{\};:'",.<>\/\?\\\|` + "`~]") + space = regexp.MustCompile(`\s`) + uname = regexp.MustCompile(`^[A-Za-z0-9_-]{3,20}$`) + + ErrInvalidUsername = errors.New("invalid username") + ErrInvalidPassword = errors.New("invalid password") + ErrInvalidEmail = errors.New("invalid email") + ErrInvalidCredentials = errors.New("invalid credentials") +) + +func IsValidUsername(username string) error { + + if !uname.MatchString(username) { + return ErrInvalidUsername + } + + return nil +} + +func IsValidPassword(password string) error { + valid_length := len(password) >= 12 && len(password) <= 64 + + if !valid_length { + return ErrInvalidPassword + } + + if !(lower.MatchString(password) && + upper.MatchString(password) && + digit.MatchString(password) && + special.MatchString(password) && + !space.MatchString(password)) { + return ErrInvalidPassword + } + + return nil +} + +func ValidateEmail(email string) (string, error) { + email = strings.TrimSpace(email) + _, err := mail.ParseAddress(email) + + if err != nil { + return "", ErrInvalidEmail + } + + return strings.ToLower(email), nil +} diff --git a/security/hash.go b/security/hash.go new file mode 100644 index 0000000..22dc4e5 --- /dev/null +++ b/security/hash.go @@ -0,0 +1,68 @@ +package security + +import ( + "crypto/rand" + "log" + + "golang.org/x/crypto/argon2" +) + +type Params struct { + Memory uint32 + Iterations uint32 + Parallelism uint8 + SaltLength uint32 + KeyLength uint32 +} + +var Argon2Params *Params = &Params{ + Memory: 64 * 1024, + Iterations: 3, + Parallelism: 2, + SaltLength: 16, + KeyLength: 32, +} + +func VerifyHash(password, storedHash, salt []byte, p *Params) (bool, error) { + log.Println("HOLA") + otherHash, err := HashPasswordWithSalt(string(password), salt, p) + if err != nil { + return false, err + } + + return string(otherHash) == string(storedHash), nil +} + +func HashPasswordWithSalt(password string, salt []byte, p *Params) (hash []byte, err error) { + // Pass the plaintext password, salt and parameters to the argon2.IDKey + // function. This will generate a hash of the password using the Argon2id + // variant. + hash = argon2.IDKey([]byte(password), salt, p.Iterations, p.Memory, p.Parallelism, p.KeyLength) + + return hash, nil +} + +func HashPassword(password string, p *Params) (hash, salt []byte, err error) { + // Generate a cryptographically secure random salt. + salt, err = generateSalt(p.SaltLength) + if err != nil { + return nil, nil, err + } + + // Pass the plaintext password, salt and parameters to the argon2.IDKey + // function. This will generate a hash of the password using the Argon2id + // variant. + hash = argon2.IDKey([]byte(password), salt, p.Iterations, p.Memory, p.Parallelism, p.KeyLength) + + return hash, salt, nil +} + +func generateSalt( n uint32) ([]byte, error) { + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + return nil, err + } + + return b, nil +} diff --git a/security/jwt.go b/security/jwt.go new file mode 100644 index 0000000..4cd9cdd --- /dev/null +++ b/security/jwt.go @@ -0,0 +1,67 @@ +package security + +import ( + "encoding/json" + "os" + "strings" + "time" + + "github.com/cristalhq/jwt/v5" +) + +var secretKey []byte = []byte(os.Getenv("JWT_KEY")) + +func GenerateJWT(username string) (string, error) { + + signer, err := jwt.NewSignerHS(jwt.HS256, secretKey) + if err != nil { + return "", err + } + + claims := &jwt.RegisteredClaims{ + Audience: []string{"admin"}, + ID: "asdf", + Subject: strings.Clone(username), + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Second * 100)), + } + + builder := jwt.NewBuilder(signer) + + token, err := builder.Build(claims) + if err != nil { + return "", err + } + + return token.String(), nil +} + +// func ValidateJWT(tokenStr, username string) (bool, error) { +func ValidateJWT(tokenStr, username string) (bool, error) { + verifier, err := jwt.NewVerifierHS(jwt.HS256, secretKey) + if err != nil { + return false, err + } + + tokenBytes := []byte(tokenStr) + newToken, err := jwt.Parse(tokenBytes, verifier) + if err != nil { + return false, err + } + + err = verifier.Verify(newToken) + if err != nil { + return false, err + } + + var newClaims jwt.RegisteredClaims + errClaims := json.Unmarshal(newToken.Claims(), &newClaims) + if errClaims != nil { + return false, err + } + + if newClaims.IsSubject(username) { + return true, nil + } + + return false, nil +} diff --git a/store/sqlite.go b/store/sqlite.go index bfe2262..a771d37 100644 --- a/store/sqlite.go +++ b/store/sqlite.go @@ -3,6 +3,9 @@ package store import ( "context" "database/sql" + "log" + + "lidsol.org/papeador/security" ) type SQLiteStore struct { @@ -14,25 +17,79 @@ func NewSQLiteStore(db *sql.DB) Store { } func (s *SQLiteStore) CreateUser(ctx context.Context, u *User) error { - // Verify unique username/email - var username string - err := s.DB.QueryRowContext(ctx, "SELECT username FROM user WHERE username=? OR email=?", u.Username, u.Email).Scan(&username) + // Check for valid username + if err := security.IsValidUsername(u.Username); err != nil { + return err + } + + // Check for a valid email + if email, err := security.ValidateEmail(u.Email); err == nil { + u.Email = email // to_lower and trimmed + } else { + return err + } + + // Check for a secure password + if err := security.IsValidPassword(u.Password); err != nil { + return err + } + + // Verify duplicated Username/email + var duplicateUsername string + err := s.DB.QueryRowContext(ctx, "SELECT username FROM user WHERE username=? OR email=?", u.Username, u.Email).Scan(&duplicateUsername) if err == nil { return ErrAlreadyExists } else if err != sql.ErrNoRows { return err } - res, err := s.DB.ExecContext(ctx, "INSERT INTO user (username,passhash,email) VALUES (?, ?, ?)", u.Username, u.Passhash, u.Email) + // Password hashing + passhash, passsalt, err := security.HashPassword(u.Password, security.Argon2Params) if err != nil { return err } + + // Inserting user + res, err := s.DB.ExecContext(ctx, "INSERT INTO user (username,passhash,passsalt,email) VALUES (?, ?, ?, ?)", u.Username, passhash, passsalt, u.Email) + + if err != nil { + return err + } + + token, err := security.GenerateJWT(u.Username) + if err != nil { + return err + } + + if id, ierr := res.LastInsertId(); ierr == nil { u.UserID = id + u.JWT = token } return nil } +func (s *SQLiteStore) GetUserByID(ctx context.Context, id int) (string, error) { + username := "" + err := s.DB.QueryRowContext(ctx, "SELECT username, FROM user WHERE id=?", id).Scan(&username) + if err != nil { + return "", err + } + + return username, nil +} + +func (s *SQLiteStore) GetUserID(ctx context.Context, username string) (int, error) { + id := 0 + err := s.DB.QueryRowContext(ctx, "SELECT user_id FROM user WHERE username=?", username).Scan(&id) + if err != nil { + log.Println("ERR", err) + return -1, err + } + + return id, nil +} + func (s *SQLiteStore) CreateContest(ctx context.Context, c *Contest) error { var name string err := s.DB.QueryRowContext(ctx, "SELECT contest_name FROM contest WHERE contest_name=?", c.ContestName).Scan(&name) @@ -42,7 +99,7 @@ func (s *SQLiteStore) CreateContest(ctx context.Context, c *Contest) error { return err } - res, err := s.DB.ExecContext(ctx, "INSERT INTO contest (contest_name) VALUES (?)", c.ContestName) + res, err := s.DB.ExecContext(ctx, "INSERT INTO contest (contest_name, start_date, end_date, organizer_id) VALUES (?, ?, ?, ?)", c.ContestName, c.StartDate, c.EndDate, c.OrganizerID) if err != nil { return err } @@ -52,6 +109,76 @@ func (s *SQLiteStore) CreateContest(ctx context.Context, c *Contest) error { return nil } +func (s *SQLiteStore) GetContests(ctx context.Context) ([]Contest, error) { + rows, err := s.DB.QueryContext(ctx, "SELECT c.contest_id, c.contest_name, c.start_date, c.end_date, u.username FROM contest c JOIN user u ON c.organizer_id = u.user_id") + + if err != nil && err != sql.ErrNoRows { + return nil, err + } + defer rows.Close() + + var contests []Contest + + for rows.Next() { + var c Contest + if err := rows.Scan(&c.ContestID, &c.ContestName, &c.StartDate, &c.EndDate, &c.OrganizerName); err != nil { + return contests, err + } + contests = append(contests, c) + } + + // if err := rows.Err(); err != nil { + // return contests, nil + // } + + return contests, nil +} + +func (s *SQLiteStore) GetContestByName(ctx context.Context, name string) (Contest, error) { + var c Contest + err := s.DB.QueryRowContext(ctx, "SELECT contest_id, contest_name, start_date, end_date, organizer_id, FROM contest WHERE contest_name=?", name).Scan(&c.ContestID, &c.ContestName, &c.StartDate, &c.EndDate, &c.OrganizerID) + if err != nil { + return Contest{}, err + } + + return c, nil +} + +func (s *SQLiteStore) GetContestByID(ctx context.Context, id int) (Contest, error) { + var c Contest + err := s.DB.QueryRowContext(ctx, "SELECT c.contest_id, c.contest_name, c.start_date, c.end_date, c.organizer_id, u.username FROM contest c JOIN user u ON c.organizer_id = u.user_id WHERE c.contest_id=?", id).Scan(&c.ContestID, &c.ContestName, &c.StartDate, &c.EndDate, &c.OrganizerID, &c.OrganizerName) + if err != nil { + return Contest{}, err + } + + return c, nil +} + +func (s *SQLiteStore) GetContestProblems(ctx context.Context, id int) ([]Problem, error) { + rows, err := s.DB.Query("SELECT p.problem_id, c.contest_id, p.problem_name from problem p JOIN contest_has_problem c ON p.problem_id = c.problem_id WHERE c.contest_id = ?", id) + + if err != nil { + return nil, err + } + defer rows.Close() + + var problems []Problem + + for rows.Next() { + var p Problem + if err := rows.Scan(&p.ProblemID, &p.ContestID, &p.ProblemName); err != nil { + return problems, err + } + problems = append(problems, p) + } + + if err := rows.Err(); err != nil { + return problems, err + } + + return problems, nil +} + func (s *SQLiteStore) CreateProblem(ctx context.Context, p *Problem) error { if p.ContestID != nil { var cid int64 @@ -63,12 +190,91 @@ func (s *SQLiteStore) CreateProblem(ctx context.Context, p *Problem) error { } } - res, err := s.DB.ExecContext(ctx, "INSERT INTO problem (contest_id, creator_id, problem_name, description) VALUES (?, ?, ?, ?)", p.ContestID, p.CreatorID, p.ProblemName, p.Description) + res, err := s.DB.ExecContext(ctx, "INSERT INTO problem (creator_id, problem_name, base_score, description) VALUES (?, ?, ?, ?)", p.CreatorID, p.ProblemName, p.BaseScore, p.Description) + if err != nil { return err } + if id, ierr := res.LastInsertId(); ierr == nil { p.ProblemID = id + + _, err := s.DB.ExecContext(ctx, "INSERT INTO contest_has_problem (contest_id, problem_id, score) VALUES (?, ?, 0)", p.ContestID, p.ProblemID) + + if err != nil { + return err + } + + } + return nil +} + +func (s *SQLiteStore) GetProblemByIDs(ctx context.Context, contestID, problemID int) (*Problem, error) { + p := Problem{} + err := s.DB.QueryRowContext(ctx, "SELECT p.problem_id, p.problem_name, p.description from problem p JOIN contest_has_problem c ON p.problem_id = c.problem_id WHERE c.contest_id = ? AND p.problem_id = ?", contestID, problemID).Scan(&p.ProblemID, &p.ProblemName, &p.Description) + + if err == sql.ErrNoRows { + return nil, ErrNotFound + } else if err != nil { + return nil, err + } + log.Println("HOLA") + + err = s.DB.QueryRowContext(ctx, "SELECT t.expected_out, t.given_input from test_case t JOIN problem p ON p.problem_id = t.problem_id WHERE t.problem_id = ? ORDER BY t.num_test_case ASC LIMIT 1", p.ProblemID).Scan(&p.SampleOut, &p.SampleInput) + if err == sql.ErrNoRows { + return nil, ErrNotFound + } else if err != nil { + return nil, err + } + + return &p, nil +} + +func (s *SQLiteStore) CreateTestCase(ctx context.Context, t *TestCase) error { + var pid int64 + err := s.DB.QueryRowContext(ctx, "SELECT problem_id FROM problem WHERE problem_id=?", *&t.ProblemID).Scan(&pid) + if err == sql.ErrNoRows { + return ErrNotFound + } else if err != nil { + return err + } + + res, err := s.DB.ExecContext(ctx, "INSERT INTO test_case (problem_id, num_test_case, time_limit, expected_out, given_input) VALUES (?, ?, ?, ?, ?)", t.ProblemID, t.NumTestCase, t.TimeLimit, t.ExpectedOut, t.GivenInput) + + if err != nil { + return err + } + + if id, ierr := res.LastInsertId(); ierr == nil { + t.TestCaseID = id + } + return nil +} + +func (s *SQLiteStore) Login(ctx context.Context, u *User) error { + var username, storedHash, salt string + + err := s.DB.QueryRowContext(ctx, "SELECT username, passhash, passsalt FROM user WHERE username = ? OR email = ?", u.Username, u.Email).Scan(&username, &storedHash, &salt) + if err == sql.ErrNoRows { + return ErrNotFound } + if err != nil { + return err + } + //Verificar password + inputPass := []byte(u.Password) + + val, err := security.VerifyHash(inputPass, []byte(storedHash), []byte(salt), security.Argon2Params) + if err != nil { + return err + } + if !val { + log.Println("ERR", err) + return security.ErrInvalidCredentials + } + return nil + } + + diff --git a/store/store.go b/store/store.go index 9f629d4..9a88c05 100644 --- a/store/store.go +++ b/store/store.go @@ -13,21 +13,40 @@ var ( type User struct { UserID int64 `json:"user_id"` Username string `json:"username"` - Passhash string `json:"passhash"` + Password string `json:"password"` Email string `json:"email"` + JWT string `json:"jwt"` } type Contest struct { - ContestID int64 `json:"contest_id"` - ContestName string `json:"contest_name"` + ContestID int64 `json:"contest_id"` + ContestName string `json:"contest_name"` + StartDate string `json:"start_date"` + EndDate string `json:"end_date"` + OrganizerID int64 `json:"organizer_id"` + OrganizerName string `json:"organizer_name"` } type Problem struct { + ProblemID int64 `json:"problem_id"` + ContestID *int64 `json:"contest_id"` + CreatorID int64 `json:"creator_id"` + ProblemName string `json:"problem_name"` + BaseScore string `json:"base_score"` + Description []byte `json:"description"` + SampleInput []byte `json:"sample_input"` + SampleOut []byte `json:"sample_out"` + SampleInputStr string `json:"sample_input_str"` + SampleOutStr string `json:"sample_out_str"` +} + +type TestCase struct { + TestCaseID int64 `json:"test_case_id"` ProblemID int64 `json:"problem_id"` - ContestID *int64 `json:"contest_id"` - CreatorID int64 `json:"creator_id"` - ProblemName string `json:"problem_name"` - Description string `json:"description"` + NumTestCase int64 `json:"num_test_case"` + TimeLimit int64 `json:"time_limit"` + ExpectedOut []byte `json:"expected_out"` + GivenInput []byte `json:"given_input"` } // Store defines persistence operations used by the HTTP layer. @@ -35,4 +54,13 @@ type Store interface { CreateUser(ctx context.Context, u *User) error CreateContest(ctx context.Context, c *Contest) error CreateProblem(ctx context.Context, p *Problem) error + CreateTestCase(ctx context.Context, t *TestCase) error + Login(ctx context.Context, u *User) error + GetUserByID(ctx context.Context, id int) (string, error) + GetUserID(ctx context.Context, username string) (int, error) + GetContests(ctx context.Context) ([]Contest, error) + GetContestByName(ctx context.Context, name string) (Contest, error) + GetContestByID(ctx context.Context, id int) (Contest, error) + GetContestProblems(ctx context.Context, id int) ([]Problem, error) + GetProblemByIDs(ctx context.Context, contestID, problemID int) (*Problem, error) } diff --git a/templates/404.html b/templates/404.html new file mode 100644 index 0000000..cf8bef6 --- /dev/null +++ b/templates/404.html @@ -0,0 +1,16 @@ + + + + + + Crer un usuario + + + + + +

Error: 404... Dato no encontrado

+ + + + diff --git a/templates/contest.html b/templates/contest.html new file mode 100644 index 0000000..b43b7c5 --- /dev/null +++ b/templates/contest.html @@ -0,0 +1,184 @@ + + + + + + {{.ContestName}} + + + + + + + +
+
+
+
+
+

{{.ContestName}}

+

+ Organizado por: {{ .OrganizerName }} +

+
+ En progreso +
+ +
+
+

+ + Inicio: {{ .StartDate }} +

+
+
+

+ + Fin: {{ .EndDate }} +

+
+
+
+
+ +
+ +
+
+
+
+ Problemas +
+
+
+
+ {{ range .Problems}} + +
+ {{.ProblemID}} + {{ .ProblemName }} +
+
+ {{ end }} +
+
+
+
+ + +
+
+
+
+ Ranking +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#ParticipantePuntaje
+ + carlos_dev1850
+ + maria_codes1720
+ + juan_prog1650
4ana_algorithm1500
5pedro_coder1420
6lucia_tech1350
7diego_solver1200
8sofia_dev1100
9miguel_codes950
10elena_prog800
+
+
+
+
+
+
+ + + + + + diff --git a/templates/contests.html b/templates/contests.html new file mode 100644 index 0000000..820cf26 --- /dev/null +++ b/templates/contests.html @@ -0,0 +1,127 @@ + + + + +Listado de Concursos + + + + + + + + +
+
+ +

Concursos Disponibles

+ + + + + + + + + + + {{ range .Contests }} + + + + + + + + + {{ end }} +
Nombre del ConcursoOrganizadorInicioFinAcción
{{ .ContestName }}{{ .OrganizerName }}{{ .StartDate }}{{ .EndDate }}
+ + + + + diff --git a/templates/createContest.html b/templates/createContest.html new file mode 100644 index 0000000..4a41a91 --- /dev/null +++ b/templates/createContest.html @@ -0,0 +1,37 @@ + + + + + + Agrega un nuevo concurso + + + + + +

Añade un nuevo concurso

+
+
+ Nombre del concurso: +
+ +
+ Fecha de inicio: +
+ +
+ Fecha de final: +
+ +
+ +
+ +
+
+
+ + + diff --git a/templates/createProblem.html b/templates/createProblem.html new file mode 100644 index 0000000..27dd288 --- /dev/null +++ b/templates/createProblem.html @@ -0,0 +1,20 @@ + + + + + + Problema + + + + + +

Problema

+ + {{ .ProblemName }} + {{ .SampleInput }} + {{ .SampleOutput }} + + + + diff --git a/templates/createUser.html b/templates/createUser.html new file mode 100644 index 0000000..a33ec45 --- /dev/null +++ b/templates/createUser.html @@ -0,0 +1,37 @@ + + + + + + Crer un usuario + + + + + +

Crea un usuario {{.Title}}

+
+
+ Nombre de usuario: +
+ +
+ Email: +
+ +
+ Contraseña: +
+ +
+ +
+ +
+
+
+ + + diff --git a/templates/problem.html b/templates/problem.html new file mode 100644 index 0000000..9602629 --- /dev/null +++ b/templates/problem.html @@ -0,0 +1,78 @@ + + + + + + Papeador + + + + + + +

{{.ProblemName}}

+ +
+
+ +
+
+
+
Enunciado del Problema
+
+ +
+
+
+
+
+
Test cases
+
+
+
+
Input
+
{{.SampleInputStr}}
+
+
+
Output
+
{{.SampleOutStr}}
+
+
+
+ +
+
+
+ + + + +