diff --git a/.gitignore b/.gitignore index 2d89407..bdbfff8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,13 @@ *.dylib bin +database.txt +plantuml.jar db.sqlite3 -*.png +diagram.puml +backend/*.png +backend/*.jpg +backend/*.svg # Test binary, built with `go test -c` *.test diff --git a/README.md b/README.md index d75861a..062c54f 100644 --- a/README.md +++ b/README.md @@ -23,12 +23,16 @@ Dependencies: - [Podman](https://podman.io/) / [Docker](https://www.docker.com/) - [Just](https://github.com/casey/just) (Optional) +### Fedora/Red Hat + If you're on [Fedora](https://fedoraproject.org/)/Red Hat derivatives, this is as simple as: ```bash sudo dnf install -y make golang nodejs podman just ``` +### Debian/Ubuntu + Any [Debian](https://www.debian.org/)/[Ubuntu](https://ubuntu.com/desktop)-based distro: ```bash @@ -36,19 +40,43 @@ sudo apt install -y make golang nodejs podman sudo apt install -y just # For Ubuntu ``` +### Arch Linux + [Arch Linux](https://archlinux.org/) & derivatives: ```bash sudo pacman -S make go nodejs npm podman just ``` +### MacOS + [MacOS](https://www.apple.com/macos/): (Requires [Homebrew](https://brew.sh/)) (Untested) ```bash brew install make go nodejs npm podman just ``` -[Windows](https://www.microsoft.com/en-us/windows): +### Windows + +#### Vanilla Windows + +The project should now build properly on Windows, given the dependencies: + +- [Go](https://go.dev/) +- [Node & npm](https://nodejs.org/en/) + +With chocolatey, you can install these dependencies with the following commands: + +```powershell +choco install -y golang nodejs +``` + +Note that none of the convenience tools (Make, Podman, Just*) are available on Windows. + +*Just is available, but the targets are written for a Unix-like environment. + +#### Windows Subsystem for Linux (WSL) + Unfortunately, [Windows Subsystem for Linux (WSL)](https://docs.microsoft.com/en-us/windows/wsl/install) is required for the build process. Running any form of containerized workload on windows is currently unsupported. More info [here](https://podman.io/docs/installation#windows). From my understanding, WSL also requires virtualization extensions to be enabled in the BIOS, which is not always the case for all users. It is possible to run the code on Windows, but this will be without the use of containers or any other build tools that are not available on Windows. @@ -62,12 +90,19 @@ You should consult the [WSL documentation](https://docs.microsoft.com/en-us/wind 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. +After this, you can open a (wsl) terminal and run the commands: -If you're **still dead set** on using a vanilla Windows environment, you will need the following: +```bash +sudo apt update && sudo apt upgrade +sudo apt install -y make podman -- [Go](https://go.dev/) -- [Node & npm](https://nodejs.org/en/) -- [MariaDB](https://mariadb.org/) / [MySQL](https://www.mysql.com/) / [PostgreSQL](https://www.postgresql.org/) (This is undecided so far) +sudo add-apt-repository ppa:longsleep/golang-backports +sudo apt update +sudo apt install golang-go -With some grit and determination, you can get it to work. It's not recommended, but I (Imbus) will try to help you. +# For a recent version of node: +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash +nvm install node +``` + +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. diff --git a/backend/Makefile b/backend/Makefile index d005846..9cfa335 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -27,6 +27,10 @@ clean: $(GOCLEAN) rm -rf bin rm -f db.sqlite3 + rm -f diagram* + rm -f plantuml.jar + rm -f erd.png + rm -f config.toml # Test target test: db.sqlite3 @@ -54,6 +58,9 @@ migrate: db.sqlite3: make migrate +dbdump: + sqlite3 $(DB_FILE) .dump > database.txt + backup: mkdir -p backups sqlite3 $(DB_FILE) .dump | gzip -9 > ./backups/BACKUP_$(DB_FILE)_$(shell date +"%Y-%m-%d_%H:%M:%S").sql.gz @@ -95,6 +102,18 @@ 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 +# Fetches the latest plantuml.jar and checks its SHA256 hash +plantuml.jar: + curl -sSfL https://github.com/plantuml/plantuml/releases/download/v1.2024.3/plantuml.jar -o plantuml.jar \ + && echo "519a4a7284c6a0357c369e4bb0caf72c4bfbbde851b8c6d6bbdb7af3c01fc82f plantuml.jar" | sha256sum -c + +# Generate UML diagrams diagral.png & diagram.svg +.PHONY: uml +uml: plantuml.jar + goplantuml -recursive . > diagram.puml + java -jar plantuml.jar -tpng diagram.puml + java -jar plantuml.jar -tsvg diagram.puml + # Convenience target to install just (requires sudo privileges) install-just: @echo "Installing just" diff --git a/backend/go.mod b/backend/go.mod index c251e95..98c606d 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -4,34 +4,33 @@ go 1.21.1 require ( github.com/BurntSushi/toml v1.3.2 + github.com/gofiber/swagger v1.0.0 github.com/jmoiron/sqlx v1.3.5 - github.com/mattn/go-sqlite3 v1.14.22 + github.com/swaggo/swag v1.16.3 + modernc.org/sqlite v1.29.5 ) 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/dustin/go-humanize v1.0.1 // 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/hashicorp/golang-lru/v2 v2.0.7 // 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/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // 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 + modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect + modernc.org/libc v1.41.0 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.7.2 // indirect + modernc.org/strutil v1.2.0 // indirect + modernc.org/token v1.1.0 // indirect ) require ( diff --git a/backend/go.sum b/backend/go.sum index a2fdf1e..3c07b9a 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -4,17 +4,12 @@ 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/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= @@ -27,27 +22,27 @@ github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfC 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= github.com/gofiber/contrib/jwt v1.0.8/go.mod h1:gWWBtBiLmKXRN7xy6a96QO0KGvPEyxdh8x496Ujtg84= -github.com/gofiber/fiber/v2 v2.52.1 h1:1RoU2NS+b98o1L77sdl5mboGPiW+0Ypsi5oLmcYlgHI= -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/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= 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/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 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/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 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= @@ -63,47 +58,55 @@ 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/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 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/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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= -github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0= 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/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= +golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= +modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk= +modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= +modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= +modernc.org/sqlite v1.29.5 h1:8l/SQKAjDtZFo9lkJLdk8g9JEOeYRG4/ghStDCCTiTE= +modernc.org/sqlite v1.29.5/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index 5a88873..b5e1981 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -5,27 +5,30 @@ import ( "os" "path/filepath" "time" + "ttime/internal/types" "github.com/jmoiron/sqlx" - _ "github.com/mattn/go-sqlite3" + _ "modernc.org/sqlite" ) // 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 + GetProjectId(projectname string) (int, error) + AddTimeReport(projectName string, userName string, start time.Time, end time.Time) error + AddUserToProject(username string, projectname string, role string) error + ChangeUserRole(username string, projectname string, role string) error + GetAllUsersProject(projectname string) ([]UserProjectMember, error) + GetAllUsersApplication() ([]string, error) + GetProjectsForUser(username string) ([]types.Project, error) + GetAllProjects() ([]types.Project, error) + GetUserRole(username string, projectname string) (string, error) } // This struct is a wrapper type that holds the database connection @@ -34,20 +37,44 @@ type Db struct { *sqlx.DB } +type UserProjectMember struct { + Username string `db:"username"` + UserRole string `db:"p_role"` +} + //go:embed migrations var scripts embed.FS +// TODO: Possibly break these out into separate files bundled with the embed package? const userInsert = "INSERT INTO users (username, password) VALUES (?, ?)" 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 = "" +const addTimeReport = `WITH UserLookup AS (SELECT id FROM users WHERE username = ?), + ProjectLookup AS (SELECT id FROM projects WHERE name = ?) + INSERT INTO time_reports (project_id, user_id, start, end) + VALUES ((SELECT id FROM ProjectLookup), (SELECT id FROM UserLookup), ?, ?);` +const addUserToProject = "INSERT INTO user_roles (user_id, project_id, p_role) VALUES (?, ?, ?)" // WIP +const changeUserRole = "UPDATE user_roles SET p_role = ? WHERE user_id = ? AND project_id = ?" + +const getProjectsForUser = ` +SELECT + projects.id, + projects.name, + projects.description, + projects.owner_user_id +FROM + projects +JOIN + user_roles ON projects.id = user_roles.project_id +JOIN + users ON user_roles.user_id = users.id +WHERE + users.username = ?;` // DbConnect connects to the database func DbConnect(dbpath string) Database { // Open the database - db, err := sqlx.Connect("sqlite3", dbpath) + db, err := sqlx.Connect("sqlite", dbpath) if err != nil { panic(err) } @@ -61,8 +88,20 @@ 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) +func (d *Db) GetProjectsForUser(username string) ([]types.Project, error) { + var projects []types.Project + err := d.Select(&projects, getProjectsForUser, username) + return projects, err +} + +func (d *Db) GetAllProjects() ([]types.Project, error) { + var projects []types.Project + err := d.Select(&projects, "SELECT * FROM projects") + return projects, err +} + +func (d *Db) AddTimeReport(projectName string, userName string, start time.Time, end time.Time) error { // WIP + _, err := d.Exec(addTimeReport, userName, projectName, start, end) return err } @@ -79,13 +118,32 @@ func (d *Db) AddUserToProject(username string, projectname string, role string) panic(err2) } - _, err3 := d.Exec(addUserToProject, projectid, userid, role) + _, err3 := d.Exec(addUserToProject, userid, projectid, role) return err3 } -// func (d *Db) ChangeUserRole(username string, projectname string, role string) error { +func (d *Db) ChangeUserRole(username string, projectname string, role string) error { + 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(changeUserRole, role, userid, projectid) + return err3 +} + +func (d *Db) GetUserRole(username string, projectname string) (string, error) { + var role string + err := d.Get(&role, "SELECT p_role FROM user_roles WHERE user_id = (SELECT id FROM users WHERE username = ?) AND project_id = (SELECT id FROM projects WHERE name = ?)", username, projectname) + return role, err +} // AddUser adds a user to the database func (d *Db) AddUser(username string, password string) error { @@ -110,9 +168,9 @@ func (d *Db) GetUserId(username string) (int, error) { return id, err } -func (d *Db) GetProjectId(projectname string) (int, error) { // WIP, denna kan vara goof +func (d *Db) GetProjectId(projectname string) (int, error) { var id int - err := d.Get(&id, "SELECT id FROM project WHERE project_name = ?", projectname) + err := d.Get(&id, "SELECT id FROM projects WHERE name = ?", projectname) return id, err } @@ -122,6 +180,69 @@ func (d *Db) AddProject(name string, description string, username string) error return err } +func (d *Db) GetAllUsersProject(projectname string) ([]UserProjectMember, error) { + // Define the SQL query to fetch users and their roles for a given project + query := ` + SELECT u.username, ur.p_role + FROM users u + INNER JOIN user_roles ur ON u.id = ur.user_id + INNER JOIN projects p ON ur.project_id = p.id + WHERE p.name = ? + ` + + // Execute the query + rows, err := d.Queryx(query, projectname) + if err != nil { + return nil, err + } + defer rows.Close() + + // Iterate over the rows and populate the result slice + var users []UserProjectMember + for rows.Next() { + var user UserProjectMember + if err := rows.StructScan(&user); err != nil { + return nil, err + } + users = append(users, user) + } + if err := rows.Err(); err != nil { + return nil, err + } + + return users, nil +} + +// GetAllUsersApplication retrieves all usernames from the database +func (d *Db) GetAllUsersApplication() ([]string, error) { + // Define the SQL query to fetch all usernames + query := ` + SELECT username FROM users + ` + + // Execute the query + rows, err := d.Queryx(query) + if err != nil { + return nil, err + } + defer rows.Close() + + // Iterate over the rows and populate the result slice + var usernames []string + for rows.Next() { + var username string + if err := rows.Scan(&username); err != nil { + return nil, err + } + usernames = append(usernames, username) + } + if err := rows.Err(); err != nil { + return nil, err + } + + return usernames, nil +} + // Reads a directory of migration files and applies them to the database. // This will eventually be used on an embedded directory func (d *Db) Migrate(dirname string) error { diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index 96eb9b7..7650739 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -2,6 +2,7 @@ package database import ( "testing" + "time" ) // Tests are not guaranteed to be sequential @@ -92,14 +93,253 @@ func TestPromoteToAdmin(t *testing.T) { } } -// func TestAddTimeReport(t *testing.T) { +func TestAddTimeReport(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } -// } + err = db.AddUser("testuser", "password") + if err != nil { + t.Error("AddUser failed:", err) + } -// func TestAddUserToProject(t *testing.T) { + err = db.AddProject("testproject", "description", "testuser") + if err != nil { + t.Error("AddProject failed:", err) + } -// } + var now = time.Now() + var then = now.Add(time.Hour) -// func TestChangeUserRole(t *testing.T) { + err = db.AddTimeReport("testproject", "testuser", now, then) + if err != nil { + t.Error("AddTimeReport failed:", err) + } +} -// } +func TestAddUserToProject(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + err = db.AddUser("testuser", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + err = db.AddProject("testproject", "description", "testuser") + if err != nil { + t.Error("AddProject failed:", err) + } + + var now = time.Now() + var then = now.Add(time.Hour) + + err = db.AddTimeReport("testproject", "testuser", now, then) + if err != nil { + t.Error("AddTimeReport failed:", err) + } + + err = db.AddUserToProject("testuser", "testproject", "user") + if err != nil { + t.Error("AddUserToProject failed:", err) + } +} + +func TestChangeUserRole(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + err = db.AddUser("testuser", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + err = db.AddProject("testproject", "description", "testuser") + if err != nil { + t.Error("AddProject failed:", err) + } + + err = db.AddUserToProject("testuser", "testproject", "user") + if err != nil { + t.Error("AddUserToProject failed:", err) + } + + role, err := db.GetUserRole("testuser", "testproject") + if err != nil { + t.Error("GetUserRole failed:", err) + } + if role != "user" { + t.Error("GetUserRole failed: expected user, got", role) + } + + err = db.ChangeUserRole("testuser", "testproject", "admin") + if err != nil { + t.Error("ChangeUserRole failed:", err) + } + + role, err = db.GetUserRole("testuser", "testproject") + if err != nil { + t.Error("GetUserRole failed:", err) + } + if role != "admin" { + t.Error("GetUserRole failed: expected admin, got", role) + } + +} + +func TestGetAllUsersProject(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + err = db.AddUser("testuser1", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + err = db.AddUser("testuser2", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + err = db.AddProject("testproject", "description", "testuser1") + if err != nil { + t.Error("AddProject failed:", err) + } + + err = db.AddUserToProject("testuser1", "testproject", "project_manager") + if err != nil { + t.Error("AddUserToProject failed:", err) + } + + err = db.AddUserToProject("testuser2", "testproject", "user") + if err != nil { + t.Error("AddUserToProject failed:", err) + } + + users, err := db.GetAllUsersProject("testproject") + if err != nil { + t.Error("GetAllUsersProject failed:", err) + } + + // Check if both users are returned with their roles + if len(users) != 2 { + t.Errorf("Expected 2 users, got %d", len(users)) + } + + // Check if testuser1 has project manager role + foundProjectManager := false + for _, user := range users { + if user.Username == "testuser1" && user.UserRole == "project_manager" { + foundProjectManager = true + break + } + } + if !foundProjectManager { + t.Error("Project Manager user not found") + } + + // Check if testuser2 has user role + foundUser := false + for _, user := range users { + if user.Username == "testuser2" && user.UserRole == "user" { + foundUser = true + break + } + } + if !foundUser { + t.Error("User user not found") + } +} + +func TestGetAllUsersApplication(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + err = db.AddUser("testuser1", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + err = db.AddUser("testuser2", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + users, err := db.GetAllUsersApplication() + if err != nil { + t.Error("GetAllUsersApplication failed:", err) + } + + // Check if both users are returned + if len(users) != 2 { + t.Errorf("Expected 2 users, got %d", len(users)) + } + + // Check if the test users are included in the list + foundTestUser1 := false + foundTestUser2 := false + for _, user := range users { + if user == "testuser1" { + foundTestUser1 = true + } + if user == "testuser2" { + foundTestUser2 = true + } + } + + if !foundTestUser1 { + t.Error("testuser1 not found") + } + if !foundTestUser2 { + t.Error("testuser2 not found") + } +} + +func TestGetProjectsForUser(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + err = db.AddUser("testuser", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + err = db.AddProject("testproject", "description", "testuser") + if err != nil { + t.Error("AddProject failed:", err) + } + + err = db.AddUserToProject("testuser", "testproject", "user") + if err != nil { + t.Error("AddUserToProject failed:", err) + } + + projects1, err := db.GetAllProjects() + if err != nil { + t.Error("GetAllProjects failed:", err) + } + + if len(projects1) != 1 { + t.Error("GetAllProjects failed: expected 1, got", len(projects1)) + } + + projects, err := db.GetProjectsForUser("testuser") + if err != nil { + t.Error("GetProjectsForUser failed:", err) + } + + if len(projects) != 1 { + t.Error("GetProjectsForUser failed: expected 1, got", len(projects)) + } +} diff --git a/backend/internal/database/migrations/0020_projects.sql b/backend/internal/database/migrations/0020_projects.sql index adfb818..58d8e97 100644 --- a/backend/internal/database/migrations/0020_projects.sql +++ b/backend/internal/database/migrations/0020_projects.sql @@ -1,11 +1,9 @@ CREATE TABLE IF NOT EXISTS projects ( id INTEGER PRIMARY KEY, - projectId TEXT DEFAULT (HEX(RANDOMBLOB(4))) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL UNIQUE, description TEXT NOT NULL, 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 (owner_user_id); \ No newline at end of file diff --git a/backend/internal/database/migrations/0030_time_reports.sql b/backend/internal/database/migrations/0030_time_reports.sql index e8f3ec1..76812a1 100644 --- a/backend/internal/database/migrations/0030_time_reports.sql +++ b/backend/internal/database/migrations/0030_time_reports.sql @@ -1,10 +1,11 @@ CREATE TABLE IF NOT EXISTS time_reports ( id INTEGER PRIMARY KEY, - reportId TEXT DEFAULT (HEX(RANDOMBLOB(6))) NOT NULL UNIQUE, project_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, start DATETIME NOT NULL, end DATETIME NOT NULL, FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ); CREATE TRIGGER IF NOT EXISTS time_reports_start_before_end diff --git a/backend/internal/database/migrations/0049_project_role.sql b/backend/internal/database/migrations/0049_project_role.sql index 8716800..f7e7151 100644 --- a/backend/internal/database/migrations/0049_project_role.sql +++ b/backend/internal/database/migrations/0049_project_role.sql @@ -5,5 +5,5 @@ CREATE TABLE IF NOT EXISTS project_role ( ); -- 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 ('project_manager'); 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 aad25f7..d3e614d 100644 --- a/backend/internal/database/migrations/0050_user_roles.sql +++ b/backend/internal/database/migrations/0050_user_roles.sql @@ -1,7 +1,7 @@ CREATE TABLE IF NOT EXISTS user_roles ( user_id INTEGER NOT NULL, project_id INTEGER NOT NULL, - p_role TEXT NOT NULL, -- 'admin' or 'member' + p_role TEXT NOT NULL, -- 'project_manager' 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) diff --git a/backend/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go index 9c42133..fea0dfd 100644 --- a/backend/internal/handlers/global_state.go +++ b/backend/internal/handlers/global_state.go @@ -11,12 +11,12 @@ 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 - // GetProjects(c *fiber.Ctx) error // To get all projects + 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 + GetUserProjects(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 // DeleteProject(c *fiber.Ctx) error // To delete a project @@ -33,6 +33,9 @@ type GlobalState interface { // SignCollection(c *fiber.Ctx) error // To sign a collection GetButtonCount(c *fiber.Ctx) error // For demonstration purposes IncrementButtonCount(c *fiber.Ctx) error // For demonstration purposes + ListAllUsers(c *fiber.Ctx) error // To get a list of all users in the application database + ListAllUsersProject(c *fiber.Ctx) error // To get a list of all users for a specific project + ProjectRoleChange(c *fiber.Ctx) error // To change a users role in a project } // "Constructor" @@ -163,3 +166,62 @@ func (gs *GState) CreateProject(c *fiber.Ctx) error { return c.Status(200).SendString("Project added") } + +// GetUserProjects returns all projects that the user is a member of +func (gs *GState) GetUserProjects(c *fiber.Ctx) error { + // First we get the username from the token + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + // Then dip into the database to get the projects + projects, err := gs.Db.GetProjectsForUser(username) + if err != nil { + return c.Status(500).SendString(err.Error()) + } + + // Return a json serialized list of projects + return c.JSON(projects) +} + +// ListAllUsers is a handler that returns a list of all users in the application database +func (gs *GState) ListAllUsers(c *fiber.Ctx) error { + // Get all users from the database + users, err := gs.Db.GetAllUsersApplication() + if err != nil { + return c.Status(500).SendString(err.Error()) + } + + // Return the list of users as JSON + return c.JSON(users) +} + +func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error { + // Extract the project name from the request parameters or body + projectName := c.Params("projectName") + + // Get all users associated with the project from the database + users, err := gs.Db.GetAllUsersProject(projectName) + if err != nil { + return c.Status(500).SendString(err.Error()) + } + + // Return the list of users as JSON + return c.JSON(users) +} + +// ProjectRoleChange is a handler that changes a user's role within a project +func (gs *GState) ProjectRoleChange(c *fiber.Ctx) error { + // Extract the necessary parameters from the request + username := c.Params("username") + projectName := c.Params("projectName") + role := c.Params("role") + + // Change the user's role within the project in the database + if err := gs.Db.ChangeUserRole(username, projectName, role); err != nil { + return c.Status(500).SendString(err.Error()) + } + + // Return a success message + return c.SendStatus(fiber.StatusOK) +} diff --git a/backend/internal/types/project.go b/backend/internal/types/project.go index cabf6c6..8fcfaf5 100644 --- a/backend/internal/types/project.go +++ b/backend/internal/types/project.go @@ -1,16 +1,11 @@ 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"` + ID int `json:"id" db:"id"` + Name string `json:"name" db:"name"` + Description string `json:"description" db:"description"` + Owner string `json:"owner" db:"owner_user_id"` } // As it arrives from the client diff --git a/backend/main.go b/backend/main.go index 1aaca45..4e0935c 100644 --- a/backend/main.go +++ b/backend/main.go @@ -11,7 +11,6 @@ import ( "github.com/BurntSushi/toml" "github.com/gofiber/fiber/v2" "github.com/gofiber/swagger" - _ "github.com/mattn/go-sqlite3" jwtware "github.com/gofiber/contrib/jwt" ) @@ -69,8 +68,10 @@ func main() { SigningKey: jwtware.SigningKey{Key: []byte("secret")}, })) + server.Get("/api/getUserProjects", gs.GetUserProjects) server.Post("/api/loginrenew", gs.LoginRenew) server.Delete("/api/userdelete", gs.UserDelete) // Perhaps just use POST to avoid headaches + server.Post("/api/project", gs.CreateProject) // Announce the port we are listening on and start the server err = server.Listen(fmt.Sprintf(":%d", conf.Port)) diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index 2dbd51e..f33c87c 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -1,3 +1,4 @@ +import { NewProject, Project } from "../Types/Project"; import { NewUser, User } from "../Types/Users"; // Defines all the methods that an instance of the API must implement @@ -6,6 +7,10 @@ interface API { registerUser(user: NewUser): Promise; /** Remove a user */ removeUser(username: string): Promise; + /** Create a project */ + createProject(project: NewProject): Promise; + /** Renew the token */ + renewToken(token: string): Promise; } // Export an instance of the API @@ -29,4 +34,24 @@ export const api: API = { body: JSON.stringify(username), }).then((res) => res.json() as Promise); }, + + async createProject(project: NewProject): Promise { + return fetch("/api/project", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(project), + }).then((res) => res.json() as Promise); + }, + + async renewToken(token: string): Promise { + return fetch("/api/loginrenew", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }).then((res) => res.json() as Promise); + }, }; diff --git a/frontend/src/Components/Register.tsx b/frontend/src/Components/Register.tsx new file mode 100644 index 0000000..d0e3da6 --- /dev/null +++ b/frontend/src/Components/Register.tsx @@ -0,0 +1,74 @@ +import { useState } from "react"; +import { NewUser } from "../Types/Users"; +import { api } from "../API/API"; + +export default function Register(): JSX.Element { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + + const handleRegister = async (): Promise => { + const newUser: NewUser = { userName: username, password }; + await api.registerUser(newUser); // TODO: Handle errors + }; + + return ( +
+
+
{ + e.preventDefault(); + void handleRegister(); + }} + > +

