init
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"jp.wtf/go-fishing/internal/models"
|
||||
)
|
||||
|
||||
type CreateAnglerRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Data any `json:"$data,omitempty"`
|
||||
Meta *APIMetadata `json:"$meta,omitempty"`
|
||||
Error *APIError `json:"$error,omitempty"`
|
||||
}
|
||||
|
||||
type APIMetadata struct {
|
||||
Version string `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
type APIErrorCode string
|
||||
|
||||
const (
|
||||
APIErrorCodeUnknown APIErrorCode = "API_ERR_UNKNOWN"
|
||||
APIErrorCodeUnauthorized APIErrorCode = "API_ERR_UNAUTHORIZED"
|
||||
APIErrorCodeNotFound APIErrorCode = "API_ERR_NOT_FOUND"
|
||||
APIErrorCodeInvalidBody APIErrorCode = "API_ERR_INVALID_BODY"
|
||||
APIErrorCodeInvalidRequest APIErrorCode = "API_ERR_INVALID_REQUEST"
|
||||
)
|
||||
|
||||
type APIError struct {
|
||||
Code APIErrorCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func SendJSONResponse(c *gin.Context, status int, response Response) {
|
||||
c.JSON(status, response)
|
||||
}
|
||||
|
||||
func OK(c *gin.Context, data any) {
|
||||
SendJSONResponse(c, http.StatusOK, Response{
|
||||
Data: data,
|
||||
Meta: &APIMetadata{
|
||||
Version: c.GetString("APIVersion"),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func Fail(c *gin.Context, status int, code APIErrorCode, message string) {
|
||||
SendJSONResponse(c, status, Response{
|
||||
Error: &APIError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func FailAndAbort(c *gin.Context, status int, code APIErrorCode, message string) {
|
||||
Fail(c, status, code, message)
|
||||
c.Abort()
|
||||
}
|
||||
|
||||
func CheckIfLoggedIn(c *gin.Context) bool {
|
||||
s := sessions.Default(c)
|
||||
val := s.Get("angler_id")
|
||||
return val != nil && val != ""
|
||||
}
|
||||
|
||||
func GetAnglerFromSession(c *gin.Context) (models.Angler, error) {
|
||||
val, exists := c.Get("angler")
|
||||
|
||||
if !exists {
|
||||
return models.Angler{}, fmt.Errorf("No angler")
|
||||
}
|
||||
|
||||
angler, ok := val.(models.Angler)
|
||||
|
||||
if !ok {
|
||||
return models.Angler{}, fmt.Errorf("Malformed or invalid angler")
|
||||
}
|
||||
|
||||
return angler, nil
|
||||
}
|
||||
|
||||
func HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
func CheckPassword(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"charm.land/ssh"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"jp.wtf/go-fishing/internal/models"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
Conn *sqlx.DB
|
||||
}
|
||||
|
||||
func (db *Database) GetAnglerByID(angler_id uuid.UUID) (models.Angler, error) {
|
||||
var angler models.Angler
|
||||
err := db.Conn.Get(&angler, "SELECT angler_id, angler_username, angler_email FROM anglers WHERE angler_id = $1", angler_id)
|
||||
|
||||
return angler, err
|
||||
}
|
||||
|
||||
func (db *Database) GetAnglerByKey(ssh_key ssh.PublicKey) (models.Angler, error) {
|
||||
var angler models.Angler
|
||||
err := db.Conn.Get(&angler, "SELECT angler_id, angler_username, angler_email FROM ssh_keys INNER JOIN anglers USING (angler_id) WHERE key_data = $1", ssh_key.Marshal())
|
||||
|
||||
return angler, err
|
||||
}
|
||||
|
||||
func (db *Database) GetRawAnglerByUsername(username string) (models.AnglerDB, error) {
|
||||
var angler models.AnglerDB
|
||||
err := db.Conn.Get(&angler, "SELECT angler_id, angler_username, angler_email, angler_password FROM anglers WHERE angler_username = $1", username)
|
||||
|
||||
return angler, err
|
||||
}
|
||||
|
||||
func (db *Database) CreateNewAngler(username string, hashed_password string) (models.Angler, error) {
|
||||
var new_angler models.Angler
|
||||
err := db.Conn.Get(&new_angler, `
|
||||
INSERT INTO public.anglers (angler_username, angler_password)
|
||||
VALUES ($1, $2)
|
||||
RETURNING angler_id, angler_username, angler_email
|
||||
`, username, hashed_password)
|
||||
|
||||
return new_angler, err
|
||||
}
|
||||
|
||||
func (db *Database) CreateNewAnglerFromKey(ssh_key ssh.PublicKey) (models.Angler, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(uuid.New().String()), bcrypt.DefaultCost)
|
||||
|
||||
if err != nil {
|
||||
return models.Angler{}, err
|
||||
}
|
||||
|
||||
fingerprint := gossh.FingerprintSHA256(ssh_key)
|
||||
|
||||
angler, err := db.CreateNewAngler(fingerprint, string(bytes))
|
||||
if err != nil {
|
||||
return models.Angler{}, err
|
||||
}
|
||||
|
||||
if _, err := db.AttachKeyToAngler(angler.ID, ssh_key); err != nil {
|
||||
return models.Angler{}, err
|
||||
}
|
||||
|
||||
return angler, err
|
||||
}
|
||||
|
||||
func (db *Database) AttachKeyToAngler(angler_id uuid.UUID, ssh_key ssh.PublicKey) (models.SSHKey, error) {
|
||||
var new_key models.SSHKey
|
||||
err := db.Conn.Get(&new_key, `
|
||||
INSERT INTO public.ssh_keys (key_type, key_data, key_fingerprint, angler_id)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING key_id, key_type, key_data, key_fingerprint
|
||||
`, ssh_key.Type(), ssh_key.Marshal(), gossh.FingerprintSHA256(ssh_key), angler_id)
|
||||
|
||||
return new_key, err
|
||||
}
|
||||
|
||||
func (db *Database) IsCasting(angler_id uuid.UUID) (bool, error) {
|
||||
var exists bool
|
||||
err := db.Conn.Get(&exists, "SELECT EXISTS( SELECT 1 FROM public.casts WHERE angler_id = $1 AND cast_ended_at IS NULL )", angler_id)
|
||||
|
||||
if err != nil {
|
||||
fmt.Print(err.Error())
|
||||
return false, err
|
||||
}
|
||||
|
||||
return exists, err
|
||||
}
|
||||
|
||||
func (db *Database) GetMostRecentCast(angler_id uuid.UUID) (models.Cast, error) {
|
||||
var cast models.Cast
|
||||
err := db.Conn.Get(&cast, "SELECT cast_id, cast_started_at, cast_ended_at, catch_id FROM public.casts WHERE angler_id =! ORDER cast_started_at DESC LIMIT 1", angler_id)
|
||||
|
||||
return cast, err
|
||||
}
|
||||
|
||||
func (db *Database) CreateNewCast(angler_id uuid.UUID) (models.Cast, error) {
|
||||
var cast models.Cast
|
||||
err := db.Conn.Get(&cast, "INSERT INTO public.casts (angler_id) VALUES ($1) RETURNING *", angler_id)
|
||||
|
||||
return cast, err
|
||||
}
|
||||
|
||||
func (db *Database) EndCastByID(cast_id uuid.UUID) (models.Cast, error) {
|
||||
var cast models.Cast
|
||||
err := db.Conn.Get(&cast, "UPDATE public.casts SET cast_ended_at = $1 WHERE cast_id = $2 RETURNING *", time.Now(), cast_id)
|
||||
|
||||
return cast, err
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE public.anglers (
|
||||
angler_id UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
angler_username TEXT NOT NULL UNIQUE,
|
||||
angler_password TEXT NOT NULL,
|
||||
angler_email TEXT
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE public.anglers;
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,37 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE public.anglers ADD PRIMARY KEY (angler_id);
|
||||
|
||||
CREATE TABLE public.fish (
|
||||
fish_id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
fish_name TEXT NOT NULL,
|
||||
fish_desc TEXT NOT NULL,
|
||||
fish_size_oz_max NUMERIC NOT NULL,
|
||||
fish_size_oz_min NUMERIC NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE public.catches (
|
||||
catch_id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
catch_caught_at TIMESTAMPTZ NOT NULL,
|
||||
catch_size_oz NUMERIC NOT NULL,
|
||||
angler_id UUID NOT NULL REFERENCES public.anglers(angler_id),
|
||||
fish_id UUID NOT NULL REFERENCES public.fish(fish_id)
|
||||
);
|
||||
|
||||
CREATE TABLE public.casts (
|
||||
cast_id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
cast_started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
cast_ended_at TIMESTAMPTZ,
|
||||
angler_id UUID NOT NULL REFERENCES public.anglers(angler_id),
|
||||
catch_id UUID REFERENCES public.catches(catch_id)
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE public.anglers DROP CONSTRAINT anglers_pkey
|
||||
|
||||
DROP TABLE public.fish;
|
||||
DROP TABLE public.catches;
|
||||
DROP TABLE public.casts;
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,15 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE TABLE public.ssh_keys (
|
||||
key_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
angler_id UUID NOT NULL REFERENCES public.anglers(angler_id),
|
||||
key_type TEXT NOT NULL,
|
||||
key_data BYTEA NOT NULL UNIQUE,
|
||||
key_fingerprint TEXT
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE public.ssh_keys;
|
||||
-- +goose StatementEnd
|
||||
@@ -0,0 +1,6 @@
|
||||
package migrations
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed *.sql
|
||||
var Files embed.FS
|
||||
@@ -0,0 +1,52 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
)
|
||||
|
||||
type Angler struct {
|
||||
ID uuid.UUID `db:"angler_id" json:"id"`
|
||||
Username string `db:"angler_username" json:"name"`
|
||||
Email *string `db:"angler_email" json:"email"`
|
||||
}
|
||||
|
||||
type SSHKey struct {
|
||||
ID uuid.UUID `db:"key_id" json:"id"`
|
||||
SSH_Type string `db:"key_type" json:"ssh_type"`
|
||||
Date []byte `db:"key_data" json:"data"`
|
||||
Fingerprint string `db:"key_fingerprint" json:"fingerprint"`
|
||||
}
|
||||
|
||||
type AnglerDB struct {
|
||||
Angler
|
||||
PasswordHash string `db:"angler_password"`
|
||||
}
|
||||
|
||||
type Fish struct {
|
||||
ID uuid.UUID `db:"fish_id" json:"id"`
|
||||
Name string `db:"fish_name" json:"name"`
|
||||
Description string `db:"fish_desc" json:"desc"`
|
||||
SizeOZMax uint16 `db:"fish_size_oz_max" json:"size_oz_max"`
|
||||
SizeOZMin uint16 `db:"fish_size_oz_maxin" json:"size_oz_min"`
|
||||
}
|
||||
|
||||
type Catch struct {
|
||||
ID uuid.UUID `db:"catch_id" json:"id"`
|
||||
At time.Time `db:"catch_caught_at" json:"caught_at"`
|
||||
AnglerID uuid.UUID `db:"angler_id" json:"angler_id"`
|
||||
FishID uuid.UUID `db:"fish_id" json:"fish_id"`
|
||||
SizeOZ uint16 `db:"catch_size_oz" json:"size_oz"`
|
||||
}
|
||||
|
||||
type Cast struct {
|
||||
ID uuid.UUID `db:"cast_id" json:"id"`
|
||||
Start time.Time `db:"cast_started_at" json:"start"`
|
||||
End sql.NullTime `db:"cast_ended_at" json:"end"`
|
||||
AnglerID uuid.UUID `db:"angler_id" json:"angler_id"`
|
||||
CatchID uuid.NullUUID `db:"catch_id" json:"catch_id"`
|
||||
}
|
||||
Reference in New Issue
Block a user