Initial commit
This commit is contained in:
52
.air.toml
Normal file
52
.air.toml
Normal file
@@ -0,0 +1,52 @@
|
||||
root = "."
|
||||
testdata_dir = "testdata"
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
args_bin = []
|
||||
bin = "./bin/api"
|
||||
cmd = "go build -v -o ./bin/api ./cmd/api"
|
||||
delay = 1000
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go"]
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
full_bin = ""
|
||||
include_dir = ["cmd", "internal"]
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
include_file = []
|
||||
kill_delay = "0s"
|
||||
log = "build-errors.log"
|
||||
poll = false
|
||||
poll_interval = 0
|
||||
post_cmd = []
|
||||
pre_cmd = []
|
||||
rerun = false
|
||||
rerun_delay = 500
|
||||
send_interrupt = false
|
||||
stop_on_error = false
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
||||
watcher = "cyan"
|
||||
|
||||
[log]
|
||||
main_only = false
|
||||
silent = false
|
||||
time = false
|
||||
|
||||
[misc]
|
||||
clean_on_exit = false
|
||||
|
||||
[proxy]
|
||||
app_port = 0
|
||||
enabled = false
|
||||
proxy_port = 0
|
||||
|
||||
[screen]
|
||||
clear_on_rebuild = false
|
||||
keep_scroll = true
|
||||
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# Environment variables
|
||||
.env
|
||||
.envrc
|
||||
|
||||
# Temporary files
|
||||
/tmp
|
||||
|
||||
# Binaries
|
||||
/bin
|
||||
/api
|
||||
|
||||
# Nix
|
||||
result*
|
||||
.direnv
|
||||
flake-profile*
|
||||
80
Justfile
Normal file
80
Justfile
Normal file
@@ -0,0 +1,80 @@
|
||||
# Load environment variables from .env
|
||||
set dotenv-load
|
||||
|
||||
# Default recipe to display help
|
||||
default:
|
||||
@just --list
|
||||
|
||||
# Run the application
|
||||
run:
|
||||
go run cmd/api/main.go
|
||||
|
||||
# Build the application
|
||||
build:
|
||||
go build -o bin/api cmd/api/main.go
|
||||
|
||||
# Start the project with file watchers
|
||||
dev: db-up migrate-up
|
||||
air
|
||||
|
||||
# Run tests
|
||||
test:
|
||||
go test -v ./...
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
rm -rf bin/
|
||||
|
||||
# Start the database
|
||||
db-up:
|
||||
docker-compose up -d
|
||||
@echo "Waiting for database to be ready..."
|
||||
@sleep 2
|
||||
|
||||
# Stop the database
|
||||
db-down:
|
||||
docker-compose down
|
||||
|
||||
# Reset the database (WARNING: deletes all data)
|
||||
db-reset:
|
||||
docker-compose down -v
|
||||
docker-compose up -d
|
||||
@echo "Waiting for database to be ready..."
|
||||
@sleep 2
|
||||
just migrate-up
|
||||
|
||||
# Run all database migrations
|
||||
migrate-up: db-up
|
||||
go run -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest -path migrations -database "${DATABASE_URL}" up
|
||||
|
||||
# Rollback last migration
|
||||
migrate-down:
|
||||
go run -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest -path migrations -database "${DATABASE_URL}" down 1
|
||||
|
||||
# Create a new migration
|
||||
migrate-create NAME:
|
||||
go run -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest create -ext sql -dir migrations -seq {{NAME}}
|
||||
|
||||
# Connect to the database with pgcli
|
||||
db-connect:
|
||||
pgcli "${DATABASE_URL}"
|
||||
|
||||
# Run seed data
|
||||
db-seed:
|
||||
@echo "Seeding database..."
|
||||
psql "${DATABASE_URL}" -f seeds/exercise_categories.sql
|
||||
psql "${DATABASE_URL}" -f seeds/exercises.sql
|
||||
psql "${DATABASE_URL}" -f seeds/users.sql
|
||||
psql "${DATABASE_URL}" -f seeds/workouts.sql
|
||||
@echo "Seed data loaded!"
|
||||
|
||||
# Reset database and run seeds
|
||||
db-fresh: db-reset db-seed
|
||||
|
||||
# View database logs
|
||||
db-logs:
|
||||
docker-compose logs -f postgres
|
||||
|
||||
# Check database status
|
||||
db-status:
|
||||
docker-compose ps
|
||||
83
cmd/api/main.go
Normal file
83
cmd/api/main.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.kling.dev/jared/WorkoutTrackerAPI/internal/database"
|
||||
"git.kling.dev/jared/WorkoutTrackerAPI/internal/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
dbHost := os.Getenv("DB_HOST")
|
||||
dbPort := os.Getenv("DB_PORT")
|
||||
dbUser := os.Getenv("DB_USER")
|
||||
dbPassword := os.Getenv("DB_PASSWORD")
|
||||
dbName := os.Getenv("DB_NAME")
|
||||
webPort := os.Getenv("PORT")
|
||||
|
||||
if dbHost == "" || dbPort == "" || dbUser == "" || dbPassword == "" || dbName == "" {
|
||||
log.Fatal("Not all database params are set")
|
||||
}
|
||||
dbPortParsed, err := strconv.ParseInt(dbPort, 10, 64)
|
||||
if err != nil {
|
||||
log.Fatal("Unable to parse db port to an int")
|
||||
}
|
||||
|
||||
if webPort == "" {
|
||||
webPort = "8080"
|
||||
}
|
||||
webPortParsed, err := strconv.ParseInt(webPort, 10, 64)
|
||||
if err != nil {
|
||||
log.Fatal("Unable to parse web port to an int")
|
||||
}
|
||||
|
||||
dbConn, err := database.NewConnection(database.Config{
|
||||
Host: dbHost,
|
||||
Port: int(dbPortParsed),
|
||||
User: dbUser,
|
||||
Password: dbPassword,
|
||||
DBName: dbName,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("Failed to connect to database: ", err)
|
||||
}
|
||||
|
||||
defer dbConn.Close()
|
||||
|
||||
jwtSecret := os.Getenv("JWT_SECRET")
|
||||
|
||||
if jwtSecret == "" {
|
||||
log.Fatal("Unable to determine the JWT_SECRET")
|
||||
}
|
||||
|
||||
s := server.NewServer(dbConn, jwtSecret)
|
||||
|
||||
go func() {
|
||||
fmt.Printf("Server listening on port %d\n", webPortParsed)
|
||||
webAddr := fmt.Sprintf(":%d", webPortParsed)
|
||||
s.Start(webAddr)
|
||||
}()
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
log.Println("Shutting down server...")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := s.Shutdown(ctx); err != nil {
|
||||
log.Fatal("Server forced to shutdown:", err)
|
||||
}
|
||||
|
||||
log.Println("Server exited")
|
||||
}
|
||||
23
docker-compose.yml
Normal file
23
docker-compose.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: workout_tracker_db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: workout_user
|
||||
POSTGRES_PASSWORD: workout_pass
|
||||
POSTGRES_DB: workout_tracker
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U workout_user -d workout_tracker"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1759381078,
|
||||
"narHash": "sha256-gTrEEp5gEspIcCOx9PD8kMaF1iEmfBcTbO0Jag2QhQs=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "7df7ff7d8e00218376575f0acdcc5d66741351ee",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
63
flake.nix
Normal file
63
flake.nix
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
description = "Go development environment";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
go
|
||||
gotools
|
||||
gopls
|
||||
go-outline
|
||||
gocode-gomod
|
||||
gopkgs
|
||||
godef
|
||||
golint
|
||||
air
|
||||
|
||||
postgresql
|
||||
pgcli
|
||||
|
||||
migrate
|
||||
|
||||
docker
|
||||
docker-compose
|
||||
|
||||
jq
|
||||
curl
|
||||
just
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "🚀 Go Development Environment"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Go version: $(go version | cut -d' ' -f3)"
|
||||
echo "PostgreSQL: $(postgres --version | cut -d' ' -f3)"
|
||||
echo ""
|
||||
echo "Quick commands:"
|
||||
echo " docker-compose up -d # Start Postgres"
|
||||
echo " docker-compose down # Stop Postgres"
|
||||
echo " just migrate-up # Run migrations"
|
||||
echo " just run # Run the API"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
19
go.mod
Normal file
19
go.mod
Normal file
@@ -0,0 +1,19 @@
|
||||
module git.kling.dev/jared/WorkoutTrackerAPI
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||
github.com/golang-migrate/migrate/v4 v4.19.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.6 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
golang.org/x/crypto v0.42.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
)
|
||||
34
go.sum
Normal file
34
go.sum
Normal file
@@ -0,0 +1,34 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE=
|
||||
github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
|
||||
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
101
internal/auth/manager.go
Normal file
101
internal/auth/manager.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidToken = errors.New("invalid token")
|
||||
ErrExpiredToken = errors.New("token has expired")
|
||||
ErrInvalidSignature = errors.New("invalid token signature")
|
||||
ErrMissingClaims = errors.New("missing required claims")
|
||||
)
|
||||
|
||||
// Claims represents the JWT claims structure
|
||||
type Claims struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
type JWTManager struct {
|
||||
secretKey []byte
|
||||
tokenDuration time.Duration
|
||||
}
|
||||
|
||||
func NewJWTManager(secretKey string, tokenDuration time.Duration) *JWTManager {
|
||||
return &JWTManager{
|
||||
secretKey: []byte(secretKey),
|
||||
tokenDuration: tokenDuration,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateToken creates a new JWT token for a user
|
||||
func (m *JWTManager) GenerateToken(userID int64, email string) (string, error) {
|
||||
claims := Claims{
|
||||
UserID: userID,
|
||||
Email: email,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(m.tokenDuration)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: os.Getenv("JWT_ISSUER"),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
tokenString, err := token.SignedString(m.secretKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign token: %w", err)
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
// ValidateToken validates the JWT token and returns the claims
|
||||
func (m *JWTManager) ValidateToken(tokenString string) (*Claims, error) {
|
||||
token, err := jwt.ParseWithClaims(
|
||||
tokenString,
|
||||
&Claims{},
|
||||
func(token *jwt.Token) (interface{}, error) {
|
||||
// Verify signing method
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return m.secretKey, nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, jwt.ErrTokenExpired) {
|
||||
return nil, ErrExpiredToken
|
||||
}
|
||||
if errors.Is(err, jwt.ErrSignatureInvalid) {
|
||||
return nil, ErrInvalidSignature
|
||||
}
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(*Claims)
|
||||
if !ok || !token.Valid {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// RefreshToken generates a new token from an existing valid token
|
||||
func (m *JWTManager) RefreshToken(tokenString string) (string, error) {
|
||||
claims, err := m.ValidateToken(tokenString)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return m.GenerateToken(claims.UserID, claims.Email)
|
||||
}
|
||||
41
internal/database/postgres.go
Normal file
41
internal/database/postgres.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
User string
|
||||
Password string
|
||||
DBName string
|
||||
}
|
||||
|
||||
func NewConnection(cfg Config) (*sql.DB, error) {
|
||||
dsn := fmt.Sprintf(
|
||||
"host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
|
||||
cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName,
|
||||
)
|
||||
|
||||
db, err := sql.Open("postgres", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Connection pooling
|
||||
db.SetMaxOpenConns(25)
|
||||
db.SetMaxOpenConns(5)
|
||||
db.SetConnMaxLifetime(5 * time.Minute)
|
||||
db.SetConnMaxIdleTime(10 * time.Minute)
|
||||
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
30
internal/handlers/health.go
Normal file
30
internal/handlers/health.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type HealthResponse struct {
|
||||
Status string `json:"status"`
|
||||
DBConnected bool `json:"db_connected"`
|
||||
}
|
||||
|
||||
type HealthcheckService struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewHealthcheckService(db *sql.DB) *HealthcheckService {
|
||||
return &HealthcheckService{db: db}
|
||||
}
|
||||
|
||||
func (h *HealthcheckService) Health(w http.ResponseWriter, r *http.Request) {
|
||||
response := HealthResponse{
|
||||
Status: "ok",
|
||||
}
|
||||
|
||||
err := h.db.Ping()
|
||||
response.DBConnected = err == nil
|
||||
|
||||
JSON(w, http.StatusOK, response)
|
||||
}
|
||||
16
internal/handlers/responses.go
Normal file
16
internal/handlers/responses.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func JSON(w http.ResponseWriter, status int, data any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
|
||||
func Error(w http.ResponseWriter, status int, message string) {
|
||||
JSON(w, status, map[string]string{"error": message})
|
||||
}
|
||||
56
internal/middleware/auth.go
Normal file
56
internal/middleware/auth.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.kling.dev/jared/WorkoutTrackerAPI/internal/auth"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
const (
|
||||
UserIDKey contextKey = "user_id"
|
||||
EmailKey contextKey = "email"
|
||||
)
|
||||
|
||||
func ValidateJWT(jwtManager auth.JWTManager) func(http.HandlerFunc) http.HandlerFunc {
|
||||
return func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("Starting ValidateJWT handler")
|
||||
auth_header := r.Header.Get("Authorization")
|
||||
if auth_header == "" {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Missing authorization header"))
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.Split(auth_header, " ")
|
||||
if len(parts) != 2 || strings.ToLower(strings.Trim(parts[0], " ")) != "bearer" {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Invalid authorization header format"))
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := parts[1]
|
||||
claims, err := jwtManager.ValidateToken(tokenString)
|
||||
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
if errors.Is(err, auth.ErrExpiredToken) {
|
||||
w.Write([]byte("Token has expired"))
|
||||
return
|
||||
}
|
||||
w.Write([]byte("Invalid token"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), UserIDKey, claims.Subject)
|
||||
ctx = context.WithValue(ctx, EmailKey, claims.Email)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
18
internal/middleware/cors.go
Normal file
18
internal/middleware/cors.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package middleware
|
||||
|
||||
import "net/http"
|
||||
|
||||
func CORS(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
30
internal/middleware/logging.go
Normal file
30
internal/middleware/logging.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Logging(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
|
||||
wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
|
||||
|
||||
next.ServeHTTP(wrapped, r)
|
||||
|
||||
log.Printf(
|
||||
"%s %s %d %s",
|
||||
r.Method,
|
||||
r.URL.Path,
|
||||
wrapped.statusCode,
|
||||
time.Since(start),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
type responseWriter struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
}
|
||||
108
internal/server/server.go
Normal file
108
internal/server/server.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.kling.dev/jared/WorkoutTrackerAPI/internal/auth"
|
||||
"git.kling.dev/jared/WorkoutTrackerAPI/internal/handlers"
|
||||
"git.kling.dev/jared/WorkoutTrackerAPI/internal/middleware"
|
||||
)
|
||||
|
||||
type Services struct {
|
||||
healthcheckService handlers.HealthcheckService
|
||||
}
|
||||
|
||||
type Handlers struct {
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
db *sql.DB
|
||||
router *http.ServeMux
|
||||
server *http.Server
|
||||
services *Services
|
||||
handlers *Handlers
|
||||
jwtManager *auth.JWTManager
|
||||
}
|
||||
|
||||
type UserMeResponse struct {
|
||||
UserId int64 `json:"userId"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
func NewServer(db *sql.DB, jwtSecret string) *Server {
|
||||
s := &Server{
|
||||
db: db,
|
||||
router: http.NewServeMux(),
|
||||
server: nil,
|
||||
}
|
||||
|
||||
s.services = &Services{
|
||||
healthcheckService: *handlers.NewHealthcheckService(db),
|
||||
}
|
||||
|
||||
s.jwtManager = auth.NewJWTManager(jwtSecret, 60*time.Minute)
|
||||
|
||||
s.routes()
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Server) routes() {
|
||||
auth := middleware.ValidateJWT(*s.jwtManager)
|
||||
s.router.HandleFunc("GET /health", s.services.healthcheckService.Health)
|
||||
s.router.HandleFunc("POST /auth", func(w http.ResponseWriter, r *http.Request) {
|
||||
token, err := s.jwtManager.GenerateToken(123, "jared")
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
handlers.JSON(w, http.StatusOK, token)
|
||||
})
|
||||
s.router.HandleFunc("GET /auth/me", auth(func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("Start of /auth/me")
|
||||
ctxUserId := r.Context().Value(middleware.UserIDKey).(string)
|
||||
email := r.Context().Value(middleware.EmailKey).(string)
|
||||
userId, _ := strconv.ParseInt(ctxUserId, 10, 64)
|
||||
log.Printf("Values: %d - %s", userId, email)
|
||||
|
||||
response := UserMeResponse{
|
||||
UserId: userId,
|
||||
Email: email,
|
||||
}
|
||||
handlers.JSON(w, http.StatusOK, response)
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *Server) Start(addr string) error {
|
||||
handler := middleware.Logging(
|
||||
middleware.CORS(
|
||||
s.router,
|
||||
),
|
||||
)
|
||||
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: handler,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
s.server = server
|
||||
|
||||
return server.ListenAndServe()
|
||||
}
|
||||
|
||||
func (s *Server) Shutdown(ctx context.Context) error {
|
||||
if s.server == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.server.Shutdown(ctx)
|
||||
}
|
||||
1
migrations/000001_initial-schema.down.sql
Normal file
1
migrations/000001_initial-schema.down.sql
Normal file
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS users;
|
||||
10
migrations/000001_initial-schema.up.sql
Normal file
10
migrations/000001_initial-schema.up.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
username VARCHAR(100) NOT NULL UNIQUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_users_email ON users(email);
|
||||
CREATE INDEX idx_users_username ON users(username);
|
||||
5
migrations/000002_create_workouts_tables.down.sql
Normal file
5
migrations/000002_create_workouts_tables.down.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
DROP TABLE IF EXISTS exercise_sets;
|
||||
DROP TABLE IF EXISTS workout_exercises;
|
||||
DROP TABLE IF EXISTS workouts;
|
||||
DROP TABLE IF EXISTS exercises;
|
||||
DROP TABLE IF EXISTS exercise_categories;
|
||||
62
migrations/000002_create_workouts_tables.up.sql
Normal file
62
migrations/000002_create_workouts_tables.up.sql
Normal file
@@ -0,0 +1,62 @@
|
||||
CREATE TABLE exercise_categories (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(100) NOT NULL UNIQUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE exercises (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
category_id UUID NULL REFERENCES exercise_categories(id) ON DELETE RESTRICT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
|
||||
);
|
||||
|
||||
CREATE INDEX idx_exercises_category_id ON exercises(category_id);
|
||||
|
||||
CREATE TABLE workouts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NULL,
|
||||
notes TEXT,
|
||||
workout_date TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_workouts_user_id ON workouts(user_id);
|
||||
CREATE INDEX idx_workouts_workout_date ON workouts(workout_date);
|
||||
|
||||
CREATE TABLE workout_exercises (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workout_id UUID NOT NULL REFERENCES workouts(id) ON DELETE CASCADE,
|
||||
exercise_id UUID NOT NULL REFERENCES exercises(id) ON DELETE RESTRICT,
|
||||
notes TEXT,
|
||||
order_index INT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_workout_exercises_workout_id ON workout_exercises(workout_id);
|
||||
CREATE INDEX idx_workout_exercises_exercise_id ON workout_exercises(exercise_id);
|
||||
|
||||
CREATE TABLE exercise_sets (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workout_exercise_id UUID NOT NULL REFERENCES workout_exercises(id) ON DELETE CASCADE,
|
||||
set_number INT NOT NULL,
|
||||
reps INT,
|
||||
weight DECIMAL(10,2),
|
||||
weight_unit VARCHAR(10),
|
||||
duration_seconds INTEGER,
|
||||
distance DECIMAL(10,2),
|
||||
distance_unit VARCHAR(10),
|
||||
notes TEXT,
|
||||
rate_perceived_exertion INT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_exercise_sets_workout_exercise_id ON exercise_sets(workout_exercise_id);
|
||||
9
seeds/exercise_categories.sql
Normal file
9
seeds/exercise_categories.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
-- Exercise Categories
|
||||
INSERT INTO exercise_categories (id, name) VALUES
|
||||
('550e8400-e29b-41d4-a716-446655440001', 'Strength'),
|
||||
('550e8400-e29b-41d4-a716-446655440002', 'Cardio'),
|
||||
('550e8400-e29b-41d4-a716-446655440003', 'Flexibility'),
|
||||
('550e8400-e29b-41d4-a716-446655440004', 'Olympic Lifting'),
|
||||
('550e8400-e29b-41d4-a716-446655440005', 'Bodyweight'),
|
||||
('550e8400-e29b-41d4-a716-446655440006', 'Sports')
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
84
seeds/exercises.sql
Normal file
84
seeds/exercises.sql
Normal file
@@ -0,0 +1,84 @@
|
||||
-- Strength Exercises
|
||||
INSERT INTO exercises (name, description, category_id) VALUES
|
||||
-- Chest
|
||||
('Barbell Bench Press', 'Compound chest exercise', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
('Dumbbell Bench Press', 'Chest exercise with dumbbells', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
('Incline Barbell Bench Press', 'Upper chest focus', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
('Decline Bench Press', 'Lower chest focus', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
('Chest Fly', 'Isolation chest exercise', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
|
||||
-- Back
|
||||
('Barbell Deadlift', 'Compound posterior chain exercise', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
('Romanian Deadlift', 'Hamstring and glute focus', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
('Barbell Row', 'Back thickness builder', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
('Dumbbell Row', 'Unilateral back exercise', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
('Lat Pulldown', 'Lat width builder', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
('Pull-ups', 'Bodyweight back exercise', '550e8400-e29b-41d4-a716-446655440005'),
|
||||
('Chin-ups', 'Bicep emphasis pull-up variation', '550e8400-e29b-41d4-a716-446655440005'),
|
||||
|
||||
-- Legs
|
||||
('Barbell Back Squat', 'Compound leg exercise', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
('Front Squat', 'Quad-focused squat variation', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
('Bulgarian Split Squat', 'Unilateral leg exercise', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
('Leg Press', 'Machine leg exercise', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
('Leg Curl', 'Hamstring isolation', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
('Leg Extension', 'Quad isolation', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
('Calf Raise', 'Calf exercise', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
('Walking Lunge', 'Dynamic leg exercise', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
|
||||
-- Shoulders
|
||||
('Overhead Press', 'Compound shoulder exercise', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
('Dumbbell Shoulder Press', 'Dumbbell shoulder variation', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
('Lateral Raise', 'Side delt isolation', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
('Front Raise', 'Front delt isolation', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
('Face Pull', 'Rear delt and upper back', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
('Upright Row', 'Shoulder and trap exercise', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
|
||||
-- Arms
|
||||
('Barbell Curl', 'Bicep exercise', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
('Dumbbell Curl', 'Unilateral bicep exercise', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
('Hammer Curl', 'Brachialis focus', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
('Tricep Pushdown', 'Tricep isolation', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
('Skull Crusher', 'Tricep exercise', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
('Close-Grip Bench Press', 'Tricep compound movement', '550e8400-e29b-41d4-a716-446655440001'),
|
||||
('Dips', 'Compound tricep and chest exercise', '550e8400-e29b-41d4-a716-446655440005')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Olympic Lifting
|
||||
INSERT INTO exercises (name, description, category_id) VALUES
|
||||
('Clean and Jerk', 'Olympic lift - ground to overhead', '550e8400-e29b-41d4-a716-446655440004'),
|
||||
('Snatch', 'Olympic lift - single movement overhead', '550e8400-e29b-41d4-a716-446655440004'),
|
||||
('Power Clean', 'Explosive pulling movement', '550e8400-e29b-41d4-a716-446655440004'),
|
||||
('Hang Clean', 'Clean from hang position', '550e8400-e29b-41d4-a716-446655440004')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Bodyweight
|
||||
INSERT INTO exercises (name, description, category_id) VALUES
|
||||
('Push-ups', 'Bodyweight chest exercise', '550e8400-e29b-41d4-a716-446655440005'),
|
||||
('Diamond Push-ups', 'Tricep-focused push-up variation', '550e8400-e29b-41d4-a716-446655440005'),
|
||||
('Plank', 'Core stability exercise', '550e8400-e29b-41d4-a716-446655440005'),
|
||||
('Side Plank', 'Oblique stability', '550e8400-e29b-41d4-a716-446655440005'),
|
||||
('Hanging Leg Raise', 'Core and hip flexor exercise', '550e8400-e29b-41d4-a716-446655440005'),
|
||||
('Pistol Squat', 'Single-leg squat', '550e8400-e29b-41d4-a716-446655440005'),
|
||||
('Burpees', 'Full body conditioning', '550e8400-e29b-41d4-a716-446655440005'),
|
||||
('Mountain Climbers', 'Core and cardio', '550e8400-e29b-41d4-a716-446655440005')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Cardio
|
||||
INSERT INTO exercises (name, description, category_id) VALUES
|
||||
('Running', 'Outdoor or treadmill running', '550e8400-e29b-41d4-a716-446655440002'),
|
||||
('Cycling', 'Road or stationary bike', '550e8400-e29b-41d4-a716-446655440002'),
|
||||
('Rowing', 'Full body cardio on rower', '550e8400-e29b-41d4-a716-446655440002'),
|
||||
('Swimming', 'Low impact cardio', '550e8400-e29b-41d4-a716-446655440002'),
|
||||
('Jump Rope', 'High intensity cardio', '550e8400-e29b-41d4-a716-446655440002'),
|
||||
('Elliptical', 'Low impact machine cardio', '550e8400-e29b-41d4-a716-446655440002'),
|
||||
('Stair Climber', 'Lower body cardio', '550e8400-e29b-41d4-a716-446655440002')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Flexibility
|
||||
INSERT INTO exercises (name, description, category_id) VALUES
|
||||
('Yoga', 'Flexibility and mindfulness practice', '550e8400-e29b-41d4-a716-446655440003'),
|
||||
('Static Stretching', 'Hold stretch positions', '550e8400-e29b-41d4-a716-446655440003'),
|
||||
('Dynamic Stretching', 'Active stretching movements', '550e8400-e29b-41d4-a716-446655440003'),
|
||||
('Foam Rolling', 'Self-myofascial release', '550e8400-e29b-41d4-a716-446655440003')
|
||||
ON CONFLICT DO NOTHING;
|
||||
7
seeds/users.sql
Normal file
7
seeds/users.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
-- Seed Users
|
||||
INSERT INTO users (id, email, username) VALUES
|
||||
('650e8400-e29b-41d4-a716-446655440001', 'john.doe@example.com', 'johndoe'),
|
||||
('650e8400-e29b-41d4-a716-446655440002', 'jane.smith@example.com', 'janesmith'),
|
||||
('650e8400-e29b-41d4-a716-446655440003', 'mike.wilson@example.com', 'mikewilson'),
|
||||
('650e8400-e29b-41d4-a716-446655440004', 'sarah.jones@example.com', 'sarahjones')
|
||||
ON CONFLICT (email) DO NOTHING;
|
||||
136
seeds/workouts.sql
Normal file
136
seeds/workouts.sql
Normal file
@@ -0,0 +1,136 @@
|
||||
-- Workout 1: John's Push Day
|
||||
INSERT INTO workouts (id, user_id, name, notes, workout_date) VALUES
|
||||
('750e8400-e29b-41d4-a716-446655440001', '650e8400-e29b-41d4-a716-446655440001', 'Push Day', 'Felt strong today', '2024-10-01 09:00:00');
|
||||
|
||||
-- Workout exercises for Push Day
|
||||
INSERT INTO workout_exercises (id, workout_id, exercise_id, order_index, notes) VALUES
|
||||
('850e8400-e29b-41d4-a716-446655440001', '750e8400-e29b-41d4-a716-446655440001',
|
||||
(SELECT id FROM exercises WHERE name = 'Barbell Bench Press'), 1, 'Felt good on chest'),
|
||||
('850e8400-e29b-41d4-a716-446655440002', '750e8400-e29b-41d4-a716-446655440001',
|
||||
(SELECT id FROM exercises WHERE name = 'Overhead Press'), 2, NULL),
|
||||
('850e8400-e29b-41d4-a716-446655440003', '750e8400-e29b-41d4-a716-446655440001',
|
||||
(SELECT id FROM exercises WHERE name = 'Tricep Pushdown'), 3, 'Burned out on last set');
|
||||
|
||||
-- Sets for Bench Press
|
||||
INSERT INTO exercise_sets (workout_exercise_id, set_number, reps, weight, weight_unit, rate_perceived_exertion) VALUES
|
||||
('850e8400-e29b-41d4-a716-446655440001', 1, 10, 135.0, 'lbs', 6),
|
||||
('850e8400-e29b-41d4-a716-446655440001', 2, 8, 185.0, 'lbs', 7),
|
||||
('850e8400-e29b-41d4-a716-446655440001', 3, 6, 205.0, 'lbs', 8),
|
||||
('850e8400-e29b-41d4-a716-446655440001', 4, 8, 185.0, 'lbs', 9);
|
||||
|
||||
-- Sets for Overhead Press
|
||||
INSERT INTO exercise_sets (workout_exercise_id, set_number, reps, weight, weight_unit, rate_perceived_exertion) VALUES
|
||||
('850e8400-e29b-41d4-a716-446655440002', 1, 10, 95.0, 'lbs', 6),
|
||||
('850e8400-e29b-41d4-a716-446655440002', 2, 8, 115.0, 'lbs', 7),
|
||||
('850e8400-e29b-41d4-a716-446655440002', 3, 6, 125.0, 'lbs', 8);
|
||||
|
||||
-- Sets for Tricep Pushdown
|
||||
INSERT INTO exercise_sets (workout_exercise_id, set_number, reps, weight, weight_unit, rate_perceived_exertion) VALUES
|
||||
('850e8400-e29b-41d4-a716-446655440003', 1, 12, 50.0, 'lbs', 6),
|
||||
('850e8400-e29b-41d4-a716-446655440003', 2, 10, 60.0, 'lbs', 7),
|
||||
('850e8400-e29b-41d4-a716-446655440003', 3, 8, 70.0, 'lbs', 9);
|
||||
|
||||
-- Workout 2: Jane's Leg Day
|
||||
INSERT INTO workouts (id, user_id, name, notes, workout_date) VALUES
|
||||
('750e8400-e29b-41d4-a716-446655440002', '650e8400-e29b-41d4-a716-446655440002', 'Leg Day', 'Tough session but got through it', '2024-10-02 10:30:00');
|
||||
|
||||
-- Workout exercises for Leg Day
|
||||
INSERT INTO workout_exercises (id, workout_id, exercise_id, order_index) VALUES
|
||||
('850e8400-e29b-41d4-a716-446655440004', '750e8400-e29b-41d4-a716-446655440002',
|
||||
(SELECT id FROM exercises WHERE name = 'Barbell Back Squat'), 1),
|
||||
('850e8400-e29b-41d4-a716-446655440005', '750e8400-e29b-41d4-a716-446655440002',
|
||||
(SELECT id FROM exercises WHERE name = 'Romanian Deadlift'), 2),
|
||||
('850e8400-e29b-41d4-a716-446655440006', '750e8400-e29b-41d4-a716-446655440002',
|
||||
(SELECT id FROM exercises WHERE name = 'Walking Lunge'), 3);
|
||||
|
||||
-- Sets for Squat
|
||||
INSERT INTO exercise_sets (workout_exercise_id, set_number, reps, weight, weight_unit, rate_perceived_exertion) VALUES
|
||||
('850e8400-e29b-41d4-a716-446655440004', 1, 8, 135.0, 'lbs', 6),
|
||||
('850e8400-e29b-41d4-a716-446655440004', 2, 6, 155.0, 'lbs', 7),
|
||||
('850e8400-e29b-41d4-a716-446655440004', 3, 5, 175.0, 'lbs', 8),
|
||||
('850e8400-e29b-41d4-a716-446655440004', 4, 6, 155.0, 'lbs', 9);
|
||||
|
||||
-- Sets for Romanian Deadlift
|
||||
INSERT INTO exercise_sets (workout_exercise_id, set_number, reps, weight, weight_unit, rate_perceived_exertion) VALUES
|
||||
('850e8400-e29b-41d4-a716-446655440005', 1, 10, 95.0, 'lbs', 6),
|
||||
('850e8400-e29b-41d4-a716-446655440005', 2, 10, 115.0, 'lbs', 7),
|
||||
('850e8400-e29b-41d4-a716-446655440005', 3, 8, 135.0, 'lbs', 8);
|
||||
|
||||
-- Sets for Walking Lunge
|
||||
INSERT INTO exercise_sets (workout_exercise_id, set_number, reps, weight, weight_unit, rate_perceived_exertion) VALUES
|
||||
('850e8400-e29b-41d4-a716-446655440006', 1, 20, 25.0, 'lbs', 7),
|
||||
('850e8400-e29b-41d4-a716-446655440006', 2, 20, 25.0, 'lbs', 8),
|
||||
('850e8400-e29b-41d4-a716-446655440006', 3, 16, 30.0, 'lbs', 9);
|
||||
|
||||
-- Workout 3: Mike's Cardio Session
|
||||
INSERT INTO workouts (id, user_id, name, notes, workout_date) VALUES
|
||||
('750e8400-e29b-41d4-a716-446655440003', '650e8400-e29b-41d4-a716-446655440003', 'Morning Run', 'Beautiful weather', '2024-10-03 06:00:00');
|
||||
|
||||
INSERT INTO workout_exercises (id, workout_id, exercise_id, order_index) VALUES
|
||||
('850e8400-e29b-41d4-a716-446655440007', '750e8400-e29b-41d4-a716-446655440003',
|
||||
(SELECT id FROM exercises WHERE name = 'Running'), 1);
|
||||
|
||||
-- Cardio set with distance and duration
|
||||
INSERT INTO exercise_sets (workout_exercise_id, set_number, duration_seconds, distance, distance_unit, rate_perceived_exertion) VALUES
|
||||
('850e8400-e29b-41d4-a716-446655440007', 1, 1800, 3.1, 'miles', 6);
|
||||
|
||||
-- Workout 4: Sarah's Pull Day
|
||||
INSERT INTO workouts (id, user_id, name, notes, workout_date) VALUES
|
||||
('750e8400-e29b-41d4-a716-446655440004', '650e8400-e29b-41d4-a716-446655440004', 'Pull Day', 'New PR on deadlift!', '2024-10-04 14:00:00');
|
||||
|
||||
INSERT INTO workout_exercises (id, workout_id, exercise_id, order_index, notes) VALUES
|
||||
('850e8400-e29b-41d4-a716-446655440008', '750e8400-e29b-41d4-a716-446655440004',
|
||||
(SELECT id FROM exercises WHERE name = 'Barbell Deadlift'), 1, 'PR on third set!'),
|
||||
('850e8400-e29b-41d4-a716-446655440009', '750e8400-e29b-41d4-a716-446655440004',
|
||||
(SELECT id FROM exercises WHERE name = 'Pull-ups'), 2, NULL),
|
||||
('850e8400-e29b-41d4-a716-446655440010', '750e8400-e29b-41d4-a716-446655440004',
|
||||
(SELECT id FROM exercises WHERE name = 'Barbell Row'), 3, NULL);
|
||||
|
||||
-- Sets for Deadlift
|
||||
INSERT INTO exercise_sets (workout_exercise_id, set_number, reps, weight, weight_unit, rate_perceived_exertion) VALUES
|
||||
('850e8400-e29b-41d4-a716-446655440008', 1, 5, 185.0, 'lbs', 6),
|
||||
('850e8400-e29b-41d4-a716-446655440008', 2, 3, 225.0, 'lbs', 7),
|
||||
('850e8400-e29b-41d4-a716-446655440008', 3, 1, 255.0, 'lbs', 9),
|
||||
('850e8400-e29b-41d4-a716-446655440008', 4, 5, 205.0, 'lbs', 8);
|
||||
|
||||
-- Sets for Pull-ups (bodyweight)
|
||||
INSERT INTO exercise_sets (workout_exercise_id, set_number, reps, rate_perceived_exertion) VALUES
|
||||
('850e8400-e29b-41d4-a716-446655440009', 1, 10, 7),
|
||||
('850e8400-e29b-41d4-a716-446655440009', 2, 8, 8),
|
||||
('850e8400-e29b-41d4-a716-446655440009', 3, 6, 9);
|
||||
|
||||
-- Sets for Barbell Row
|
||||
INSERT INTO exercise_sets (workout_exercise_id, set_number, reps, weight, weight_unit, rate_perceived_exertion) VALUES
|
||||
('850e8400-e29b-41d4-a716-446655440010', 1, 10, 95.0, 'lbs', 6),
|
||||
('850e8400-e29b-41d4-a716-446655440010', 2, 8, 115.0, 'lbs', 7),
|
||||
('850e8400-e29b-41d4-a716-446655440010', 3, 8, 115.0, 'lbs', 8);
|
||||
|
||||
-- Workout 5: John's Second Workout - Full Body
|
||||
INSERT INTO workouts (id, user_id, name, notes, workout_date) VALUES
|
||||
('750e8400-e29b-41d4-a716-446655440005', '650e8400-e29b-41d4-a716-446655440001', 'Full Body', 'Quick session before work', '2024-10-05 07:00:00');
|
||||
|
||||
INSERT INTO workout_exercises (id, workout_id, exercise_id, order_index) VALUES
|
||||
('850e8400-e29b-41d4-a716-446655440011', '750e8400-e29b-41d4-a716-446655440005',
|
||||
(SELECT id FROM exercises WHERE name = 'Front Squat'), 1),
|
||||
('850e8400-e29b-41d4-a716-446655440012', '750e8400-e29b-41d4-a716-446655440005',
|
||||
(SELECT id FROM exercises WHERE name = 'Dumbbell Bench Press'), 2),
|
||||
('850e8400-e29b-41d4-a716-446655440013', '750e8400-e29b-41d4-a716-446655440005',
|
||||
(SELECT id FROM exercises WHERE name = 'Lat Pulldown'), 3);
|
||||
|
||||
-- Sets for Front Squat
|
||||
INSERT INTO exercise_sets (workout_exercise_id, set_number, reps, weight, weight_unit, rate_perceived_exertion) VALUES
|
||||
('850e8400-e29b-41d4-a716-446655440011', 1, 8, 95.0, 'lbs', 6),
|
||||
('850e8400-e29b-41d4-a716-446655440011', 2, 8, 115.0, 'lbs', 7),
|
||||
('850e8400-e29b-41d4-a716-446655440011', 3, 6, 135.0, 'lbs', 8);
|
||||
|
||||
-- Sets for Dumbbell Bench Press
|
||||
INSERT INTO exercise_sets (workout_exercise_id, set_number, reps, weight, weight_unit, rate_perceived_exertion, notes) VALUES
|
||||
('850e8400-e29b-41d4-a716-446655440012', 1, 10, 50.0, 'lbs', 6, 'Per dumbbell'),
|
||||
('850e8400-e29b-41d4-a716-446655440012', 2, 8, 60.0, 'lbs', 7, 'Per dumbbell'),
|
||||
('850e8400-e29b-41d4-a716-446655440012', 3, 8, 60.0, 'lbs', 8, 'Per dumbbell');
|
||||
|
||||
-- Sets for Lat Pulldown
|
||||
INSERT INTO exercise_sets (workout_exercise_id, set_number, reps, weight, weight_unit, rate_perceived_exertion) VALUES
|
||||
('850e8400-e29b-41d4-a716-446655440013', 1, 12, 100.0, 'lbs', 6),
|
||||
('850e8400-e29b-41d4-a716-446655440013', 2, 10, 120.0, 'lbs', 7),
|
||||
('850e8400-e29b-41d4-a716-446655440013', 3, 8, 140.0, 'lbs', 8);
|
||||
Reference in New Issue
Block a user