diff --git a/.github/workflows/sqlite3-migrations.yml b/.github/workflows/sqlite3-migrations.yml new file mode 100644 index 0000000..0e53b98 --- /dev/null +++ b/.github/workflows/sqlite3-migrations.yml @@ -0,0 +1,25 @@ +name: SQLite3 Migrations + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + + runs-on: ubuntu-latest + + defaults: + run: + working-directory: ./backend + + steps: + - uses: actions/checkout@v3 + - name: Install SQLite3 + run: sudo apt-get install sqlite3 + - name: Install Make + run: sudo apt-get install make + - name: Run Migrations + run: make migrate diff --git a/.gitignore b/.gitignore index 9ad11ec..2d89407 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ bin db.sqlite3 +*.png # Test binary, built with `go test -c` *.test diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..668ccf1 --- /dev/null +++ b/Makefile @@ -0,0 +1,41 @@ +# Builds a release container +build-container-release: + podman build -t ttime-server -f container/Containerfile . + +# Builds a release container and runs it +start-release: build-container-release remove-podman-containers + podman run -d -e DATABASE_URL=sqlite:release.db -p 8080:8080 --name ttime ttime-server + @echo "Started production ttime-server on http://localhost:8080" + +# Removes and stops any containers related to the project +remove-podman-containers: + podman container rm -fi ttime + +# Tests every part of the project +testall: + cd frontend && npm test + cd frontend && npm run lint + cd backend && make test + cd backend && make lint + +# Cleans up everything related to the project +clean: remove-podman-containers + podman image rm -fi ttime-server + rm -rf frontend/dist + rm -rf frontend/node_modules + rm -f ttime-server.tar.gz + cd backend && make clean + @echo "Cleaned up!" + +# Cleans up everything related to podman, not just the project. Make sure you understand what this means. +podman-clean: + podman system reset --force + +# Installs the linter, which is not included in the ubuntu repo +install-linter: + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.56.2 + +# This installs just, a make alternative, which is slightly more ergonomic to use +install-just: + @echo "Installing just" + @curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin \ No newline at end of file diff --git a/README.md b/README.md index fc55126..d75861a 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ My recommendation would be to make WSL your primary development environment if y You should consult the [WSL documentation](https://docs.microsoft.com/en-us/windows/wsl/install), but for any recent version of windows, installation essentially boils down to running the following command in **PowerShell as an administrator**: ```powershell -wsl --install +wsl --install -d Ubuntu-22.04 # To get a somewhat recent version of Go ``` If you get any errors related to virtualization, you will need to enable virtualization in the BIOS. This is a common issue, and you can find a guide for your specific motherboard online. This is a one-time operation and will not affect your windows installation. This setting is usually called "VT-x" or "AMD-V" and is usually found in the CPU settings. If you can't find it, shoot me a message and I'll find it for you. diff --git a/backend/Makefile b/backend/Makefile index dcc79b4..d005846 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -13,7 +13,7 @@ MIGRATIONS_DIR = internal/database/migrations # Build target build: - $(GOBUILD) -o bin/server cmd/*.go + $(GOBUILD) -o bin/server main.go # Run target run: build @@ -43,6 +43,7 @@ update: # Migration target migrate: + @echo "If this ever fails, run make clean and try again" @echo "Migrating database $(DB_FILE) using SQL scripts in $(MIGRATIONS_DIR)" @for file in $(wildcard $(MIGRATIONS_DIR)/*.sql); do \ echo "Applying migration: $$file"; \ @@ -70,6 +71,31 @@ lint: # Default target default: build +# Generate swagger docs +.PHONY: docs +docs: + swag init -outputTypes go + +.PHONY: docfmt +docfmt: + swag fmt + +# Generate ERD +# Requires eralchemy2 +.PHONY: erd +erd: + eralchemy2 -i sqlite:///db.sqlite3 -o erd.png + +install-swag: + @echo "Installing swag" + @go get -u github.com/swaggo/swag/cmd/swag + +# Convenience target to install golangci-lint +install-lint: + @echo "Installing golangci-lint" + @curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.42.1 + +# Convenience target to install just (requires sudo privileges) install-just: @echo "Installing just" @curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin diff --git a/backend/docs/docs.go b/backend/docs/docs.go new file mode 100644 index 0000000..8026f58 --- /dev/null +++ b/backend/docs/docs.go @@ -0,0 +1,80 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "license": { + "name": "AGPL", + "url": "https://www.gnu.org/licenses/agpl-3.0.html" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/api/register": { + "post": { + "description": "Register a new user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Register a new user", + "responses": { + "200": { + "description": "User added", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } + } + }, + "externalDocs": { + "description": "OpenAPI", + "url": "https://swagger.io/resources/open-api/" + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "0.0.1", + Host: "localhost:8080", + BasePath: "/api", + Schemes: []string{}, + Title: "TTime API", + Description: "This is the API for TTime, a time tracking application.", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/backend/go.mod b/backend/go.mod index ab9be66..c251e95 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -8,10 +8,36 @@ require ( github.com/mattn/go-sqlite3 v1.14.22 ) +require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.2.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect + github.com/go-openapi/jsonpointer v0.20.3 // indirect + github.com/go-openapi/jsonreference v0.20.5 // indirect + github.com/go-openapi/spec v0.20.15 // indirect + github.com/go-openapi/swag v0.22.10 // indirect + github.com/gofiber/swagger v1.0.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect + github.com/swaggo/files/v2 v2.0.0 // indirect + github.com/swaggo/swag v1.16.3 // indirect + github.com/urfave/cli/v2 v2.27.1 // indirect + github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect + golang.org/x/net v0.22.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.19.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) + require ( github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect - github.com/gofiber/contrib/jwt v1.0.8 // indirect - github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/gofiber/contrib/jwt v1.0.8 + github.com/golang-jwt/jwt/v5 v5.2.1 ) // These are all for fiber diff --git a/backend/go.sum b/backend/go.sum index 42908f6..a2fdf1e 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,11 +1,28 @@ github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k= github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k= +github.com/PuerkitoBio/purell v1.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9N28= +github.com/PuerkitoBio/purell v1.2.1/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-openapi/jsonpointer v0.20.3 h1:jykzYWS/kyGtsHfRt6aV8JTB9pcQAXPIA7qlZ5aRlyk= +github.com/go-openapi/jsonpointer v0.20.3/go.mod h1:c7l0rjoouAuIxCm8v/JWKRgMjDG/+/7UBWsXMrv6PsM= +github.com/go-openapi/jsonreference v0.20.5 h1:hutI+cQI+HbSQaIGSfsBsYI0pHk+CATf8Fk5gCSj0yI= +github.com/go-openapi/jsonreference v0.20.5/go.mod h1:thAqAp31UABtI+FQGKAQfmv7DbFpKNUlva2UPCxKu2Y= +github.com/go-openapi/spec v0.20.15 h1:8bDcVxF607pTh9NpPwgsH4J5Uhh5mV5XoWnkurdiY+U= +github.com/go-openapi/spec v0.20.15/go.mod h1:o0upgqg5uYFG7O5mADrDVmSG3Wa6y6OLhwiCqQ+sTv4= +github.com/go-openapi/swag v0.22.10 h1:4y86NVn7Z2yYd6pfS4Z+Nyh3aAUL3Nul+LMbhFKy0gA= +github.com/go-openapi/swag v0.22.10/go.mod h1:Cnn8BYtRlx6BNE3DPN86f/xkapGIcLWzh3CLEb4C1jI= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/gofiber/contrib/jwt v1.0.8 h1:/GeOsm/Mr1OGr0GTy+RIVSz5VgNNyP3ZgK4wdqxF/WY= @@ -14,20 +31,28 @@ github.com/gofiber/fiber/v2 v2.52.1 h1:1RoU2NS+b98o1L77sdl5mboGPiW+0Ypsi5oLmcYlg github.com/gofiber/fiber/v2 v2.52.1/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= github.com/gofiber/fiber/v2 v2.52.2 h1:b0rYH6b06Df+4NyrbdptQL8ifuxw/Tf2DgfkZkDaxEo= github.com/gofiber/fiber/v2 v2.52.2/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= +github.com/gofiber/swagger v1.0.0 h1:BzUzDS9ZT6fDUa692kxmfOjc1DZiloLiPK/W5z1H1tc= +github.com/gofiber/swagger v1.0.0/go.mod h1:QrYNF1Yrc7ggGK6ATsJ6yfH/8Zi5bu9lA7wB8TmCecg= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -38,10 +63,21 @@ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= +github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= +github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= +github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= +github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= +github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= @@ -50,9 +86,24 @@ github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7g github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI= +github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= +golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index 6e86641..5a88873 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -2,9 +2,9 @@ package database import ( "embed" - "log" "os" "path/filepath" + "time" "github.com/jmoiron/sqlx" _ "github.com/mattn/go-sqlite3" @@ -12,11 +12,20 @@ import ( // Interface for the database type Database interface { + // Insert a new user into the database, password should be hashed before calling AddUser(username string, password string) error + RemoveUser(username string) error + PromoteToAdmin(username string) error GetUserId(username string) (int, error) AddProject(name string, description string, username string) error Migrate(dirname string) error + // AddTimeReport(projectname string, start time.Time, end time.Time) error + // AddUserToProject(username string, projectname string) error + // ChangeUserRole(username string, projectname string, role string) error + // AddTimeReport(projectname string, start time.Time, end time.Time) error + // AddUserToProject(username string, projectname string) error + // ChangeUserRole(username string, projectname string, role string) error } // This struct is a wrapper type that holds the database connection @@ -29,7 +38,11 @@ type Db struct { var scripts embed.FS const userInsert = "INSERT INTO users (username, password) VALUES (?, ?)" -const projectInsert = "INSERT INTO projects (name, description, user_id) SELECT ?, ?, id FROM users WHERE username = ?" +const projectInsert = "INSERT INTO projects (name, description, owner_user_id) SELECT ?, ?, id FROM users WHERE username = ?" +const promoteToAdmin = "INSERT INTO site_admin (admin_id) SELECT id FROM users WHERE username = ?" +const addTimeReport = "INSERT INTO activity (report_id, activity_nbr, start_time, end_time, break, comment) VALUES (?, ?, ?, ?, ?, ?)" // WIP +const addUserToProject = "INSERT INTO project_member (project_id, user_id, role) VALUES (?, ?, ?)" // WIP +// const changeUserRole = "" // DbConnect connects to the database func DbConnect(dbpath string) Database { @@ -48,6 +61,32 @@ func DbConnect(dbpath string) Database { return &Db{db} } +func (d *Db) AddTimeReport(projectname string, start time.Time, end time.Time, breakTime uint32) error { // WIP + _, err := d.Exec(addTimeReport, projectname, 0, start, end, breakTime, false) + return err +} + +func (d *Db) AddUserToProject(username string, projectname string, role string) error { // WIP + var userid int + userid, err := d.GetUserId(username) + if err != nil { + panic(err) + } + + var projectid int + projectid, err2 := d.GetProjectId(projectname) + if err2 != nil { + panic(err2) + } + + _, err3 := d.Exec(addUserToProject, projectid, userid, role) + return err3 +} + +// func (d *Db) ChangeUserRole(username string, projectname string, role string) error { + +// } + // AddUser adds a user to the database func (d *Db) AddUser(username string, password string) error { _, err := d.Exec(userInsert, username, password) @@ -60,9 +99,20 @@ func (d *Db) RemoveUser(username string) error { return err } +func (d *Db) PromoteToAdmin(username string) error { + _, err := d.Exec(promoteToAdmin, username) + return err +} + func (d *Db) GetUserId(username string) (int, error) { var id int - err := d.Get(&id, "SELECT id FROM users WHERE username = ?", username) + err := d.Get(&id, "SELECT id FROM users WHERE username = ?", username) // Borde det inte vara "user" i singular + return id, err +} + +func (d *Db) GetProjectId(projectname string) (int, error) { // WIP, denna kan vara goof + var id int + err := d.Get(&id, "SELECT id FROM project WHERE project_name = ?", projectname) return id, err } @@ -101,7 +151,6 @@ func (d *Db) Migrate(dirname string) error { if err != nil { return err } - log.Println("Executed SQL file:", file.Name()) } if tr.Commit() != nil { diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index 6830668..96eb9b7 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -74,3 +74,32 @@ func TestDbRemoveUser(t *testing.T) { t.Error("RemoveUser failed:", err) } } + +func TestPromoteToAdmin(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + err = db.AddUser("test", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + err = db.PromoteToAdmin("test") + if err != nil { + t.Error("PromoteToAdmin failed:", err) + } +} + +// func TestAddTimeReport(t *testing.T) { + +// } + +// func TestAddUserToProject(t *testing.T) { + +// } + +// func TestChangeUserRole(t *testing.T) { + +// } diff --git a/backend/internal/database/migrations/0010_users.sql b/backend/internal/database/migrations/0010_users.sql index 7cb23fd..5c9d329 100644 --- a/backend/internal/database/migrations/0010_users.sql +++ b/backend/internal/database/migrations/0010_users.sql @@ -1,3 +1,7 @@ +-- Id is a surrogate key for in ternal use +-- userId is what is used for external id +-- username is what is used for login +-- password is the hashed password CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY, userId TEXT DEFAULT (HEX(RANDOMBLOB(4))) NOT NULL UNIQUE, @@ -5,5 +9,6 @@ CREATE TABLE IF NOT EXISTS users ( password VARCHAR(255) NOT NULL ); +-- Users are commonly searched by username and userId CREATE INDEX IF NOT EXISTS users_username_index ON users (username); CREATE INDEX IF NOT EXISTS users_userId_index ON users (userId); \ No newline at end of file diff --git a/backend/internal/database/migrations/0020_projects.sql b/backend/internal/database/migrations/0020_projects.sql index 8592e75..adfb818 100644 --- a/backend/internal/database/migrations/0020_projects.sql +++ b/backend/internal/database/migrations/0020_projects.sql @@ -3,9 +3,9 @@ CREATE TABLE IF NOT EXISTS projects ( projectId TEXT DEFAULT (HEX(RANDOMBLOB(4))) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL UNIQUE, description TEXT NOT NULL, - user_id INTEGER NOT NULL, - FOREIGN KEY (user_id) REFERENCES users (id) + owner_user_id INTEGER NOT NULL, + FOREIGN KEY (owner_user_id) REFERENCES users (id) ); CREATE INDEX IF NOT EXISTS projects_projectId_index ON projects (projectId); -CREATE INDEX IF NOT EXISTS projects_user_id_index ON projects (user_id); \ No newline at end of file +CREATE INDEX IF NOT EXISTS projects_user_id_index ON projects (owner_user_id); \ No newline at end of file diff --git a/backend/internal/database/migrations/0049_project_role.sql b/backend/internal/database/migrations/0049_project_role.sql new file mode 100644 index 0000000..8716800 --- /dev/null +++ b/backend/internal/database/migrations/0049_project_role.sql @@ -0,0 +1,9 @@ +-- This table represents the possible role a user can have in a project. +-- It has nothing to do with the site admin table. +CREATE TABLE IF NOT EXISTS project_role ( + p_role TEXT PRIMARY KEY +); + +-- Insert the possible roles a user can have in a project. +INSERT OR IGNORE INTO project_role (p_role) VALUES ('admin'); +INSERT OR IGNORE INTO project_role (p_role) VALUES ('member'); diff --git a/backend/internal/database/migrations/0050_user_roles.sql b/backend/internal/database/migrations/0050_user_roles.sql index 56e597b..aad25f7 100644 --- a/backend/internal/database/migrations/0050_user_roles.sql +++ b/backend/internal/database/migrations/0050_user_roles.sql @@ -1,16 +1,9 @@ CREATE TABLE IF NOT EXISTS user_roles ( user_id INTEGER NOT NULL, project_id INTEGER NOT NULL, - role STRING NOT NULL, -- 'admin' or 'member' + p_role TEXT NOT NULL, -- 'admin' or 'member' FOREIGN KEY (user_id) REFERENCES users (id) FOREIGN KEY (project_id) REFERENCES projects (id) + FOREIGN KEY (p_role) REFERENCES project_role (p_role) PRIMARY KEY (user_id, project_id) -); - --- Make sure that the role is either 'admin' or 'member' -CREATE TRIGGER IF NOT EXISTS user_role_admin_or_member - BEFORE INSERT ON user_roles - FOR EACH ROW - BEGIN - SELECT RAISE(ABORT, 'Invalid role') WHERE NEW.role NOT IN ('admin', 'member'); - END; \ No newline at end of file +); \ No newline at end of file diff --git a/backend/internal/database/migrations/0060_site_admin.sql b/backend/internal/database/migrations/0060_site_admin.sql new file mode 100644 index 0000000..14de60d --- /dev/null +++ b/backend/internal/database/migrations/0060_site_admin.sql @@ -0,0 +1,4 @@ +CREATE TABLE IF NOT EXISTS site_admin ( + admin_id INTEGER PRIMARY KEY, + FOREIGN KEY (admin_id) REFERENCES users (id) ON DELETE CASCADE +) \ No newline at end of file diff --git a/backend/internal/database/migrations/0070_salts.sql b/backend/internal/database/migrations/0070_salts.sql new file mode 100644 index 0000000..b84dfac --- /dev/null +++ b/backend/internal/database/migrations/0070_salts.sql @@ -0,0 +1,16 @@ +-- It is unclear weather this table will be used + +-- Create the table to store hash salts +CREATE TABLE salts ( + id INTEGER PRIMARY KEY, + salt TEXT NOT NULL +); + +-- Commented out for now, no time for good practices, which is atrocious +-- Create a trigger to automatically generate a salt when inserting a new user record +-- CREATE TRIGGER generate_salt_trigger +-- AFTER INSERT ON users +-- BEGIN +-- INSERT INTO salts (salt) VALUES (randomblob(16)); +-- UPDATE users SET salt_id = (SELECT last_insert_rowid()) WHERE id = new.id; +-- END; diff --git a/backend/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go index 5dc8895..9c42133 100644 --- a/backend/internal/handlers/global_state.go +++ b/backend/internal/handlers/global_state.go @@ -11,11 +11,11 @@ import ( // The actual interface that we will use type GlobalState interface { - Register(c *fiber.Ctx) error // To register a new user - UserDelete(c *fiber.Ctx) error // To delete a user - Login(c *fiber.Ctx) error // To get the token - LoginRenew(c *fiber.Ctx) error // To renew the token - // CreateProject(c *fiber.Ctx) error // To create a new project + Register(c *fiber.Ctx) error // To register a new user + UserDelete(c *fiber.Ctx) error // To delete a user + Login(c *fiber.Ctx) error // To get the token + LoginRenew(c *fiber.Ctx) error // To renew the token + CreateProject(c *fiber.Ctx) error // To create a new project // GetProjects(c *fiber.Ctx) error // To get all projects // GetProject(c *fiber.Ctx) error // To get a specific project // UpdateProject(c *fiber.Ctx) error // To update a project @@ -46,8 +46,19 @@ type GState struct { ButtonCount int } +// Register is a simple handler that registers a new user +// +// @Summary Register a new user +// @Description Register a new user +// @Tags User +// @Accept json +// @Produce json +// @Success 200 {string} string "User added" +// @Failure 400 {string} string "Bad request" +// @Failure 500 {string} string "Internal server error" +// @Router /api/register [post] func (gs *GState) Register(c *fiber.Ctx) error { - u := new(types.User) + u := new(types.NewUser) if err := c.BodyParser(u); err != nil { return c.Status(400).SendString(err.Error()) } @@ -115,6 +126,7 @@ func (gs *GState) Login(c *fiber.Ctx) error { // LoginRenew is a simple handler that renews the token func (gs *GState) LoginRenew(c *fiber.Ctx) error { + // For testing: curl localhost:3000/restricted -H "Authorization: Bearer " user := c.Locals("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) claims["exp"] = time.Now().Add(time.Hour * 72).Unix() @@ -130,3 +142,24 @@ func (gs *GState) LoginRenew(c *fiber.Ctx) error { } return c.JSON(fiber.Map{"token": t}) } + +// CreateProject is a simple handler that creates a new project +func (gs *GState) CreateProject(c *fiber.Ctx) error { + user := c.Locals("user").(*jwt.Token) + + p := new(types.NewProject) + if err := c.BodyParser(p); err != nil { + return c.Status(400).SendString(err.Error()) + } + + // Get the username from the token and set it as the owner of the project + // This is ugly but + claims := user.Claims.(jwt.MapClaims) + p.Owner = claims["name"].(string) + + if err := gs.Db.AddProject(p.Name, p.Description, p.Owner); err != nil { + return c.Status(500).SendString(err.Error()) + } + + return c.Status(200).SendString("Project added") +} diff --git a/backend/internal/types/project.go b/backend/internal/types/project.go new file mode 100644 index 0000000..cabf6c6 --- /dev/null +++ b/backend/internal/types/project.go @@ -0,0 +1,21 @@ +package types + +import ( + "time" +) + +// Project is a struct that holds the information about a project +type Project struct { + ID int `json:"id" db:"id"` + Name string `json:"name" db:"name"` + Description string `json:"description" db:"description"` + Owner string `json:"owner" db:"owner"` + Created time.Time `json:"created" db:"created"` +} + +// As it arrives from the client +type NewProject struct { + Name string `json:"name"` + Description string `json:"description"` + Owner string `json:"owner"` +} diff --git a/backend/internal/types/users.go b/backend/internal/types/users.go index fa735d7..233ec71 100644 --- a/backend/internal/types/users.go +++ b/backend/internal/types/users.go @@ -16,6 +16,11 @@ func (u *User) ToPublicUser() (*PublicUser, error) { }, nil } +type NewUser struct { + Username string `json:"username"` + Password string `json:"password"` +} + // PublicUser represents a user that is safe to send over the API (no password) type PublicUser struct { UserId string `json:"userId"` diff --git a/backend/cmd/main.go b/backend/main.go similarity index 67% rename from backend/cmd/main.go rename to backend/main.go index bae7a83..1aaca45 100644 --- a/backend/cmd/main.go +++ b/backend/main.go @@ -1,18 +1,34 @@ package main import ( - "encoding/json" "fmt" + "os" + _ "ttime/docs" "ttime/internal/config" "ttime/internal/database" "ttime/internal/handlers" + "github.com/BurntSushi/toml" "github.com/gofiber/fiber/v2" + "github.com/gofiber/swagger" _ "github.com/mattn/go-sqlite3" jwtware "github.com/gofiber/contrib/jwt" ) +// @title TTime API +// @version 0.0.1 +// @description This is the API for TTime, a time tracking application. + +// @license.name AGPL +// @license.url https://www.gnu.org/licenses/agpl-3.0.html + +// @host localhost:8080 +// @BasePath /api + +// @externalDocs.description OpenAPI +// @externalDocs.url https://swagger.io/resources/open-api/ + func main() { conf, err := config.ReadConfigFromFile("config.toml") if err != nil { @@ -20,9 +36,11 @@ func main() { _ = conf.WriteConfigToFile("config.toml") } - // Pretty print the current config - str, _ := json.MarshalIndent(conf, "", " ") - fmt.Println(string(str)) + // Pretty print the current config with toml + _ = toml.NewEncoder(os.Stdout).Encode(conf) + + fmt.Printf("Starting server on http://localhost:%d\n", conf.Port) + fmt.Printf("For documentation, go to http://localhost:%d/swagger/index.html\n", conf.Port) // Connect to the database db := database.DbConnect(conf.DbPath) @@ -31,6 +49,8 @@ func main() { // Create the server server := fiber.New() + server.Get("/swagger/*", swagger.HandlerDefault) + // Mount our static files (Beware of the security implications of this!) // This will likely be replaced by an embedded filesystem in the future server.Static("/", "./static") diff --git a/frontend/src/Pages/AdminPages/AdminMenuPage.tsx b/frontend/src/Pages/AdminPages/AdminMenuPage.tsx new file mode 100644 index 0000000..1b32ed4 --- /dev/null +++ b/frontend/src/Pages/AdminPages/AdminMenuPage.tsx @@ -0,0 +1,27 @@ +import { Link } from "react-router-dom"; +import BasicWindow from "../../Components/BasicWindow"; + +function AdminMenuPage(): JSX.Element { + const content = ( + <> +

