From c28f7a4038d8d9c06820d341c2c4eca22031511f Mon Sep 17 00:00:00 2001 From: Morall0 Date: Tue, 7 Oct 2025 19:45:56 -0600 Subject: [PATCH 01/22] Adding password hashing and storing the hash --- security/hash.go | 39 +++++++++++++++++++++++++++++++++++++++ store/sqlite.go | 19 ++++++++++++++++--- store/store.go | 2 +- 3 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 security/hash.go diff --git a/security/hash.go b/security/hash.go new file mode 100644 index 0000000..46eb31a --- /dev/null +++ b/security/hash.go @@ -0,0 +1,39 @@ +package security + +import ( + "crypto/rand" + "golang.org/x/crypto/argon2" +) + +type Params struct { + Memory uint32 + Iterations uint32 + Parallelism uint8 + SaltLength uint32 + KeyLength uint32 +} + +func HashPassword(password string, p *Params) (hash []byte, err error) { + // Generate a cryptographically secure random salt. + salt, err := generateSalt(p.SaltLength) + if err != nil { + return 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, 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/store/sqlite.go b/store/sqlite.go index 0414336..c141195 100644 --- a/store/sqlite.go +++ b/store/sqlite.go @@ -1,8 +1,11 @@ package store import ( - "context" - "database/sql" + "context" + "database/sql" + "log" + + "lidsol.org/papeador/security" ) type SQLiteStore struct{ @@ -23,7 +26,17 @@ func (s *SQLiteStore) CreateUser(ctx context.Context, u *User) error { return err } - res, err := s.DB.ExecContext(ctx, "INSERT INTO user (username,passhash,email) VALUES (?, ?, ?)", u.Username, u.Passhash, u.Email) + // Initializing params for Argon2 + p := &security.Params{ + Memory: 64 * 1024, + Iterations: 3, + Parallelism: 2, + SaltLength: 16, + KeyLength: 32, + } + + passhash, err := security.HashPassword(u.Password, p) + res, err := s.DB.ExecContext(ctx, "INSERT INTO user (username,passhash,email) VALUES (?, ?, ?)", u.Username, passhash, u.Email) if err != nil { return err } diff --git a/store/store.go b/store/store.go index 85808ac..c571665 100644 --- a/store/store.go +++ b/store/store.go @@ -13,7 +13,7 @@ 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"` } From 94bfc548296bc7c7503ccc26bf164080aac0afd2 Mon Sep 17 00:00:00 2001 From: Francisco-Galindo Date: Mon, 13 Oct 2025 19:43:54 -0600 Subject: [PATCH 02/22] store/sqlite.go: Indent with tabs --- manifest.scm | 2 +- store/sqlite.go | 112 ++++++++++++++++++++++++------------------------ 2 files changed, 57 insertions(+), 57 deletions(-) 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/store/sqlite.go b/store/sqlite.go index c141195..778b943 100644 --- a/store/sqlite.go +++ b/store/sqlite.go @@ -9,79 +9,79 @@ import ( ) type SQLiteStore struct{ - DB *sql.DB + DB *sql.DB } func NewSQLiteStore(db *sql.DB) Store { - return &SQLiteStore{DB: db} + return &SQLiteStore{DB: db} } 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) - if err == nil { - return ErrAlreadyExists - } else if err != sql.ErrNoRows { - return err - } + // 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) + if err == nil { + return ErrAlreadyExists + } else if err != sql.ErrNoRows { + return err + } // Initializing params for Argon2 p := &security.Params{ - Memory: 64 * 1024, - Iterations: 3, - Parallelism: 2, - SaltLength: 16, - KeyLength: 32, - } + Memory: 64 * 1024, + Iterations: 3, + Parallelism: 2, + SaltLength: 16, + KeyLength: 32, + } passhash, err := security.HashPassword(u.Password, p) - res, err := s.DB.ExecContext(ctx, "INSERT INTO user (username,passhash,email) VALUES (?, ?, ?)", u.Username, passhash, u.Email) - if err != nil { - return err - } - if id, ierr := res.LastInsertId(); ierr == nil { - u.UserID = id - } - return nil + res, err := s.DB.ExecContext(ctx, "INSERT INTO user (username,passhash,email) VALUES (?, ?, ?)", u.Username, passhash, u.Email) + if err != nil { + return err + } + if id, ierr := res.LastInsertId(); ierr == nil { + u.UserID = id + } + return 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) - if err == nil { - return ErrAlreadyExists - } else if err != sql.ErrNoRows { - return err - } + var name string + err := s.DB.QueryRowContext(ctx, "SELECT contest_name FROM contest WHERE contest_name=?", c.ContestName).Scan(&name) + if err == nil { + return ErrAlreadyExists + } else if err != sql.ErrNoRows { + return err + } - res, err := s.DB.ExecContext(ctx, "INSERT INTO contest (contest_name) VALUES (?)", c.ContestName) - if err != nil { - return err - } - if id, ierr := res.LastInsertId(); ierr == nil { - c.ContestID = id - } - return nil + res, err := s.DB.ExecContext(ctx, "INSERT INTO contest (contest_name) VALUES (?)", c.ContestName) + if err != nil { + return err + } + if id, ierr := res.LastInsertId(); ierr == nil { + c.ContestID = id + } + return nil } func (s *SQLiteStore) CreateProblem(ctx context.Context, p *Problem) error { - if p.ContestID != nil { - var cid int64 - err := s.DB.QueryRowContext(ctx, "SELECT contest_id FROM contest WHERE contest_id=?", *p.ContestID).Scan(&cid) - if err == sql.ErrNoRows { - return ErrNotFound - } else if err != nil { - return err - } - } + if p.ContestID != nil { + var cid int64 + err := s.DB.QueryRowContext(ctx, "SELECT contest_id FROM contest WHERE contest_id=?", *p.ContestID).Scan(&cid) + if err == sql.ErrNoRows { + return ErrNotFound + } else if err != nil { + return err + } + } - 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) - if err != nil { - return err - } - if id, ierr := res.LastInsertId(); ierr == nil { - p.ProblemID = id - } - return nil + 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) + if err != nil { + return err + } + if id, ierr := res.LastInsertId(); ierr == nil { + p.ProblemID = id + } + return nil } From fef5a2ad3f669c894de22746a5762fd5c572cbcd Mon Sep 17 00:00:00 2001 From: Francisco-Galindo Date: Tue, 14 Oct 2025 19:04:38 -0600 Subject: [PATCH 03/22] Add JWT (WIP) --- api/contest.go | 24 ++++++++++++++++++++++-- go.mod | 10 +++++++--- go.sum | 8 ++++++-- store/sqlite.go | 10 +++++++++- store/store.go | 37 +++++++++++++++++++------------------ 5 files changed, 63 insertions(+), 26 deletions(-) diff --git a/api/contest.go b/api/contest.go index c7f1a3c..3eb8229 100644 --- a/api/contest.go +++ b/api/contest.go @@ -5,17 +5,37 @@ import ( "log" "net/http" + "lidsol.org/papeador/security" "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 + var in ContestRequestContent if err := json.NewDecoder(r.Body).Decode(&in); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - if err := api.Store.CreateContest(r.Context(), &in); err != nil { + ok, err := security.ValidateJWT(in.JWT, in.Username) + if err != nil { + log.Println("WHATAADF") + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if !ok { + http.Error(w, "Token inválida", http.StatusUnauthorized) + return + } + + contData := store.Contest{ContestID: in.ContestID, ContestName: in.ContestName} + + if err := api.Store.CreateContest(r.Context(), &contData); 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") diff --git a/go.mod b/go.mod index 0c1c08a..1881bd8 100644 --- a/go.mod +++ b/go.mod @@ -1,19 +1,23 @@ module lidsol.org/papeador -go 1.23.0 +go 1.24.0 toolchain go1.24.3 -require modernc.org/sqlite v1.38.2 +require ( + golang.org/x/crypto v0.43.0 + modernc.org/sqlite v1.38.2 +) require ( github.com/dustin/go-humanize v1.0.1 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect - golang.org/x/sys v0.34.0 // indirect + golang.org/x/sys v0.37.0 // indirect modernc.org/libc v1.66.3 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect diff --git a/go.sum b/go.sum index aac187a..2a8dac3 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -10,6 +12,8 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +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-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= @@ -17,8 +21,8 @@ golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -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/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM= diff --git a/store/sqlite.go b/store/sqlite.go index 778b943..36fd429 100644 --- a/store/sqlite.go +++ b/store/sqlite.go @@ -3,7 +3,7 @@ package store import ( "context" "database/sql" - "log" + // "log" "lidsol.org/papeador/security" ) @@ -37,11 +37,19 @@ func (s *SQLiteStore) CreateUser(ctx context.Context, u *User) error { passhash, err := security.HashPassword(u.Password, p) res, err := s.DB.ExecContext(ctx, "INSERT INTO user (username,passhash,email) VALUES (?, ?, ?)", u.Username, passhash, u.Email) + + if err != nil { + return err + } + + token, err := security.GenerateJWT(username) if err != nil { return err } + if id, ierr := res.LastInsertId(); ierr == nil { u.UserID = id + u.JWT = token } return nil } diff --git a/store/store.go b/store/store.go index c571665..2b85223 100644 --- a/store/store.go +++ b/store/store.go @@ -1,38 +1,39 @@ package store import ( - "context" - "errors" + "context" + "errors" ) var ( - ErrAlreadyExists = errors.New("already exists") - ErrNotFound = errors.New("not found") + ErrAlreadyExists = errors.New("already exists") + ErrNotFound = errors.New("not found") ) type User struct { - UserID int64 `json:"user_id"` - Username string `json:"username"` - Password string `json:"password"` - Email string `json:"email"` + UserID int64 `json:"user_id"` + Username string `json:"username"` + 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"` } type Problem struct { - ProblemID int64 `json:"problem_id"` - ContestID *int64 `json:"contest_id"` - CreatorID int64 `json:"creator_id"` - ProblemName string `json:"problem_name"` - Description string `json:"description"` + ProblemID int64 `json:"problem_id"` + ContestID *int64 `json:"contest_id"` + CreatorID int64 `json:"creator_id"` + ProblemName string `json:"problem_name"` + Description string `json:"description"` } // Store defines persistence operations used by the HTTP layer. type Store interface { - CreateUser(ctx context.Context, u *User) error - CreateContest(ctx context.Context, c *Contest) error - CreateProblem(ctx context.Context, p *Problem) error + CreateUser(ctx context.Context, u *User) error + CreateContest(ctx context.Context, c *Contest) error + CreateProblem(ctx context.Context, p *Problem) error } From 37bbe9a6761956436b6274916b2c17438bbdc605 Mon Sep 17 00:00:00 2001 From: Francisco-Galindo Date: Mon, 3 Nov 2025 20:11:45 -0600 Subject: [PATCH 04/22] Using jwt library --- api/contest.go | 3 +++ api/user.go | 4 +++- go.mod | 3 ++- go.sum | 2 ++ store/sqlite.go | 8 ++++---- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/api/contest.go b/api/contest.go index 3eb8229..c93c045 100644 --- a/api/contest.go +++ b/api/contest.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "fmt" "log" "net/http" @@ -22,6 +23,8 @@ func (api *ApiContext) createContest(w http.ResponseWriter, r *http.Request) { return } + fmt.Printf("E: %v\n", in) + fmt.Printf("E: %v\n", in.Username) ok, err := security.ValidateJWT(in.JWT, in.Username) if err != nil { log.Println("WHATAADF") diff --git a/api/user.go b/api/user.go index 22a0bd3..a3803ea 100644 --- a/api/user.go +++ b/api/user.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "fmt" "log" "net/http" @@ -16,6 +17,7 @@ func (api *ApiContext) createUser(w http.ResponseWriter, r *http.Request) { return } + fmt.Printf("E: %v\n", in) if err := api.Store.CreateUser(r.Context(), &in); err != nil { if err == store.ErrAlreadyExists { http.Error(w, "El usuario ya está registrado", http.StatusConflict) @@ -29,4 +31,4 @@ func (api *ApiContext) createUser(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(in) -} \ No newline at end of file +} diff --git a/go.mod b/go.mod index 1881bd8..c6a5ebd 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,14 @@ go 1.24.0 toolchain go1.24.3 require ( + github.com/golang-jwt/jwt/v5 v5.3.0 golang.org/x/crypto v0.43.0 modernc.org/sqlite v1.38.2 ) require ( + github.com/cristalhq/jwt/v5 v5.4.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect diff --git a/go.sum b/go.sum index 2a8dac3..ffecb9c 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/cristalhq/jwt/v5 v5.4.0 h1:Wxi1TocFHaijyV608j7v7B9mPc4ZNjvWT3LKBO0d4QI= +github.com/cristalhq/jwt/v5 v5.4.0/go.mod h1:+b/BzaCWEpFDmXxspJ5h4SdJ1N/45KMjKOetWzmHvDA= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= diff --git a/store/sqlite.go b/store/sqlite.go index 36fd429..8481d58 100644 --- a/store/sqlite.go +++ b/store/sqlite.go @@ -17,9 +17,9 @@ 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) + // Verify unique duplicateUsername/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 { @@ -42,7 +42,7 @@ func (s *SQLiteStore) CreateUser(ctx context.Context, u *User) error { return err } - token, err := security.GenerateJWT(username) + token, err := security.GenerateJWT(u.Username) if err != nil { return err } From 464b7fef63c15b1dd682048a09bab51711e46c6f Mon Sep 17 00:00:00 2001 From: Francisco-Galindo Date: Sat, 22 Nov 2025 23:20:21 -0600 Subject: [PATCH 05/22] JWT module --- security/jwt.go | 66 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 security/jwt.go diff --git a/security/jwt.go b/security/jwt.go new file mode 100644 index 0000000..d8ad556 --- /dev/null +++ b/security/jwt.go @@ -0,0 +1,66 @@ +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) { + 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 +} From e83f924f948ebbb07cb661f122608229c132e0ec Mon Sep 17 00:00:00 2001 From: Morall0 Date: Sun, 23 Nov 2025 17:43:29 -0600 Subject: [PATCH 06/22] security functions to validate username, password and email --- api/user.go | 13 ++++++++++ security/ensure.go | 59 ++++++++++++++++++++++++++++++++++++++++++++++ store/sqlite.go | 27 +++++++++++++++++++-- 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 security/ensure.go diff --git a/api/user.go b/api/user.go index a3803ea..6ec4fbe 100644 --- a/api/user.go +++ b/api/user.go @@ -6,6 +6,7 @@ import ( "log" "net/http" + "lidsol.org/papeador/security" "lidsol.org/papeador/store" ) @@ -23,6 +24,18 @@ func (api *ApiContext) createUser(w http.ResponseWriter, r *http.Request) { 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(err) diff --git a/security/ensure.go b/security/ensure.go new file mode 100644 index 0000000..ee434f1 --- /dev/null +++ b/security/ensure.go @@ -0,0 +1,59 @@ +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") +) + +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/store/sqlite.go b/store/sqlite.go index 8481d58..225ee58 100644 --- a/store/sqlite.go +++ b/store/sqlite.go @@ -17,7 +17,24 @@ func NewSQLiteStore(db *sql.DB) Store { } func (s *SQLiteStore) CreateUser(ctx context.Context, u *User) error { - // Verify unique duplicateUsername/email + // 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 { @@ -35,7 +52,13 @@ func (s *SQLiteStore) CreateUser(ctx context.Context, u *User) error { KeyLength: 32, } - passhash, err := security.HashPassword(u.Password, p) + // Password hashing + passhash, err := security.HashPassword(u.Password, p); + if err != nil { + return err + } + + // Inserting user res, err := s.DB.ExecContext(ctx, "INSERT INTO user (username,passhash,email) VALUES (?, ?, ?)", u.Username, passhash, u.Email) if err != nil { From d652a0ab0f9b21067174c56f9c059fa460688ceb Mon Sep 17 00:00:00 2001 From: Francisco-Galindo Date: Tue, 25 Nov 2025 09:11:23 -0600 Subject: [PATCH 07/22] schema.sql Add missing fields to contest table --- schema.sql | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/schema.sql b/schema.sql index 8c2bdcd..6975a1f 100644 --- a/schema.sql +++ b/schema.sql @@ -14,14 +14,20 @@ 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_name text not null unique, + fecha_inicio date not null, + fecha_inicio date not null, + organizador integer not null, + constraint fk_organizador + foreign key (organizador_id) + references user(user_id) ); 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), @@ -68,7 +74,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, From 3ab4f26aee9e017a5383c81c8bb782c51605b400 Mon Sep 17 00:00:00 2001 From: Francisco-Galindo Date: Tue, 25 Nov 2025 09:09:40 -0600 Subject: [PATCH 08/22] Setup API endpoints, implementations missing --- api/api.go | 30 ++++++++++++++++-------------- api/contest.go | 15 +++++++++++++++ api/problem.go | 18 ++++++++++++++++++ api/submit.go | 29 +++++++++++++++++++++++++++++ api/user.go | 11 ++++++++++- schema.sql | 7 ++++--- 6 files changed, 92 insertions(+), 18 deletions(-) diff --git a/api/api.go b/api/api.go index de81aa5..8828ff3 100644 --- a/api/api.go +++ b/api/api.go @@ -17,20 +17,22 @@ func API(s store.Store) { } mux := http.NewServeMux() - 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(":8000", mux)) -} + mux.HandleFunc("POST /users", apiCtx.createUser) + mux.HandleFunc("GET /users/{id}", apiCtx.getUserByID) -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 /contests", apiCtx.createContest) + mux.HandleFunc("GET /contests", apiCtx.getContests) + mux.HandleFunc("GET /contests/{id}", apiCtx.getContestByID) + + mux.HandleFunc("POST /contests/{id}/problems", apiCtx.createProblem) + mux.HandleFunc("GET /contests/{id}/problems", apiCtx.getProblems) + mux.HandleFunc("GET /contests/{contestID}/problems/{problemID}", apiCtx.getProblemByID) + + mux.HandleFunc("POST /contests/{constestID}/problems/{problemID}/submit", apiCtx.submitProgram) + mux.HandleFunc("GET /contests/{constestID}/problems/{problemID}/submit/{submitID}", apiCtx.getSubmissionByID) + mux.HandleFunc("GET /contests/{constestID}/problems/{problemID}/submit", apiCtx.getSubmissions) + mux.HandleFunc("GET /contests/{constestID}/problems/{problemID}/last-submit", apiCtx.getLastSubmission) + + log.Fatal(http.ListenAndServe(":8000", mux)) } diff --git a/api/contest.go b/api/contest.go index c7f1a3c..88d6b2d 100644 --- a/api/contest.go +++ b/api/contest.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "fmt" "log" "net/http" @@ -30,3 +31,17 @@ func (api *ApiContext) createContest(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(in) } + +func (api *ApiContext) getContests(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusCreated) + w.Write([]byte("

Not implemented:...

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

Not implemented: %v...

", id))) +} diff --git a/api/problem.go b/api/problem.go index faa15ca..5a6e872 100644 --- a/api/problem.go +++ b/api/problem.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "fmt" "log" "net/http" @@ -30,3 +31,20 @@ func (api *ApiContext) createProblem(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(in) } + +func (api *ApiContext) getProblems(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusCreated) + w.Write([]byte(fmt.Sprintf("

Not implemented: %v...

", id))) +} + +func (api *ApiContext) getProblemByID(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/submit.go b/api/submit.go index b3bf405..0138eb8 100644 --- a/api/submit.go +++ b/api/submit.go @@ -59,6 +59,35 @@ func (api *ApiContext) SubmitProgram(w http.ResponseWriter, r *http.Request) { resp := stdoutBuf.String() + "\n" log.Printf("Resultado: %v", resp) + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(resp)) } + +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..f098890 100644 --- a/api/user.go +++ b/api/user.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "fmt" "log" "net/http" @@ -29,4 +30,12 @@ func (api *ApiContext) createUser(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(in) -} \ No newline at end of file +} + +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) + w.Write([]byte(fmt.Sprintf("

Not implemented: %v...

", id))) +} diff --git a/schema.sql b/schema.sql index 6975a1f..65b5686 100644 --- a/schema.sql +++ b/schema.sql @@ -15,14 +15,15 @@ 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_inicio date not null, - fecha_inicio date not null, - organizador integer not null, + start_date date not null, + end_date date not null, + organizer integer not null, constraint fk_organizador foreign key (organizador_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, From 1b23b7252eff823dd88fb3ce1290d76f9dd26075 Mon Sep 17 00:00:00 2001 From: Francisco-Galindo Date: Wed, 26 Nov 2025 15:37:34 -0600 Subject: [PATCH 09/22] Esqueleto de API --- api/api.go | 14 ++++++++++++-- api/contest.go | 10 ++++------ api/submit.go | 2 +- api/user.go | 36 ++++++++++++++++++++++++++---------- papeador.go | 20 +++++++++----------- schema.sql | 14 +++++++------- security/jwt.go | 9 +++++---- store/sqlite.go | 15 ++++++++++++++- store/store.go | 2 ++ 9 files changed, 80 insertions(+), 42 deletions(-) diff --git a/api/api.go b/api/api.go index 8828ff3..3986245 100644 --- a/api/api.go +++ b/api/api.go @@ -1,6 +1,8 @@ package api import ( + "fmt" + "html/template" "log" "net/http" @@ -11,14 +13,22 @@ type ApiContext struct { Store store.Store } -func API(s 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("GET /", apiCtx.getContests) + + mux.HandleFunc("POST /login", apiCtx.createUser) + mux.HandleFunc("GET /login", apiCtx.createUser) + mux.HandleFunc("POST /users", apiCtx.createUser) + mux.HandleFunc("GET /users", apiCtx.createUserView) mux.HandleFunc("GET /users/{id}", apiCtx.getUserByID) mux.HandleFunc("POST /contests", apiCtx.createContest) @@ -34,5 +44,5 @@ func API(s store.Store) { mux.HandleFunc("GET /contests/{constestID}/problems/{problemID}/submit", apiCtx.getSubmissions) mux.HandleFunc("GET /contests/{constestID}/problems/{problemID}/last-submit", apiCtx.getLastSubmission) - log.Fatal(http.ListenAndServe(":8000", mux)) + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), mux)) } diff --git a/api/contest.go b/api/contest.go index bdc7122..11eff7c 100644 --- a/api/contest.go +++ b/api/contest.go @@ -13,7 +13,7 @@ import ( type ContestRequestContent struct { store.Contest Username string `json:"username"` - JWT string `json:"jwt"` + JWT string `json:"jwt"` } func (api *ApiContext) createContest(w http.ResponseWriter, r *http.Request) { @@ -23,11 +23,8 @@ func (api *ApiContext) createContest(w http.ResponseWriter, r *http.Request) { return } - fmt.Printf("E: %v\n", in) - fmt.Printf("E: %v\n", in.Username) - ok, err := security.ValidateJWT(in.JWT, in.Username) + ok, err := security.ValidateJWT(in.JWT) if err != nil { - log.Println("WHATAADF") http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -55,9 +52,10 @@ func (api *ApiContext) createContest(w http.ResponseWriter, r *http.Request) { } func (api *ApiContext) getContests(w http.ResponseWriter, r *http.Request) { + log.Println("getcontests") w.Header().Set("Content-Type", "text/html") w.WriteHeader(http.StatusCreated) - w.Write([]byte("

Not implemented:...

")) + w.Write([]byte("

getContests: Not implemented:...

")) } func (api *ApiContext) getContestByID(w http.ResponseWriter, r *http.Request) { diff --git a/api/submit.go b/api/submit.go index 0138eb8..595bc59 100644 --- a/api/submit.go +++ b/api/submit.go @@ -11,7 +11,7 @@ import ( "github.com/containers/podman/v5/pkg/bindings/containers" ) -func (api *ApiContext) SubmitProgram(w http.ResponseWriter, r *http.Request) { +func (api *ApiContext) submitProgram(w http.ResponseWriter, r *http.Request) { file, fileHeader, err := r.FormFile("program") if err != nil { log.Printf("Could not get form file: %v\n", err) diff --git a/api/user.go b/api/user.go index e06415d..db72da2 100644 --- a/api/user.go +++ b/api/user.go @@ -1,7 +1,6 @@ package api import ( - "encoding/json" "fmt" "log" "net/http" @@ -12,13 +11,12 @@ import ( func (api *ApiContext) createUser(w http.ResponseWriter, r *http.Request) { var in store.User + r.ParseForm() - 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") - fmt.Printf("E: %v\n", in) if err := api.Store.CreateUser(r.Context(), &in); err != nil { if err == store.ErrAlreadyExists { http.Error(w, "El usuario ya está registrado", http.StatusConflict) @@ -38,12 +36,30 @@ func (api *ApiContext) createUser(w http.ResponseWriter, r *http.Request) { return } http.Error(w, err.Error(), http.StatusInternalServerError) - log.Println(err) + log.Println("Error", err) return } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(in) + log.Println("SI SE PUDO REGISTRAR") + 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: "TITULO"} + templates.ExecuteTemplate(w, "createUser.html", &a) } func (api *ApiContext) getUserByID(w http.ResponseWriter, r *http.Request) { diff --git a/papeador.go b/papeador.go index eb544d3..09805b3 100644 --- a/papeador.go +++ b/papeador.go @@ -2,11 +2,11 @@ package main import ( "flag" + "strings" "database/sql" "fmt" "log" - "net/http" "os/exec" "lidsol.org/papeador/api" @@ -16,7 +16,7 @@ import ( _ "modernc.org/sqlite" ) -func testDB() { +func testDB(port int) { db, err := sql.Open("sqlite", "test.db") if err != nil { log.Fatal(err) @@ -30,7 +30,7 @@ func testDB() { log.Fatal(err) } s := store.NewSQLiteStore(db) - api.API(s) + api.API(s, port) } func main() { @@ -40,14 +40,12 @@ func main() { flag.Parse() uri := *uriPtr - _, err := judge.ConnectToPodman(uri) - if err != nil { - log.Fatalf("Could not connect to Podman: %v", err) + for _, u := range strings.Split(uri, " ") { + _, err := judge.ConnectToPodman(u) + if err != nil { + // log.Fatalf("Could not connect to Podman: %v", err) + } } - mux := http.NewServeMux() - - mux.HandleFunc("POST /program", api.SubmitProgram) - log.Printf("Starting server at :%v\n", port) - http.ListenAndServe(fmt.Sprintf(":%v", port), mux) + testDB(port) } diff --git a/schema.sql b/schema.sql index 65b5686..7097800 100644 --- a/schema.sql +++ b/schema.sql @@ -13,13 +13,13 @@ 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, - start_date date not null, - end_date date not null, - organizer integer not null, - constraint fk_organizador - foreign key (organizador_id) + 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) ); diff --git a/security/jwt.go b/security/jwt.go index d8ad556..27ebdbd 100644 --- a/security/jwt.go +++ b/security/jwt.go @@ -35,7 +35,8 @@ func GenerateJWT(username string) (string, error) { return token.String(), nil } -func ValidateJWT(tokenStr, username string) (bool, error) { +// func ValidateJWT(tokenStr, username string) (bool, error) { +func ValidateJWT(tokenStr string) (bool, error) { verifier, err := jwt.NewVerifierHS(jwt.HS256, secretKey) if err != nil { return false, err @@ -58,9 +59,9 @@ func ValidateJWT(tokenStr, username string) (bool, error) { return false, err } - if newClaims.IsSubject(username) { - return true, nil - } + // if newClaims.IsSubject(username) { + // return true, nil + // } return false, nil } diff --git a/store/sqlite.go b/store/sqlite.go index 0b02475..6673ea3 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 { @@ -74,6 +77,16 @@ func (s *SQLiteStore) CreateUser(ctx context.Context, u *User) error { 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) 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) @@ -83,7 +96,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) VALUES (?p)", c.ContestName) if err != nil { return err } diff --git a/store/store.go b/store/store.go index 2b85223..cf358f6 100644 --- a/store/store.go +++ b/store/store.go @@ -21,6 +21,8 @@ type User struct { type Contest struct { ContestID int64 `json:"contest_id"` ContestName string `json:"contest_name"` + StartDate string `json:"start_date"` + EndDate string `json:"end_date"` } type Problem struct { From 72aa063c551daf7075adbac6984fe82ab6abe6cf Mon Sep 17 00:00:00 2001 From: Francisco-Galindo Date: Wed, 26 Nov 2025 15:54:20 -0600 Subject: [PATCH 10/22] Add VerifyHash function --- security/hash.go | 19 +++++++++++++++++++ store/sqlite.go | 10 +--------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/security/hash.go b/security/hash.go index 46eb31a..110253e 100644 --- a/security/hash.go +++ b/security/hash.go @@ -1,7 +1,9 @@ package security import ( + "bytes" "crypto/rand" + "golang.org/x/crypto/argon2" ) @@ -13,6 +15,23 @@ type Params struct { KeyLength uint32 } +var Argon2Params *Params = &Params{ + Memory: 64 * 1024, + Iterations: 3, + Parallelism: 2, + SaltLength: 16, + KeyLength: 32, +} + +func VerifyHash(password string, hash []byte, p *Params) (bool, error) { + givenHash, err := HashPassword(password, p) + if err != nil { + return false, err + } + + return bytes.Equal(givenHash, hash), nil +} + func HashPassword(password string, p *Params) (hash []byte, err error) { // Generate a cryptographically secure random salt. salt, err := generateSalt(p.SaltLength) diff --git a/store/sqlite.go b/store/sqlite.go index 6673ea3..6b60373 100644 --- a/store/sqlite.go +++ b/store/sqlite.go @@ -43,17 +43,9 @@ func (s *SQLiteStore) CreateUser(ctx context.Context, u *User) error { return err } - // Initializing params for Argon2 - p := &security.Params{ - Memory: 64 * 1024, - Iterations: 3, - Parallelism: 2, - SaltLength: 16, - KeyLength: 32, - } // Password hashing - passhash, err := security.HashPassword(u.Password, p); + passhash, err := security.HashPassword(u.Password, security.Argon2Params); if err != nil { return err } From b936856b98819869c615ade121ecd926c7f20a85 Mon Sep 17 00:00:00 2001 From: Francisco-Galindo Date: Thu, 27 Nov 2025 16:13:30 -0600 Subject: [PATCH 11/22] Add score data to database schema --- schema.sql | 3 +++ 1 file changed, 3 insertions(+) diff --git a/schema.sql b/schema.sql index 7097800..092a4ec 100644 --- a/schema.sql +++ b/schema.sql @@ -41,6 +41,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) @@ -62,6 +63,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) From 2d3a4752ad247c7f39f41dfdd16f0585ac452fb4 Mon Sep 17 00:00:00 2001 From: Francisco-Galindo Date: Thu, 27 Nov 2025 21:11:49 -0600 Subject: [PATCH 12/22] (Untested) Add endpoints for contests --- api/api.go | 3 +- api/contest.go | 69 +++++++++++++++++++++------ api/user.go | 3 +- security/jwt.go | 8 ++-- store/sqlite.go | 91 +++++++++++++++++++++++++++++++++--- store/store.go | 19 ++++++-- templates/404.html | 16 +++++++ templates/contest.html | 16 +++++++ templates/createContest.html | 37 +++++++++++++++ templates/createUser.html | 37 +++++++++++++++ 10 files changed, 268 insertions(+), 31 deletions(-) create mode 100644 templates/404.html create mode 100644 templates/contest.html create mode 100644 templates/createContest.html create mode 100644 templates/createUser.html diff --git a/api/api.go b/api/api.go index 3986245..6486b48 100644 --- a/api/api.go +++ b/api/api.go @@ -31,7 +31,8 @@ func API(s store.Store, port int) { mux.HandleFunc("GET /users", apiCtx.createUserView) mux.HandleFunc("GET /users/{id}", apiCtx.getUserByID) - mux.HandleFunc("POST /contests", apiCtx.createContest) + mux.HandleFunc("POST /contests/new", apiCtx.createContest) + mux.HandleFunc("GET /contests/new", apiCtx.createContestView) mux.HandleFunc("GET /contests", apiCtx.getContests) mux.HandleFunc("GET /contests/{id}", apiCtx.getContestByID) diff --git a/api/contest.go b/api/contest.go index 11eff7c..8cc4aa7 100644 --- a/api/contest.go +++ b/api/contest.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "net/http" + "strconv" "lidsol.org/papeador/security" "lidsol.org/papeador/store" @@ -17,25 +18,30 @@ type ContestRequestContent struct { } func (api *ApiContext) createContest(w http.ResponseWriter, r *http.Request) { - var in ContestRequestContent + var in store.Contest if err := json.NewDecoder(r.Body).Decode(&in); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - ok, err := security.ValidateJWT(in.JWT) + 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 + + id, err := api.Store.GetUserID(r.Context(), username) + + // We shouldn't ever get here, handle it anyways if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - if !ok { - http.Error(w, "Token inválida", http.StatusUnauthorized) + http.Error(w, "This user does not exist", http.StatusNotFound) return } - contData := store.Contest{ContestID: in.ContestID, ContestName: in.ContestName} + in.OrganizerID = int64(id) - if err := api.Store.CreateContest(r.Context(), &contData); err != nil { + 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") @@ -51,17 +57,52 @@ func (api *ApiContext) createContest(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(in) } +func (api *ApiContext) createContestView(w http.ResponseWriter, r *http.Request) { + templates.ExecuteTemplate(w, "createContext.html", nil) +} + func (api *ApiContext) getContests(w http.ResponseWriter, r *http.Request) { - log.Println("getcontests") + type contestsInfo struct { + contests []store.Contest + } + + var info contestsInfo + contests, err := api.Store.GetContests(r.Context()) + info.contests = contests + + if err != nil { + w.WriteHeader(http.StatusNotFound) + templates.ExecuteTemplate(w, "404.html", &info) + return + } + w.Header().Set("Content-Type", "text/html") w.WriteHeader(http.StatusCreated) - w.Write([]byte("

getContests: Not implemented:...

")) + templates.ExecuteTemplate(w, "contest.html", &info) } func (api *ApiContext) getContestByID(w http.ResponseWriter, r *http.Request) { - id := r.PathValue("id") + idStr := r.PathValue("id") + id, _ := strconv.Atoi(idStr) + + type contestInfo struct { + store.Contest + problems []store.Problem + } + + c, err := api.Store.GetContestByID(r.Context(), id) + info := contestInfo{Contest: c} w.Header().Set("Content-Type", "text/html") - w.WriteHeader(http.StatusCreated) - w.Write([]byte(fmt.Sprintf("

Not implemented: %v...

", id))) + if err != nil { + w.WriteHeader(http.StatusNotFound) + templates.ExecuteTemplate(w, "404.html", &info) + return + } + + problems, err := api.Store.GetContestProblems(r.Context(), int(info.ContestID)) + info.problems = problems + + w.WriteHeader(http.StatusOK) + templates.ExecuteTemplate(w, "contest.html", &info) } diff --git a/api/user.go b/api/user.go index db72da2..65197e8 100644 --- a/api/user.go +++ b/api/user.go @@ -39,7 +39,7 @@ func (api *ApiContext) createUser(w http.ResponseWriter, r *http.Request) { log.Println("Error", err) return } - log.Println("SI SE PUDO REGISTRAR") + http.SetCookie(w, &http.Cookie{ Name: "session", Value: in.JWT, @@ -50,6 +50,7 @@ func (api *ApiContext) createUser(w http.ResponseWriter, r *http.Request) { Value: in.Username, Path: "/", }) + w.Header().Set("HX-Redirect", "/") w.WriteHeader(http.StatusOK) } diff --git a/security/jwt.go b/security/jwt.go index 27ebdbd..4cd9cdd 100644 --- a/security/jwt.go +++ b/security/jwt.go @@ -36,7 +36,7 @@ func GenerateJWT(username string) (string, error) { } // func ValidateJWT(tokenStr, username string) (bool, error) { -func ValidateJWT(tokenStr string) (bool, error) { +func ValidateJWT(tokenStr, username string) (bool, error) { verifier, err := jwt.NewVerifierHS(jwt.HS256, secretKey) if err != nil { return false, err @@ -59,9 +59,9 @@ func ValidateJWT(tokenStr string) (bool, error) { return false, err } - // if newClaims.IsSubject(username) { - // return true, nil - // } + if newClaims.IsSubject(username) { + return true, nil + } return false, nil } diff --git a/store/sqlite.go b/store/sqlite.go index 6b60373..a76710b 100644 --- a/store/sqlite.go +++ b/store/sqlite.go @@ -18,7 +18,7 @@ func NewSQLiteStore(db *sql.DB) Store { func (s *SQLiteStore) CreateUser(ctx context.Context, u *User) error { // Check for valid username - if err:=security.IsValidUsername(u.Username); err != nil { + if err := security.IsValidUsername(u.Username); err != nil { return err } @@ -30,7 +30,7 @@ func (s *SQLiteStore) CreateUser(ctx context.Context, u *User) error { } // Check for a secure password - if err:=security.IsValidPassword(u.Password); err != nil { + if err := security.IsValidPassword(u.Password); err != nil { return err } @@ -43,9 +43,8 @@ func (s *SQLiteStore) CreateUser(ctx context.Context, u *User) error { return err } - // Password hashing - passhash, err := security.HashPassword(u.Password, security.Argon2Params); + passhash, err := security.HashPassword(u.Password, security.Argon2Params) if err != nil { return err } @@ -69,7 +68,7 @@ func (s *SQLiteStore) CreateUser(ctx context.Context, u *User) error { return nil } -func (s *SQLiteStore) getUserByID(ctx context.Context, id int) (string, error) { +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 { @@ -79,6 +78,16 @@ func (s *SQLiteStore) getUserByID(ctx context.Context, id int) (string, error) { return username, nil } +func (s *SQLiteStore) GetUserID(ctx context.Context, username string) (int, error) { + id := 0 + err := s.DB.QueryRowContext(ctx, "SELECT id, FROM user WHERE username=?", username).Scan(&id) + if err != nil { + 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) @@ -88,7 +97,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 (?p)", 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 } @@ -98,6 +107,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.Query("SELECT c.contest_id, c.contest_name, c.start_date, c.end_date, u.username from contest JOIN user ON c.organizer_id = u.user_id") + + if err != nil { + 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, err + } + + 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.organzer_name FROM contest JOIN user 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 problem_id, contest_id, problem_name from problem WHERE 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 diff --git a/store/store.go b/store/store.go index cf358f6..b6f4e3a 100644 --- a/store/store.go +++ b/store/store.go @@ -19,10 +19,12 @@ type User struct { } type Contest struct { - ContestID int64 `json:"contest_id"` - ContestName string `json:"contest_name"` - StartDate string `json:"start_date"` - EndDate string `json:"end_date"` + 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 { @@ -30,7 +32,8 @@ type Problem struct { ContestID *int64 `json:"contest_id"` CreatorID int64 `json:"creator_id"` ProblemName string `json:"problem_name"` - Description string `json:"description"` + BaseScore string `json:"problem_name"` + Description []byte `json:"description"` } // Store defines persistence operations used by the HTTP layer. @@ -38,4 +41,10 @@ type Store interface { CreateUser(ctx context.Context, u *User) error CreateContest(ctx context.Context, c *Contest) error CreateProblem(ctx context.Context, p *Problem) 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) } 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..3cb29a7 --- /dev/null +++ b/templates/contest.html @@ -0,0 +1,16 @@ + + + + + + Contests + + + + + +

Contests

+ + + + 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/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: +
+ +
+ +
+ +
+
+
+ + + From 2d653e75bcfe8fe91b15d994b52dd8b25be5d5f0 Mon Sep 17 00:00:00 2001 From: OhhhMyGucci <94269402+OhhhMyGucci@users.noreply.github.com> Date: Thu, 27 Nov 2025 21:15:35 -0600 Subject: [PATCH 13/22] Login para usuarios Co-authored-by: OhhhMyGucci Co-authored-by: Francisco Galindo <69480179+Francisco-Galindo@users.noreply.github.com> --- api/login.go | 65 ++++++++++++++++++++++++++++++++++++++++++++++ security/ensure.go | 1 + store/sqlite.go | 27 +++++++++++++++++++ store/store.go | 1 + 4 files changed, 94 insertions(+) create mode 100644 api/login.go 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/security/ensure.go b/security/ensure.go index ee434f1..2f38adb 100644 --- a/security/ensure.go +++ b/security/ensure.go @@ -18,6 +18,7 @@ var ( 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 { diff --git a/store/sqlite.go b/store/sqlite.go index a76710b..92c2bc5 100644 --- a/store/sqlite.go +++ b/store/sqlite.go @@ -197,3 +197,30 @@ func (s *SQLiteStore) CreateProblem(ctx context.Context, p *Problem) error { } return nil } + +func (s *SQLiteStore) Login(ctx context.Context, u *User) error { + var username, password string + + err := s.DB.QueryRowContext(ctx, "SELECT username, password FROM user WHERE username = ? OR email = ?", u.Username, u.Email).Scan(&username, &password) + if err == sql.ErrNoRows { + return ErrNotFound + } + if err != nil { + return err + } + //Verificar password + hash := []byte(u.Password) + + val, err := security.VerifyHash(password,hash, security.Argon2Params) + if err != nil { + return err + } + if !val { + return security.ErrInvalidCredentials + } + + return nil + +} + + diff --git a/store/store.go b/store/store.go index b6f4e3a..39418ac 100644 --- a/store/store.go +++ b/store/store.go @@ -41,6 +41,7 @@ type Store interface { CreateUser(ctx context.Context, u *User) error CreateContest(ctx context.Context, c *Contest) error CreateProblem(ctx context.Context, p *Problem) 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) From 1786b034fe310247f45ec23ce2987fa58dc46cf1 Mon Sep 17 00:00:00 2001 From: Francisco-Galindo Date: Thu, 27 Nov 2025 21:17:44 -0600 Subject: [PATCH 14/22] =?UTF-8?q?Agregar=20paquete=20de=20autentificaci?= =?UTF-8?q?=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- auth/auth.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 auth/auth.go diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 0000000..7a8fdef --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,32 @@ +package auth + +import ( + "net/http" + + "lidsol.org/papeador/security" +) + +func IsAuthenticated(r *http.Request) bool { + cookieJWT, err := r.Cookie("jwt") + cookieUsername, err := r.Cookie("username") + + 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) { + http.Redirect(w, r, "/login", http.StatusFound) + return + } + next(w, r) + } +} From 72cd3083040b5cce7fbfc70bd0080cb2fd61004e Mon Sep 17 00:00:00 2001 From: Francisco-Galindo Date: Thu, 27 Nov 2025 21:18:49 -0600 Subject: [PATCH 15/22] Ignore files with trailing '~' --- .gitignore | 1 + 1 file changed, 1 insertion(+) 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 From 8a4efd48a0327afc1cde53dbfc2bb75331c6bec2 Mon Sep 17 00:00:00 2001 From: Francisco-Galindo Date: Thu, 27 Nov 2025 22:31:29 -0600 Subject: [PATCH 16/22] Fix query errors on contest creation --- api/api.go | 3 ++- api/contest.go | 37 +++++++++++++++++++++---------------- api/user.go | 1 + templates/contest.html | 8 ++++++-- templates/contests.html | 22 ++++++++++++++++++++++ 5 files changed, 52 insertions(+), 19 deletions(-) create mode 100644 templates/contests.html diff --git a/api/api.go b/api/api.go index 6486b48..e2e8c4a 100644 --- a/api/api.go +++ b/api/api.go @@ -36,7 +36,8 @@ func API(s store.Store, port int) { mux.HandleFunc("GET /contests", apiCtx.getContests) mux.HandleFunc("GET /contests/{id}", apiCtx.getContestByID) - mux.HandleFunc("POST /contests/{id}/problems", apiCtx.createProblem) + mux.HandleFunc("POST /contests/{id}/problems/new", apiCtx.createProblem) + mux.HandleFunc("GET /contests/{id}/problems/new", apiCtx.createProblemView) mux.HandleFunc("GET /contests/{id}/problems", apiCtx.getProblems) mux.HandleFunc("GET /contests/{contestID}/problems/{problemID}", apiCtx.getProblemByID) diff --git a/api/contest.go b/api/contest.go index 8cc4aa7..cd096b3 100644 --- a/api/contest.go +++ b/api/contest.go @@ -1,13 +1,11 @@ package api import ( - "encoding/json" "fmt" "log" "net/http" "strconv" - "lidsol.org/papeador/security" "lidsol.org/papeador/store" ) @@ -19,10 +17,6 @@ type ContestRequestContent struct { 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) - return - } in.ContestName = r.FormValue("contest-name") in.StartDate = r.FormValue("start-date") @@ -31,11 +25,19 @@ func (api *ApiContext) createContest(w http.ResponseWriter, r *http.Request) { 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 { - http.Error(w, "This user does not exist", http.StatusNotFound) + log.Println("Usuario no existe") + http.Error(w, "Este usuario no existe", http.StatusNotFound) return } @@ -52,25 +54,26 @@ func (api *ApiContext) createContest(w http.ResponseWriter, r *http.Request) { return } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(in) + 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, "createContext.html", nil) + templates.ExecuteTemplate(w, "createContest.html", nil) } func (api *ApiContext) getContests(w http.ResponseWriter, r *http.Request) { type contestsInfo struct { - contests []store.Contest + Contests []store.Contest } var info contestsInfo contests, err := api.Store.GetContests(r.Context()) - info.contests = contests + info.Contests = contests if err != nil { + log.Println("ERROR", err) w.WriteHeader(http.StatusNotFound) templates.ExecuteTemplate(w, "404.html", &info) return @@ -78,7 +81,7 @@ func (api *ApiContext) getContests(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") w.WriteHeader(http.StatusCreated) - templates.ExecuteTemplate(w, "contest.html", &info) + templates.ExecuteTemplate(w, "contests.html", &info) } func (api *ApiContext) getContestByID(w http.ResponseWriter, r *http.Request) { @@ -87,7 +90,7 @@ func (api *ApiContext) getContestByID(w http.ResponseWriter, r *http.Request) { type contestInfo struct { store.Contest - problems []store.Problem + Problems []store.Problem } c, err := api.Store.GetContestByID(r.Context(), id) @@ -95,13 +98,15 @@ func (api *ApiContext) getContestByID(w http.ResponseWriter, r *http.Request) { 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 + info.Problems = problems + log.Println("info", info) w.WriteHeader(http.StatusOK) templates.ExecuteTemplate(w, "contest.html", &info) diff --git a/api/user.go b/api/user.go index 65197e8..710888d 100644 --- a/api/user.go +++ b/api/user.go @@ -60,6 +60,7 @@ func (api *ApiContext) createUserView(w http.ResponseWriter, r *http.Request) { Title string } a := prueba{Title: "TITULO"} + log.Println("HOLA A TODOS") templates.ExecuteTemplate(w, "createUser.html", &a) } diff --git a/templates/contest.html b/templates/contest.html index 3cb29a7..078bf47 100644 --- a/templates/contest.html +++ b/templates/contest.html @@ -3,13 +3,17 @@ - Contests + {{ .ContestName }} -

Contests

+

{{ .ContestName }}

+

{{ .StartDate }}

+

{{ .EndDate }}

+

{{ .OrganizerName }}

+

{{ .ContestID }}

diff --git a/templates/contests.html b/templates/contests.html new file mode 100644 index 0000000..605cd85 --- /dev/null +++ b/templates/contests.html @@ -0,0 +1,22 @@ + + + + + + Concursos + + + + + + {{ range .Contests}} +

{{ .ContestName }}

+

{{ .StartDate }}

+

{{ .EndDate }}

+

{{ .OrganizerName }}

+

{{ .ContestID }}

+ {{ end }} + + + + From 9a5baef667a11f5fcd772d572de524eb9c87c5b9 Mon Sep 17 00:00:00 2001 From: Francisco-Galindo Date: Fri, 28 Nov 2025 02:15:11 -0600 Subject: [PATCH 17/22] Adding api for uploading and viewing problems --- api/api.go | 2 +- api/problem.go | 207 ++++++++++++++++++++++++++++++++--- api/user.go | 3 +- schema.sql | 1 + store/sqlite.go | 77 +++++++++++-- store/store.go | 25 ++++- templates/contest.html | 4 + templates/createProblem.html | 20 ++++ templates/problem.html | 25 +++++ 9 files changed, 328 insertions(+), 36 deletions(-) create mode 100644 templates/createProblem.html create mode 100644 templates/problem.html diff --git a/api/api.go b/api/api.go index e2e8c4a..9894cc5 100644 --- a/api/api.go +++ b/api/api.go @@ -38,8 +38,8 @@ func API(s store.Store, port int) { mux.HandleFunc("POST /contests/{id}/problems/new", apiCtx.createProblem) mux.HandleFunc("GET /contests/{id}/problems/new", apiCtx.createProblemView) - mux.HandleFunc("GET /contests/{id}/problems", apiCtx.getProblems) 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", apiCtx.submitProgram) mux.HandleFunc("GET /contests/{constestID}/problems/{problemID}/submit/{submitID}", apiCtx.getSubmissionByID) diff --git a/api/problem.go b/api/problem.go index 5a6e872..d680e3b 100644 --- a/api/problem.go +++ b/api/problem.go @@ -1,21 +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) @@ -27,24 +129,97 @@ 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) getProblems(w http.ResponseWriter, r *http.Request) { - id := r.PathValue("id") +func (api *ApiContext) createProblemView(w http.ResponseWriter, r *http.Request) { + idStr := r.PathValue("id") + type probleminfo struct { + ID string + } - w.Header().Set("Content-Type", "text/html") - w.WriteHeader(http.StatusCreated) - w.Write([]byte(fmt.Sprintf("

Not implemented: %v...

", id))) + info := probleminfo{ID: idStr} + templates.ExecuteTemplate(w, "createProblem.html", &info) } func (api *ApiContext) getProblemByID(w http.ResponseWriter, r *http.Request) { - contestId := r.PathValue("contestID") - problemId := r.PathValue("problemID") + 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 + } + + info := pageInfo{Problem: *problem, ContestID: contestID} + + 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", "text/html") - w.WriteHeader(http.StatusCreated) - w.Write([]byte(fmt.Sprintf("

Not implemented: %v, %v...

", contestId, problemId))) + w.Header().Set("Content-Type", "application/pdf") + w.WriteHeader(http.StatusOK) + w.Write(problem.Description) } diff --git a/api/user.go b/api/user.go index 710888d..e3612e7 100644 --- a/api/user.go +++ b/api/user.go @@ -59,8 +59,7 @@ func (api *ApiContext) createUserView(w http.ResponseWriter, r *http.Request) { type prueba struct { Title string } - a := prueba{Title: "TITULO"} - log.Println("HOLA A TODOS") + a := prueba{Title: ""} templates.ExecuteTemplate(w, "createUser.html", &a) } diff --git a/schema.sql b/schema.sql index 092a4ec..1e4b5a0 100644 --- a/schema.sql +++ b/schema.sql @@ -52,6 +52,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 diff --git a/store/sqlite.go b/store/sqlite.go index 92c2bc5..f42bddb 100644 --- a/store/sqlite.go +++ b/store/sqlite.go @@ -3,7 +3,7 @@ package store import ( "context" "database/sql" - // "log" + "log" "lidsol.org/papeador/security" ) @@ -61,6 +61,7 @@ func (s *SQLiteStore) CreateUser(ctx context.Context, u *User) error { return err } + if id, ierr := res.LastInsertId(); ierr == nil { u.UserID = id u.JWT = token @@ -80,8 +81,9 @@ func (s *SQLiteStore) GetUserByID(ctx context.Context, id int) (string, error) { func (s *SQLiteStore) GetUserID(ctx context.Context, username string) (int, error) { id := 0 - err := s.DB.QueryRowContext(ctx, "SELECT id, FROM user WHERE username=?", username).Scan(&id) + 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 } @@ -108,9 +110,9 @@ func (s *SQLiteStore) CreateContest(ctx context.Context, c *Contest) error { } func (s *SQLiteStore) GetContests(ctx context.Context) ([]Contest, error) { - rows, err := s.DB.Query("SELECT c.contest_id, c.contest_name, c.start_date, c.end_date, u.username from contest JOIN user ON c.organizer_id = u.user_id") + 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 { + if err != nil && err != sql.ErrNoRows { return nil, err } defer rows.Close() @@ -119,15 +121,15 @@ func (s *SQLiteStore) GetContests(ctx context.Context) ([]Contest, error) { for rows.Next() { var c Contest - if err := rows.Scan(c.ContestID, c.ContestName, c.StartDate, c.EndDate, c.OrganizerName); err != nil { + 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, err - } + // if err := rows.Err(); err != nil { + // return contests, nil + // } return contests, nil } @@ -144,7 +146,7 @@ func (s *SQLiteStore) GetContestByName(ctx context.Context, name string) (Contes 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.organzer_name FROM contest JOIN user 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) + 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 } @@ -153,7 +155,7 @@ func (s *SQLiteStore) GetContestByID(ctx context.Context, id int) (Contest, erro } func (s *SQLiteStore) GetContestProblems(ctx context.Context, id int) ([]Problem, error) { - rows, err := s.DB.Query("SELECT problem_id, contest_id, problem_name from problem WHERE contest_id = ?", id) + 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 @@ -164,7 +166,7 @@ func (s *SQLiteStore) GetContestProblems(ctx context.Context, id int) ([]Problem for rows.Next() { var p Problem - if err := rows.Scan(p.ProblemID, p.ContestID, p.ProblemName); err != nil { + if err := rows.Scan(&p.ProblemID, &p.ContestID, &p.ProblemName); err != nil { return problems, err } problems = append(problems, p) @@ -188,12 +190,63 @@ 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 } diff --git a/store/store.go b/store/store.go index 39418ac..9a88c05 100644 --- a/store/store.go +++ b/store/store.go @@ -28,12 +28,25 @@ type Contest struct { } 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"` - BaseScore string `json:"problem_name"` - Description []byte `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. @@ -41,6 +54,7 @@ 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) @@ -48,4 +62,5 @@ type Store interface { 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/contest.html b/templates/contest.html index 078bf47..1240c50 100644 --- a/templates/contest.html +++ b/templates/contest.html @@ -14,6 +14,10 @@

{{ .StartDate }}

{{ .EndDate }}

{{ .OrganizerName }}

{{ .ContestID }}

+ + {{ range .Problems}} +

{{ .ProblemName }}

+ {{ end }} 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/problem.html b/templates/problem.html new file mode 100644 index 0000000..d7e3c8c --- /dev/null +++ b/templates/problem.html @@ -0,0 +1,25 @@ + + + + + + {{ .ProblemName }} + + + + + +

{{ .ProblemName }}

+ +

{{ .SampleInputStr }}

+

{{ .SampleOutStr }}

+ + + + + + From 1331e76cedba545dda954a6051aeee63904b2068 Mon Sep 17 00:00:00 2001 From: OhhhMyGucci <94269402+OhhhMyGucci@users.noreply.github.com> Date: Fri, 28 Nov 2025 02:20:00 -0600 Subject: [PATCH 18/22] Use templates for some of the views Co-authored-by: OhhhMyGucci Co-authored-by: Francisco Galindo <69480179+Francisco-Galindo@users.noreply.github.com> --- templates/contest.html | 196 ++++++++++++++++++++++++++++++++++++---- templates/contests.html | 136 ++++++++++++++++++++++++---- 2 files changed, 293 insertions(+), 39 deletions(-) diff --git a/templates/contest.html b/templates/contest.html index 1240c50..429e0d4 100644 --- a/templates/contest.html +++ b/templates/contest.html @@ -1,24 +1,180 @@ + + + + + Papeador + + + + + + - - - - {{ .ContestName }} - - - - - -

{{ .ContestName }}

-

{{ .StartDate }}

-

{{ .EndDate }}

-

{{ .OrganizerName }}

-

{{ .ContestID }}

+
+
+
+
+
+

{{.ContestName}}

+

+ Organizado por: {{ .OrganizerName }} +

+
+ En progreso +
- {{ range .Problems}} -

{{ .ProblemName }}

- {{ end }} - +
+
+

+ + 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
+
+
+
+
+
+
+ + - - + + + \ No newline at end of file diff --git a/templates/contests.html b/templates/contests.html index 605cd85..25cf8ca 100644 --- a/templates/contests.html +++ b/templates/contests.html @@ -1,22 +1,120 @@ + + + +Listado de Concursos + + + + + + + + +
+
+ +

Concursos Disponibles

+ + + + + + + + + + + {{ range .Contests }} + + + + + + + + + {{ end }} +
Nombre del ConcursoOrganizadorInicioFinAcción
{{ .ContestName }}{{ .OrganizerName }}{{ .StartDate }}{{ .EndDate }}
+ + + + From 9d7c727e4ea19921e3d7872540bbbf12ee2ef251 Mon Sep 17 00:00:00 2001 From: Francisco-Galindo Date: Fri, 28 Nov 2025 02:49:58 -0600 Subject: [PATCH 19/22] Fix up links in some views --- templates/contest.html | 8 ++++---- templates/contests.html | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/templates/contest.html b/templates/contest.html index 429e0d4..ba2cd96 100644 --- a/templates/contest.html +++ b/templates/contest.html @@ -3,7 +3,7 @@ - Papeador + {{.ContestName}} @@ -22,7 +22,7 @@ diff --git a/templates/contests.html b/templates/contests.html index 8d80ae2..820cf26 100644 --- a/templates/contests.html +++ b/templates/contests.html @@ -64,14 +64,21 @@ diff --git a/templates/problem.html b/templates/problem.html index 8244d24..9602629 100644 --- a/templates/problem.html +++ b/templates/problem.html @@ -27,7 +27,11 @@ Perfil @@ -68,6 +72,7 @@
Output
+ From b78ece5ee949d4196cbf7da13c126ec164e2c50c Mon Sep 17 00:00:00 2001 From: Francisco-Galindo Date: Fri, 28 Nov 2025 12:27:29 -0600 Subject: [PATCH 22/22] A --- api/api.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/api.go b/api/api.go index 37b3833..0f52c1b 100644 --- a/api/api.go +++ b/api/api.go @@ -44,9 +44,9 @@ func API(s store.Store, port int) { 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/{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)) + // mux.HandleFunc("GET /contests/{constestID}/problems/{problemID}/last-submit", auth.RequireAuth(apiCtx.getLastSubmission)) log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), mux)) }