Register new user

+
+ + { + setUsername(e.target.value); + }} + /> +
+
+ + { + setPassword(e.target.value); + }} + /> +
+
+ +
+
+

+
+
+ ); +} diff --git a/frontend/src/Pages/LoginPage.tsx b/frontend/src/Pages/LoginPage.tsx index 9b4290d..e6d14bc 100644 --- a/frontend/src/Pages/LoginPage.tsx +++ b/frontend/src/Pages/LoginPage.tsx @@ -29,15 +29,15 @@ const PreloadBackgroundAnimation = (): JSX.Element => { function LoginPage(): JSX.Element { //Example users for testing without backend, remove when using backend const admin: NewUser = { - name: "admin", + userName: "admin", password: "123", }; const pmanager: NewUser = { - name: "pmanager", + userName: "pmanager", password: "123", }; const user: NewUser = { - name: "user", + userName: "user", password: "123", }; @@ -48,11 +48,14 @@ function LoginPage(): JSX.Element { function handleSubmit(event: FormEvent): void { event.preventDefault(); //TODO: Compare with db instead when finished - if (username === admin.name && password === admin.password) { + if (username === admin.userName && password === admin.password) { navigate("/admin-menu"); - } else if (username === pmanager.name && password === pmanager.password) { + } else if ( + username === pmanager.userName && + password === pmanager.password + ) { navigate("/PM-project-page"); - } else if (username === user.name && password === user.password) { + } else if (username === user.userName && password === user.password) { navigate("/your-projects"); } } diff --git a/frontend/src/Types/Project.ts b/frontend/src/Types/Project.ts new file mode 100644 index 0000000..bb4f8c7 --- /dev/null +++ b/frontend/src/Types/Project.ts @@ -0,0 +1,13 @@ +export interface Project { + id: number; + name: string; + description: string; + owner: string; + created: string; // This is a date +} + +export interface NewProject { + name: string; + description: string; + owner: string; +} diff --git a/frontend/src/Types/Users.ts b/frontend/src/Types/Users.ts index 2a195b2..8323b2e 100644 --- a/frontend/src/Types/Users.ts +++ b/frontend/src/Types/Users.ts @@ -1,11 +1,11 @@ // This is how the API responds export interface User { id: number; - name: string; + userName: string; } // Used to create a new user export interface NewUser { - name: string; + userName: string; password: string; } diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 40f6c8f..29f168e 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 Register from "./Components/Register.tsx"; import AdminMenuPage from "./Pages/AdminPages/AdminMenuPage.tsx"; import UserEditTimeReportPage from "./Pages/UserPages/UserEditTimeReportPage.tsx"; import UserNewTimeReportPage from "./Pages/UserPages/UserNewTimeReportPage.tsx"; @@ -52,6 +53,14 @@ const router = createBrowserRouter([ path: "/project", element: , }, + { + path: "/register", + element: , + }, + { + path: "/admin-menu", + element: , + }, { path: "/project-page", element: , diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5b28e18 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "TTime", + "lockfileVersion": 3, + "requires": true, + "packages": {} +}