Administrator Menu

+
+ +

+ Manage Users +

+ + +

+ Manage Projects +

+ +
+ + ); + + const buttons = <>; + + return ; +} +export default AdminMenuPage; diff --git a/frontend/src/Pages/Home.tsx b/frontend/src/Pages/Home.tsx deleted file mode 100644 index 7ce73a6..0000000 --- a/frontend/src/Pages/Home.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import reactLogo from "../assets/react.svg"; -import viteLogo from "/vite.svg"; -import "../index.css"; -import { CountButton } from "../Components/CountButton"; -import { Link } from "react-router-dom"; - -/** - * The home page of the application - * @returns {JSX.Element} The home page - */ -export default function HomePage(): JSX.Element { - return ( - <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- - To Settings -
-

- Click on the Vite and React logos to learn more -

- - ); -} diff --git a/frontend/src/Pages/Settings.tsx b/frontend/src/Pages/Settings.tsx deleted file mode 100644 index b5bf81c..0000000 --- a/frontend/src/Pages/Settings.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import "../index.css"; -import { Link } from "react-router-dom"; - -/** - * The settings page of the application - * @returns {JSX.Element} The settings page - */ -export default function SettingsPage(): JSX.Element { - return ( - <> -

Very Fancy Settings Page

-
- To Home -
- - ); -} diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index d5616c7..03091a2 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -5,6 +5,7 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom"; import LoginPage from "./Pages/LoginPage.tsx"; import YourProjectsPage from "./Pages/YourProjectsPage.tsx"; import UserProjectPage from "./Pages/UserPages/UserProjectPage.tsx"; +import AdminMenuPage from "./Pages/AdminPages/AdminMenuPage.tsx"; // This is where the routes are mounted const router = createBrowserRouter([ @@ -20,6 +21,10 @@ const router = createBrowserRouter([ path: "/project", element: , }, + { + path: "/admin-menu", + element: , + }, ]); // Semi-hacky way to get the root element diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..a58340b --- /dev/null +++ b/go.work.sum @@ -0,0 +1,15 @@ +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=