Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c28f7a4
Adding password hashing and storing the hash
Morall0 Oct 8, 2025
94bfc54
store/sqlite.go: Indent with tabs
Francisco-Galindo Oct 14, 2025
fef5a2a
Add JWT (WIP)
Francisco-Galindo Oct 15, 2025
37bbe9a
Using jwt library
Francisco-Galindo Nov 4, 2025
464b7fe
JWT module
Francisco-Galindo Nov 23, 2025
e83f924
security functions to validate username, password and email
Morall0 Nov 23, 2025
d652a0a
schema.sql Add missing fields to contest table
Francisco-Galindo Nov 25, 2025
3ab4f26
Setup API endpoints, implementations missing
Francisco-Galindo Nov 25, 2025
c09db90
Merge security enhancements into api
Francisco-Galindo Nov 25, 2025
1b23b72
Esqueleto de API
Francisco-Galindo Nov 26, 2025
72aa063
Add VerifyHash function
Francisco-Galindo Nov 26, 2025
b936856
Add score data to database schema
Francisco-Galindo Nov 27, 2025
2d3a475
(Untested) Add endpoints for contests
Francisco-Galindo Nov 28, 2025
2d653e7
Login para usuarios
OhhhMyGucci Nov 28, 2025
1786b03
Agregar paquete de autentificación
Francisco-Galindo Nov 28, 2025
72cd308
Ignore files with trailing '~'
Francisco-Galindo Nov 28, 2025
8a4efd4
Fix query errors on contest creation
Francisco-Galindo Nov 28, 2025
9a5baef
Adding api for uploading and viewing problems
Francisco-Galindo Nov 28, 2025
1331e76
Use templates for some of the views
OhhhMyGucci Nov 28, 2025
9d7c727
Fix up links in some views
Francisco-Galindo Nov 28, 2025
a975104
Add problem view
Francisco-Galindo Nov 28, 2025
92708b7
Use authentication
Francisco-Galindo Nov 28, 2025
b78ece5
A
Francisco-Galindo Nov 28, 2025
bc3e939
Merge branch 'main' into htmx-api
Francisco-Galindo Nov 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ go.work.sum
.idea/
.vscode/
*.~
*~

# Database files
api/*.db
Expand Down
44 changes: 29 additions & 15 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,51 @@ package api

import (
"fmt"
"html/template"
"log"
"net/http"

"lidsol.org/papeador/auth"
"lidsol.org/papeador/store"
)

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))
}
107 changes: 101 additions & 6 deletions api/contest.go
Original file line number Diff line number Diff line change
@@ -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")
Expand All @@ -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)
}
65 changes: 65 additions & 0 deletions api/login.go
Original file line number Diff line number Diff line change
@@ -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)
}
}

Loading
Loading