This commit is contained in:
2026-09-15 09:15:53 -05:00
parent 97c930196a
commit ab9573b875
21 changed files with 1664 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
package handlers
import (
"github.com/gin-gonic/gin"
"jp.wtf/go-fishing/internal/db"
v1 "jp.wtf/go-fishing/internal/handlers/v1"
)
type Env struct {
DB *db.Database
}
func (env *Env) RegisterRoutes(rg *gin.RouterGroup) {
v1Router := v1.V1Router{DB: env.DB}
v1Router.RegisterRoutes(rg)
}
+122
View File
@@ -0,0 +1,122 @@
package v1
import (
"database/sql"
"errors"
"net/http"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"jp.wtf/go-fishing/internal/api"
"jp.wtf/go-fishing/internal/models"
)
func (h *V1Router) Login(c *gin.Context) {
if api.CheckIfLoggedIn(c) {
api.Fail(c, http.StatusBadRequest, api.APIErrorCodeInvalidRequest, "You are already logged in!")
return
}
var json struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := c.ShouldBind(&json); err != nil {
api.Fail(c, http.StatusBadRequest, api.APIErrorCodeInvalidBody, err.Error())
return
}
angler, err := h.DB.GetRawAnglerByUsername(json.Username)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
api.Fail(c, http.StatusUnauthorized, api.APIErrorCodeUnauthorized, "Invalid username or password")
return
}
api.Fail(c, http.StatusInternalServerError, api.APIErrorCodeUnknown, err.Error())
return
}
if valid := api.CheckPassword(json.Password, angler.PasswordHash); valid {
s := sessions.Default(c)
s.Set("angler_id", angler.ID)
err := s.Save()
if err != nil {
api.Fail(c, http.StatusInternalServerError, api.APIErrorCodeUnknown, "Failed to create new session")
return
}
api.OK(c, models.Angler{
ID: angler.ID,
Username: angler.Username,
})
return
}
api.Fail(c, http.StatusUnauthorized, api.APIErrorCodeUnauthorized, "Invalid username or password")
}
func (h *V1Router) Signup(c *gin.Context) {
if api.CheckIfLoggedIn(c) {
api.Fail(c, http.StatusBadRequest, api.APIErrorCodeInvalidRequest, "You are already logged in!")
return
}
var json api.CreateAnglerRequest
if err := c.ShouldBind(&json); err != nil {
api.Fail(c, http.StatusBadRequest, api.APIErrorCodeInvalidBody, err.Error())
return
}
hashed_password, hash_err := api.HashPassword(json.Password)
if hash_err != nil {
api.Fail(c, http.StatusInternalServerError, api.APIErrorCodeUnknown, hash_err.Error())
return
}
new_angler, create_err := h.DB.CreateNewAngler(json.Username, hashed_password)
if create_err != nil {
api.Fail(c, http.StatusInternalServerError, api.APIErrorCodeUnknown, create_err.Error())
return
}
s := sessions.Default(c)
s.Set("angler_id", new_angler.ID)
if err := s.Save(); err != nil {
api.Fail(c, http.StatusInternalServerError, api.APIErrorCodeUnknown, "Failed to create new session")
return
}
api.OK(c, models.Angler{
ID: new_angler.ID,
Username: new_angler.Username,
})
}
func (h *V1Router) Logout(c *gin.Context) {
if !api.CheckIfLoggedIn(c) {
api.Fail(c, http.StatusBadRequest, api.APIErrorCodeInvalidRequest, "You are not logged in...thus you cannot logout!")
return
}
s := sessions.Default(c)
s.Clear()
s.Save()
api.OK(c, nil)
}
func (h *V1Router) GetMe(c *gin.Context) {
angler, err := api.GetAnglerFromSession(c)
if err != nil {
api.Fail(c, http.StatusInternalServerError, api.APIErrorCodeUnknown, err.Error())
return
}
api.OK(c, angler)
}
+108
View File
@@ -0,0 +1,108 @@
package v1
import (
"math/rand/v2"
"net/http"
"time"
"github.com/gin-gonic/gin"
"jp.wtf/go-fishing/internal/api"
)
var sayings = []string{
// Near Misses
"Close but no cigar",
"Almost only counts in horseshoes and hand grenades",
"A day late and a dollar short",
"A miss is as good as a mile",
"Always a bridesmaid, never a bride",
"Snatched defeat from the jaws of victory",
"Fell at the last hurdle",
"Missed the boat",
"Missed it by a whisker",
"So near, and yet so far",
"A swing and a miss",
// Futile Efforts
"Beating a dead horse",
"Tilting at windmills",
"Squeezing blood from a stone",
"Nailing jelly to a wall",
"Herding cats",
"Rearranging deck chairs on the Titanic",
"Pushing a rope",
"Spitting in the wind",
"Putting toothpaste back in the tube",
"Shoveling snow in a blizzard",
"Spinning your wheels",
"Emptying the ocean with a teaspoon",
"Barking up the wrong tree",
"A wild goose chase",
"Catching the wind in a net",
}
func (h *V1Router) Cast(c *gin.Context) {
angler, err := api.GetAnglerFromSession(c)
if err != nil {
api.Fail(c, http.StatusInternalServerError, api.APIErrorCodeUnknown, err.Error())
return
}
is_casting, check_err := h.DB.IsCasting(angler.ID)
if check_err != nil {
api.Fail(c, http.StatusInternalServerError, api.APIErrorCodeUnknown, "Unable to check for existing cast")
return
}
if is_casting {
api.Fail(c, http.StatusBadRequest, api.APIErrorCodeInvalidRequest, "You are already casting! >:|")
return
}
cast, cast_create_err := h.DB.CreateNewCast(angler.ID)
if cast_create_err != nil {
api.Fail(c, http.StatusInternalServerError, api.APIErrorCodeUnknown, "Unable to start your cast")
return
}
// Fishing can take anywhere from 2-5 seconds
minTime, maxTime := 2, 5
timeToWait := rand.IntN(maxTime-minTime+1) + minTime
select {
case <-time.After(time.Duration(timeToWait) * time.Second):
// One we complete fishing we must decide if we caught anything
chance := 75
gen := rand.IntN(100)
caught := gen < chance
_, end_cast_err := h.DB.EndCastByID(cast.ID)
if end_cast_err != nil {
api.Fail(c, http.StatusInternalServerError, api.APIErrorCodeUnknown, "Unable to update your cast — may have left you in a bad state :(")
return
}
if caught {
// If so then we must select a fish.
// Then we must determine a size between the min and max of said fish
// Finally record the catch and return it
api.OK(c, "Got one! Nice!")
} else {
// Otherwise return. You are left empty handed
api.Fail(c, http.StatusNotFound, api.APIErrorCodeNotFound, sayings[rand.IntN(len(sayings))])
}
case <-c.Done():
_, end_cast_err := h.DB.EndCastByID(cast.ID)
if end_cast_err != nil {
api.Fail(c, http.StatusInternalServerError, api.APIErrorCodeUnknown, "Unable to update your cast — may have left you in a bad state :(")
return
}
api.Fail(c, http.StatusRequestTimeout, api.APIErrorCodeUnknown, "Cast terminiated for unknown reason")
}
}
+59
View File
@@ -0,0 +1,59 @@
package v1
import (
"log"
"net/http"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"jp.wtf/go-fishing/internal/api"
"jp.wtf/go-fishing/internal/db"
)
type V1Router struct {
DB *db.Database
}
func (h *V1Router) AnglerAuthMiddleware(c *gin.Context) {
s := sessions.Default(c)
val := s.Get("angler_id")
if val == nil || val == "" {
api.FailAndAbort(c, http.StatusUnauthorized, api.APIErrorCodeUnauthorized, "You are not logged in!")
return
}
valStr, ok := val.(string)
angler_id, uuid_err := uuid.Parse(valStr)
if uuid_err != nil || !ok {
s.Clear()
s.Save()
api.FailAndAbort(c, http.StatusInternalServerError, api.APIErrorCodeUnknown, "Session data malformed")
return
}
angler, err := h.DB.GetAnglerByID(angler_id)
if err != nil {
s.Clear()
s.Save()
log.Print("Failed to find angler: ", err.Error())
api.FailAndAbort(c, http.StatusUnauthorized, api.APIErrorCodeUnauthorized, "Bad or expired session")
return
}
c.Set("angler", angler)
s.Save()
}
func (h *V1Router) RegisterRoutes(rg *gin.RouterGroup) {
auth := rg.Group("/auth")
auth.POST("/login", h.Login)
auth.POST("/signup", h.Signup)
auth.POST("/logout", h.Logout)
auth.GET("/me", h.GetMe)
rg.GET("/cast", h.AnglerAuthMiddleware, h.Cast)
}