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 }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | # |
+ Participante |
+ Puntaje |
+
+
+
+
+ |
+
+ |
+ carlos_dev |
+ 1850 |
+
+
+ |
+
+ |
+ maria_codes |
+ 1720 |
+
+
+ |
+
+ |
+ juan_prog |
+ 1650 |
+
+
+ | 4 |
+ ana_algorithm |
+ 1500 |
+
+
+ | 5 |
+ pedro_coder |
+ 1420 |
+
+
+ | 6 |
+ lucia_tech |
+ 1350 |
+
+
+ | 7 |
+ diego_solver |
+ 1200 |
+
+
+ | 8 |
+ sofia_dev |
+ 1100 |
+
+
+ | 9 |
+ miguel_codes |
+ 950 |
+
+
+ | 10 |
+ elena_prog |
+ 800 |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
+
+
+
+ | Nombre del Concurso |
+ Organizador |
+ Inicio |
+ Fin |
+ Acción |
+
+
+ {{ range .Contests }}
+
+
+ | {{ .ContestName }} |
+ {{ .OrganizerName }} |
+ {{ .StartDate }} |
+ {{ .EndDate }} |
+ |
+
+ {{ end }}
+
+
+
+
+
+
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
+
+
+
+
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}}
+
+
+
+
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}}
+
+
+
+
+
+
+
+
+
+
+
Input
+
{{.SampleInputStr}}
+
+
+
Output
+
{{.SampleOutStr}}
+
+
+
+
+
+
+
+
+
+
+
+