Compare commits

..

1 commit

Author SHA1 Message Date
Emily Ha
ee96386a2f add martin to database for test and lint txt report 2024-03-20 17:25:38 +01:00
153 changed files with 2836 additions and 9711 deletions

6
.gitignore vendored
View file

@ -9,16 +9,10 @@ bin
database.txt database.txt
plantuml.jar plantuml.jar
db.sqlite3 db.sqlite3
db.sqlite3-journal
diagram.puml diagram.puml
backend/*.png backend/*.png
backend/*.jpg backend/*.jpg
backend/*.svg backend/*.svg
__pycache__
/go.work.sum
/package-lock.json
/backend/docs/swagger.json
# Test binary, built with `go test -c` # Test binary, built with `go test -c`
*.test *.test

View file

@ -15,7 +15,7 @@ remove-podman-containers:
# Saves the release container to a tarball, pigz is just gzip but multithreaded # Saves the release container to a tarball, pigz is just gzip but multithreaded
save-release: build-container-release save-release: build-container-release
podman save --format=oci-archive ttime-server | pigz -9 > ttime-server_`date -I`_`git rev-parse --short HEAD`.tar.gz podman save --format=oci-archive ttime-server | pigz -9 > ttime-server.tar.gz
# Loads the release container from a tarball # Loads the release container from a tarball
load-release file: load-release file:
@ -23,13 +23,10 @@ load-release file:
# Tests every part of the project # Tests every part of the project
testall: testall:
cd frontend && npm install
cd frontend && npm test cd frontend && npm test
cd frontend && npm run lint cd frontend && npm run lint
cd frontend && npm run build
cd backend && make test cd backend && make test
cd backend && make lint cd backend && make lint
cd backend && make itest
# Cleans up everything related to the project # Cleans up everything related to the project
clean: remove-podman-containers clean: remove-podman-containers

View file

@ -13,13 +13,10 @@ remove-podman-containers:
# Tests every part of the project # Tests every part of the project
testall: testall:
cd frontend && npm install
cd frontend && npm test cd frontend && npm test
cd frontend && npm run lint cd frontend && npm run lint
cd frontend && npm run build
cd backend && make test cd backend && make test
cd backend && make lint cd backend && make lint
cd backend && make itest
# Cleans up everything related to the project # Cleans up everything related to the project
clean: remove-podman-containers clean: remove-podman-containers

View file

@ -8,19 +8,17 @@ GOGET = $(GOCMD) get
# SQLite database filename # SQLite database filename
DB_FILE = db.sqlite3 DB_FILE = db.sqlite3
PROC_NAME = ttime_server
# Directory containing migration SQL scripts # Directory containing migration SQL scripts
MIGRATIONS_DIR = internal/database/migrations MIGRATIONS_DIR = internal/database/migrations
SAMPLE_DATA_DIR = internal/database/sample_data SAMPLE_DATA_DIR = internal/database/sample_data
# Build target # Build target
build: build:
$(GOBUILD) -o bin/$(PROC_NAME) main.go $(GOBUILD) -o bin/server main.go
# Run target # Run target
run: build run: build
./bin/$(PROC_NAME) ./bin/server
watch: build watch: build
watchexec -c -w . -r make run watchexec -c -w . -r make run
@ -34,22 +32,11 @@ clean:
rm -f plantuml.jar rm -f plantuml.jar
rm -f erd.png rm -f erd.png
rm -f config.toml rm -f config.toml
rm -f database.txt
# Test target # Test target
test: db.sqlite3 test: db.sqlite3
$(GOTEST) ./... -count=1 $(GOTEST) ./... -count=1
# Integration test target
.PHONY: itest
itest:
pgrep $(PROC_NAME) && echo "Server already running" && exit 1 || true
make build
./bin/$(PROC_NAME) >/dev/null 2>&1 &
sleep 1 # Adjust if needed
python ../testing/testing.py || pkill $(PROC_NAME)
pkill $(PROC_NAME)
# Get dependencies target # Get dependencies target
deps: deps:
$(GOGET) -v ./... $(GOGET) -v ./...
@ -105,17 +92,6 @@ default: build
docs: docs:
swag init -outputTypes go swag init -outputTypes go
api: ./docs/swagger.json
rm ../frontend/src/API/GenApi.ts
npx swagger-typescript-api \
--api-class-name GenApi \
--path ./docs/swagger.json \
--output ../frontend/src/API \
--name GenApi.ts \
./docs/swagger.json:
swag init -outputTypes json
.PHONY: docfmt .PHONY: docfmt
docfmt: docfmt:
swag fmt swag fmt
@ -155,11 +131,3 @@ install-just:
.PHONY: types .PHONY: types
types: types:
tygo generate tygo generate
.PHONY: install-golds
install-golds:
go install go101.org/golds@latest
.PHONY: golds
golds:
golds -port 6060 -nouses -plainsrc -wdpkgs-listing=promoted ./...

View file

@ -21,21 +21,21 @@ const docTemplate = `{
"paths": { "paths": {
"/login": { "/login": {
"post": { "post": {
"description": "Logs in a user and returns a JWT token", "description": "logs the user in and returns a jwt token",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
"produces": [ "produces": [
"application/json" "text/plain"
], ],
"tags": [ "tags": [
"Auth" "User"
], ],
"summary": "Login", "summary": "login",
"parameters": [ "parameters": [
{ {
"description": "User credentials", "description": "login info",
"name": "body", "name": "NewUser",
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
@ -45,9 +45,9 @@ const docTemplate = `{
], ],
"responses": { "responses": {
"200": { "200": {
"description": "JWT token", "description": "Successfully signed token for user",
"schema": { "schema": {
"$ref": "#/definitions/types.Token" "type": "Token"
} }
}, },
"400": { "400": {
@ -71,26 +71,29 @@ const docTemplate = `{
} }
} }
}, },
"/loginrenew": { "/loginerenew": {
"post": { "post": {
"security": [ "security": [
{ {
"JWT": [] "bererToken": []
} }
], ],
"description": "Renews the users token.", "description": "renews the users token",
"produces": [ "consumes": [
"application/json" "application/json"
], ],
"produces": [
"text/plain"
],
"tags": [ "tags": [
"Auth" "User"
], ],
"summary": "LoginRenews", "summary": "LoginRenews",
"responses": { "responses": {
"200": { "200": {
"description": "Successfully signed token for user", "description": "Successfully signed token for user",
"schema": { "schema": {
"$ref": "#/definitions/types.Token" "type": "Token"
} }
}, },
"401": { "401": {
@ -108,64 +111,9 @@ const docTemplate = `{
} }
} }
}, },
"/promote/{projectName}": {
"put": {
"security": [
{
"JWT": []
}
],
"description": "Promote a user to project manager",
"consumes": [
"text/plain"
],
"produces": [
"text/plain"
],
"tags": [
"Auth"
],
"summary": "Promote to project manager",
"parameters": [
{
"type": "string",
"description": "Project name",
"name": "projectName",
"in": "path",
"required": true
},
{
"type": "string",
"description": "User name",
"name": "userName",
"in": "query",
"required": true
}
],
"responses": {
"403": {
"description": "Forbidden",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "string"
}
}
}
}
},
"/promoteToAdmin": { "/promoteToAdmin": {
"post": { "post": {
"security": [ "description": "promote chosen user to admin",
{
"JWT": []
}
],
"description": "Promote chosen user to site admin",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -189,13 +137,13 @@ const docTemplate = `{
], ],
"responses": { "responses": {
"200": { "200": {
"description": "Successfully promoted user", "description": "Successfully prometed user",
"schema": { "schema": {
"$ref": "#/definitions/types.Token" "type": "json"
} }
}, },
"400": { "400": {
"description": "Bad request", "description": "bad request",
"schema": { "schema": {
"type": "string" "type": "string"
} }
@ -225,7 +173,7 @@ const docTemplate = `{
"text/plain" "text/plain"
], ],
"tags": [ "tags": [
"Auth" "User"
], ],
"summary": "Register", "summary": "Register",
"parameters": [ "parameters": [
@ -263,11 +211,6 @@ const docTemplate = `{
}, },
"/userdelete/{username}": { "/userdelete/{username}": {
"delete": { "delete": {
"security": [
{
"JWT": []
}
],
"description": "UserDelete deletes a user from the database", "description": "UserDelete deletes a user from the database",
"consumes": [ "consumes": [
"application/json" "application/json"
@ -309,27 +252,22 @@ const docTemplate = `{
}, },
"/users/all": { "/users/all": {
"get": { "get": {
"security": [
{
"JWT": []
}
],
"description": "lists all users", "description": "lists all users",
"produces": [ "consumes": [
"application/json" "application/json"
], ],
"produces": [
"text/plain"
],
"tags": [ "tags": [
"User" "User"
], ],
"summary": "ListsAllUsers", "summary": "ListsAllUsers",
"responses": { "responses": {
"200": { "200": {
"description": "Successfully returned all users", "description": "Successfully signed token for user",
"schema": { "schema": {
"type": "array", "type": "json"
"items": {
"type": "string"
}
} }
}, },
"401": { "401": {
@ -353,27 +291,16 @@ const docTemplate = `{
"type": "object", "type": "object",
"properties": { "properties": {
"password": { "password": {
"type": "string", "type": "string"
"example": "password123"
}, },
"username": { "username": {
"type": "string",
"example": "username123"
}
}
},
"types.Token": {
"type": "object",
"properties": {
"token": {
"type": "string" "type": "string"
} }
} }
} }
}, },
"securityDefinitions": { "securityDefinitions": {
"JWT": { "bererToken": {
"description": "Use the JWT token provided by the login endpoint to authenticate requests. **Prefix the token with \"Bearer \".**",
"type": "apiKey", "type": "apiKey",
"name": "Authorization", "name": "Authorization",
"in": "header" "in": "header"

View file

@ -13,10 +13,10 @@ require (
require ( require (
github.com/KyleBanks/depth v1.2.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonpointer v0.20.3 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.5 // indirect
github.com/go-openapi/spec v0.21.0 // indirect github.com/go-openapi/spec v0.20.15 // indirect
github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/swag v0.22.10 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
@ -25,10 +25,10 @@ require (
github.com/swaggo/files/v2 v2.0.0 // indirect github.com/swaggo/files/v2 v2.0.0 // indirect
golang.org/x/tools v0.19.0 // indirect golang.org/x/tools v0.19.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b // indirect modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.49.1 // indirect modernc.org/libc v1.41.0 // indirect
modernc.org/mathutil v1.6.0 // indirect modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect modernc.org/memory v1.7.2 // indirect
modernc.org/strutil v1.2.0 // indirect modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect modernc.org/token v1.1.0 // indirect
) )
@ -42,7 +42,7 @@ require (
// These are all for fiber // These are all for fiber
require ( require (
github.com/andybalholm/brotli v1.1.0 // indirect github.com/andybalholm/brotli v1.1.0 // indirect
github.com/gofiber/fiber/v2 v2.52.4 github.com/gofiber/fiber/v2 v2.52.2
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.17.7 // indirect github.com/klauspost/compress v1.17.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
@ -52,5 +52,5 @@ require (
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.52.0 // indirect github.com/valyala/fasthttp v1.52.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.19.0 // indirect golang.org/x/sys v0.18.0 // indirect
) )

View file

@ -10,20 +10,20 @@ 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/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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.20.3 h1:jykzYWS/kyGtsHfRt6aV8JTB9pcQAXPIA7qlZ5aRlyk=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonpointer v0.20.3/go.mod h1:c7l0rjoouAuIxCm8v/JWKRgMjDG/+/7UBWsXMrv6PsM=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= github.com/go-openapi/jsonreference v0.20.5 h1:hutI+cQI+HbSQaIGSfsBsYI0pHk+CATf8Fk5gCSj0yI=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/jsonreference v0.20.5/go.mod h1:thAqAp31UABtI+FQGKAQfmv7DbFpKNUlva2UPCxKu2Y=
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= github.com/go-openapi/spec v0.20.15 h1:8bDcVxF607pTh9NpPwgsH4J5Uhh5mV5XoWnkurdiY+U=
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= github.com/go-openapi/spec v0.20.15/go.mod h1:o0upgqg5uYFG7O5mADrDVmSG3Wa6y6OLhwiCqQ+sTv4=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.22.10 h1:4y86NVn7Z2yYd6pfS4Z+Nyh3aAUL3Nul+LMbhFKy0gA=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 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 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 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 h1:/GeOsm/Mr1OGr0GTy+RIVSz5VgNNyP3ZgK4wdqxF/WY=
github.com/gofiber/contrib/jwt v1.0.8/go.mod h1:gWWBtBiLmKXRN7xy6a96QO0KGvPEyxdh8x496Ujtg84= github.com/gofiber/contrib/jwt v1.0.8/go.mod h1:gWWBtBiLmKXRN7xy6a96QO0KGvPEyxdh8x496Ujtg84=
github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM= github.com/gofiber/fiber/v2 v2.52.2 h1:b0rYH6b06Df+4NyrbdptQL8ifuxw/Tf2DgfkZkDaxEo=
github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= 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 h1:BzUzDS9ZT6fDUa692kxmfOjc1DZiloLiPK/W5z1H1tc=
github.com/gofiber/swagger v1.0.0/go.mod h1:QrYNF1Yrc7ggGK6ATsJ6yfH/8Zi5bu9lA7wB8TmCecg= 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 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
@ -85,8 +85,8 @@ 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/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.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= 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/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -94,26 +94,16 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.20.0 h1:45Or8mQfbUqJOG9WaxvlFYOAQO0lQ5RvqBcFCXngjxk=
modernc.org/cc/v4 v4.20.0/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.15.0 h1:uwfCZOkKhaNNotgYW7kxkJwrkQC1HfGitt/7ousudJE=
modernc.org/ccgo/v4 v4.15.0/go.mod h1:XVITcYGiI+O97UNDLMsnZ9ZjJOhC+ACX+TfxpsWWyRc=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b h1:BnN1t+pb1cy61zbvSUV7SeI0PwosMhlAEi/vBY4qxp8= modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk=
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY=
modernc.org/libc v1.49.1 h1:r4UaWllkYXRPA7Mq/KzmassZBvNJiH9egF4O/KV/gdE=
modernc.org/libc v1.49.1/go.mod h1:Hx2rWfza47GSzCluTU7Vf0Qx3z9rWCVORL6RNgq+Xog=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.29.5 h1:8l/SQKAjDtZFo9lkJLdk8g9JEOeYRG4/ghStDCCTiTE= modernc.org/sqlite v1.29.5 h1:8l/SQKAjDtZFo9lkJLdk8g9JEOeYRG4/ghStDCCTiTE=
modernc.org/sqlite v1.29.5/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U= 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 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=

View file

@ -5,12 +5,8 @@ import (
"testing" "testing"
) )
// TestNewConfig tests the creation of a new configuration object
func TestNewConfig(t *testing.T) { func TestNewConfig(t *testing.T) {
// Arrange
c := NewConfig() c := NewConfig()
// Act & Assert
if c.Port != 8080 { if c.Port != 8080 {
t.Errorf("Expected port to be 8080, got %d", c.Port) t.Errorf("Expected port to be 8080, got %d", c.Port)
} }
@ -28,15 +24,9 @@ func TestNewConfig(t *testing.T) {
} }
} }
// TestWriteConfig tests the function to write the configuration to a file
func TestWriteConfig(t *testing.T) { func TestWriteConfig(t *testing.T) {
// Arrange
c := NewConfig() c := NewConfig()
//Act
err := c.WriteConfigToFile("test.toml") err := c.WriteConfigToFile("test.toml")
// Assert
if err != nil { if err != nil {
t.Errorf("Expected no error, got %s", err) t.Errorf("Expected no error, got %s", err)
} }
@ -45,23 +35,14 @@ func TestWriteConfig(t *testing.T) {
_ = os.Remove("test.toml") _ = os.Remove("test.toml")
} }
// TestReadConfig tests the function to read the configuration from a file
func TestReadConfig(t *testing.T) { func TestReadConfig(t *testing.T) {
// Arrange
c := NewConfig() c := NewConfig()
// Act
err := c.WriteConfigToFile("test.toml") err := c.WriteConfigToFile("test.toml")
// Assert
if err != nil { if err != nil {
t.Errorf("Expected no error, got %s", err) t.Errorf("Expected no error, got %s", err)
} }
// Act
c2, err := ReadConfigFromFile("test.toml") c2, err := ReadConfigFromFile("test.toml")
// Assert
if err != nil { if err != nil {
t.Errorf("Expected no error, got %s", err) t.Errorf("Expected no error, got %s", err)
} }

View file

@ -2,12 +2,10 @@ package database
import ( import (
"embed" "embed"
"encoding/json"
"errors" "errors"
"path/filepath" "path/filepath"
"ttime/internal/types" "ttime/internal/types"
"github.com/gofiber/fiber/v2/log"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
) )
@ -18,16 +16,15 @@ type Database interface {
AddUser(username string, password string) error AddUser(username string, password string) error
CheckUser(username string, password string) bool CheckUser(username string, password string) bool
RemoveUser(username string) error RemoveUser(username string) error
RemoveUserFromProject(username string, projectname string) error
PromoteToAdmin(username string) error PromoteToAdmin(username string) error
GetUserId(username string) (int, error) GetUserId(username string) (int, error)
AddProject(name string, description string, username string) error AddProject(name string, description string, username string) error
DeleteProject(name string, username string) error Migrate() error
MigrateSampleData() error
GetProjectId(projectname string) (int, error) GetProjectId(projectname string) (int, error)
AddWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error AddWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error
AddUserToProject(username string, projectname string, role string) error AddUserToProject(username string, projectname string, role string) error
ChangeUserRole(username string, projectname string, role string) error ChangeUserRole(username string, projectname string, role string) error
ChangeUserName(username string, newname string) error
GetAllUsersProject(projectname string) ([]UserProjectMember, error) GetAllUsersProject(projectname string) ([]UserProjectMember, error)
GetAllUsersApplication() ([]string, error) GetAllUsersApplication() ([]string, error)
GetProjectsForUser(username string) ([]types.Project, error) GetProjectsForUser(username string) ([]types.Project, error)
@ -35,26 +32,14 @@ type Database interface {
GetProject(projectId int) (types.Project, error) GetProject(projectId int) (types.Project, error)
GetUserRole(username string, projectname string) (string, error) GetUserRole(username string, projectname string) (string, error)
GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error) GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error)
GetAllWeeklyReports(username string, projectname string) ([]types.WeeklyReportList, error)
GetUnsignedWeeklyReports(projectName string) ([]types.WeeklyReport, error)
SignWeeklyReport(reportId int, projectManagerId int) error SignWeeklyReport(reportId int, projectManagerId int) error
IsSiteAdmin(username string) (bool, error) IsSiteAdmin(username string) (bool, error)
IsProjectManager(username string, projectname string) (bool, error)
ReportStatistics(username string, projectName string) (*types.Statistics, error)
GetProjectTimes(projectName string) (map[string]int, error)
UpdateWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error
RemoveProject(projectname string) error
GetUserName(id int) (string, error)
UnsignWeeklyReport(reportId int, projectManagerId int) error
DeleteReport(reportID int) error
ChangeProjectName(projectName string, newProjectName string) error
ChangeUserPassword(username string, password string) error
} }
// This struct is a wrapper type that holds the database connection // This struct is a wrapper type that holds the database connection
// Internally DB holds a connection pool, so it's safe for concurrent use // Internally DB holds a connection pool, so it's safe for concurrent use
type Db struct { type Db struct {
*sqlx.Tx *sqlx.DB
} }
type UserProjectMember struct { type UserProjectMember struct {
@ -70,45 +55,22 @@ var sampleData embed.FS
// TODO: Possibly break these out into separate files bundled with the embed package? // TODO: Possibly break these out into separate files bundled with the embed package?
const userInsert = "INSERT INTO users (username, password) VALUES (?, ?)" const userInsert = "INSERT INTO users (username, password) VALUES (?, ?)"
const projectInsert = "INSERT INTO projects (name, description, owner_user_id) VALUES (?, ?, (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 promoteToAdmin = "INSERT INTO site_admin (admin_id) SELECT id FROM users WHERE username = ?"
const addWeeklyReport = `WITH UserLookup AS (SELECT id FROM users WHERE username = ?), const addWeeklyReport = `WITH UserLookup AS (SELECT id FROM users WHERE username = ?),
ProjectLookup AS (SELECT id FROM projects WHERE name = ?) ProjectLookup AS (SELECT id FROM projects WHERE name = ?)
INSERT INTO weekly_reports (project_id, user_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time) INSERT INTO weekly_reports (project_id, user_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time)
VALUES ((SELECT id FROM ProjectLookup), (SELECT id FROM UserLookup),?, ?, ?, ?, ?, ?, ?);` VALUES ((SELECT id FROM ProjectLookup), (SELECT id FROM UserLookup),?, ?, ?, ?, ?, ?, ?);`
const addUserToProject = `INSERT OR IGNORE INTO user_roles (user_id, project_id, p_role) const addUserToProject = "INSERT INTO user_roles (user_id, project_id, p_role) VALUES (?, ?, ?)" // WIP
VALUES ((SELECT id FROM users WHERE username = ?), const changeUserRole = "UPDATE user_roles SET p_role = ? WHERE user_id = ? AND project_id = ?"
(SELECT id FROM projects WHERE name = ?), ?)`
const changeUserRole = "UPDATE user_roles SET p_role = ? WHERE user_id = (SELECT id FROM users WHERE username = ?) AND project_id = (SELECT id FROM projects WHERE name = ?)"
const getProjectsForUser = `SELECT p.id, p.name, p.description FROM projects p const getProjectsForUser = `SELECT p.id, p.name, p.description FROM projects p
JOIN user_roles ur ON p.id = ur.project_id JOIN user_roles ur ON p.id = ur.project_id
JOIN users u ON ur.user_id = u.id JOIN users u ON ur.user_id = u.id
WHERE u.username = ?` WHERE u.username = ?`
const deleteProject = `DELETE FROM projects
WHERE id = ? AND owner_username = ?`
const isProjectManagerQuery = `SELECT COUNT(*) > 0 FROM user_roles
JOIN users ON user_roles.user_id = users.id
JOIN projects ON user_roles.project_id = projects.id
WHERE users.username = ? AND projects.name = ? AND user_roles.p_role = 'project_manager'`
const removeUserFromProjectQuery = `DELETE FROM user_roles
WHERE user_id = (SELECT id FROM users WHERE username = ?)
AND project_id = (SELECT id FROM projects WHERE name = ?)`
const reportStatistics = `SELECT SUM(development_time) AS total_development_time,
SUM(meeting_time) AS total_meeting_time,
SUM(admin_time) AS total_admin_time,
SUM(own_work_time) AS total_own_work_time,
SUM(study_time) AS total_study_time,
SUM(testing_time) AS total_testing_time
FROM weekly_reports
WHERE user_id = (SELECT id FROM users WHERE username = ?)
AND project_id = (SELECT id FROM projects WHERE name = ?)
GROUP BY user_id, project_id`
// DbConnect connects to the database // DbConnect connects to the database
func DbConnect(dbpath string) sqlx.DB { func DbConnect(dbpath string) Database {
// Open the database // Open the database
db, err := sqlx.Connect("sqlite", dbpath) db, err := sqlx.Connect("sqlite", dbpath)
if err != nil { if err != nil {
@ -121,25 +83,7 @@ func DbConnect(dbpath string) sqlx.DB {
panic(err) panic(err)
} }
return *db return &Db{db}
}
func (d *Db) ReportStatistics(username string, projectName string) (*types.Statistics, error) {
var result types.Statistics
err := d.Get(&result, reportStatistics, username, projectName)
if err != nil {
return nil, err
}
serialized, err := json.Marshal(result)
if err != nil {
return nil, err
}
log.Info(string(serialized))
return &result, nil
} }
func (d *Db) CheckUser(username string, password string) bool { func (d *Db) CheckUser(username string, password string) bool {
@ -181,28 +125,42 @@ func (d *Db) AddWeeklyReport(projectName string, userName string, week int, deve
} }
// AddUserToProject adds a user to a project with a specified role. // AddUserToProject adds a user to a project with a specified role.
func (d *Db) AddUserToProject(username string, projectname string, role string) error { func (d *Db) AddUserToProject(username string, projectname string, role string) error { // WIP
_, err := d.Exec(addUserToProject, username, projectname, role) var userid int
return err userid, err := d.GetUserId(username)
} if err != nil {
panic(err)
}
func (d *Db) RemoveUserFromProject(username string, projectname string) error { var projectid int
_, err := d.Exec(removeUserFromProjectQuery, username, projectname) projectid, err2 := d.GetProjectId(projectname)
return err if err2 != nil {
panic(err2)
}
_, err3 := d.Exec(addUserToProject, userid, projectid, role)
return err3
} }
// ChangeUserRole changes the role of a user within a project. // ChangeUserRole changes the role of a user within a project.
func (d *Db) ChangeUserRole(username string, projectname string, role string) error { func (d *Db) ChangeUserRole(username string, projectname string, role string) error {
// Execute the SQL query to change the user's role // Get the user ID
_, err := d.Exec(changeUserRole, role, username, projectname) var userid int
return err userid, err := d.GetUserId(username)
} if err != nil {
panic(err)
}
// ChangeUserName changes the username of a user. // Get the project ID
func (d *Db) ChangeUserName(username string, newname string) error { var projectid int
// Execute the SQL query to update the username projectid, err2 := d.GetProjectId(projectname)
_, err := d.Exec("UPDATE users SET username = ? WHERE username = ?", newname, username) if err2 != nil {
return err panic(err2)
}
// Execute the SQL query to change the user's role
_, err3 := d.Exec(changeUserRole, role, userid, projectid)
return err3
} }
// GetUserRole retrieves the role of a user within a project. // GetUserRole retrieves the role of a user within a project.
@ -243,23 +201,24 @@ func (d *Db) GetProjectId(projectname string) (int, error) {
// Creates a new project in the database, associated with a user // Creates a new project in the database, associated with a user
func (d *Db) AddProject(name string, description string, username string) error { func (d *Db) AddProject(name string, description string, username string) error {
// Insert the project into the database tx := d.MustBegin()
_, err := d.Exec(projectInsert, name, description, username) _, err := tx.Exec(projectInsert, name, description, username)
if err != nil { if err != nil {
if err := tx.Rollback(); err != nil {
return err return err
} }
// Add creator to project as project manager
_, err = d.Exec(addUserToProject, username, name, "project_manager")
if err != nil {
return err return err
} }
_, err = tx.Exec(changeUserRole, "project_manager", username, name)
if err != nil {
if err := tx.Rollback(); err != nil {
return err return err
} }
return err
func (d *Db) DeleteProject(projectID string, username string) error { }
_, err := d.Exec(deleteProject, projectID, username) if err := tx.Commit(); err != nil {
return err
}
return err return err
} }
@ -364,14 +323,9 @@ func (d *Db) SignWeeklyReport(reportId int, projectManagerId int) error {
return err return err
} }
managerQuery := `SELECT project_id FROM user_roles
WHERE user_id = ?
AND project_id = (SELECT project_id FROM weekly_reports WHERE report_id = ?)
AND p_role = 'project_manager'`
// Retrieve the project ID associated with the project manager // Retrieve the project ID associated with the project manager
var managerProjectID int var managerProjectID int
err = d.Get(&managerProjectID, managerQuery, projectManagerId, reportId) err = d.Get(&managerProjectID, "SELECT project_id FROM user_roles WHERE user_id = ? AND p_role = 'project_manager'", projectManagerId)
if err != nil { if err != nil {
return err return err
} }
@ -386,81 +340,6 @@ func (d *Db) SignWeeklyReport(reportId int, projectManagerId int) error {
return err return err
} }
func (d *Db) UnsignWeeklyReport(reportId int, projectManagerId int) error {
// Retrieve the project ID associated with the report
var reportProjectID int
err := d.Get(&reportProjectID, "SELECT project_id FROM weekly_reports WHERE report_id = ?", reportId)
if err != nil {
return err
}
managerQuery := `SELECT project_id FROM user_roles
WHERE user_id = ?
AND project_id = (SELECT project_id FROM weekly_reports WHERE report_id = ?)
AND p_role = 'project_manager'`
// Retrieve the project ID associated with the project manager
var managerProjectID int
err = d.Get(&managerProjectID, managerQuery, projectManagerId, reportId)
if err != nil {
return err
}
// Check if the project manager is in the same project as the report
if reportProjectID != managerProjectID {
return errors.New("project manager doesn't have permission to unsign the report")
}
// Update the signed_by field of the specified report
_, err = d.Exec("UPDATE weekly_reports SET signed_by = NULL WHERE report_id = ?;", reportId)
return err
}
func (d *Db) GetUnsignedWeeklyReports(projectName string) ([]types.WeeklyReport, error) {
// Define the SQL query to fetch unsigned reports for a given user
query := `
SELECT
report_id,
user_id,
project_id,
week,
development_time,
meeting_time,
admin_time,
own_work_time,
study_time,
testing_time,
signed_by
FROM
weekly_reports
WHERE
signed_by IS NULL
AND project_id = (SELECT id FROM projects WHERE 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 reports []types.WeeklyReport
for rows.Next() {
var report types.WeeklyReport
if err := rows.StructScan(&report); err != nil {
return nil, err
}
reports = append(reports, report)
}
if err := rows.Err(); err != nil {
return nil, err
}
return reports, nil
}
// IsSiteAdmin checks if a given username is a site admin // IsSiteAdmin checks if a given username is a site admin
func (d *Db) IsSiteAdmin(username string) (bool, error) { func (d *Db) IsSiteAdmin(username string) (bool, error) {
// Define the SQL query to check if the user is a site admin // Define the SQL query to check if the user is a site admin
@ -483,7 +362,7 @@ func (d *Db) IsSiteAdmin(username string) (bool, error) {
// Reads a directory of migration files and applies them to the database. // Reads a directory of migration files and applies them to the database.
// This will eventually be used on an embedded directory // This will eventually be used on an embedded directory
func Migrate(db sqlx.DB) error { func (d *Db) Migrate() error {
// Read the embedded scripts directory // Read the embedded scripts directory
files, err := scripts.ReadDir("migrations") files, err := scripts.ReadDir("migrations")
if err != nil { if err != nil {
@ -495,7 +374,7 @@ func Migrate(db sqlx.DB) error {
return nil return nil
} }
tr := db.MustBegin() tr := d.MustBegin()
// Iterate over each SQL file and execute it // Iterate over each SQL file and execute it
for _, file := range files { for _, file := range files {
@ -523,65 +402,8 @@ func Migrate(db sqlx.DB) error {
return nil return nil
} }
// GetAllWeeklyReports retrieves weekly reports for a specific user and project.
func (d *Db) GetAllWeeklyReports(username string, projectName string) ([]types.WeeklyReportList, error) {
query := `
SELECT
wr.week,
wr.development_time,
wr.meeting_time,
wr.admin_time,
wr.own_work_time,
wr.study_time,
wr.testing_time,
wr.signed_by
FROM
weekly_reports wr
INNER JOIN
users u ON wr.user_id = u.id
INNER JOIN
projects p ON wr.project_id = p.id
WHERE
u.username = ? AND p.name = ?
`
var reports []types.WeeklyReportList
if err := d.Select(&reports, query, username, projectName); err != nil {
return nil, err
}
return reports, nil
}
// IsProjectManager checks if a given username is a project manager for the specified project
func (d *Db) IsProjectManager(username string, projectname string) (bool, error) {
var manager bool
err := d.Get(&manager, isProjectManagerQuery, username, projectname)
return manager, err
}
func (d *Db) UpdateWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error {
query := `
UPDATE weekly_reports
SET
development_time = ?,
meeting_time = ?,
admin_time = ?,
own_work_time = ?,
study_time = ?,
testing_time = ?
WHERE
user_id = (SELECT id FROM users WHERE username = ?)
AND project_id = (SELECT id FROM projects WHERE name = ?)
AND week = ?
`
_, err := d.Exec(query, developmentTime, meetingTime, adminTime, ownWorkTime, studyTime, testingTime, userName, projectName, week)
return err
}
// MigrateSampleData applies sample data to the database. // MigrateSampleData applies sample data to the database.
func MigrateSampleData(db sqlx.DB) error { func (d *Db) MigrateSampleData() error {
// Insert sample data // Insert sample data
files, err := sampleData.ReadDir("sample_data") files, err := sampleData.ReadDir("sample_data")
if err != nil { if err != nil {
@ -591,7 +413,7 @@ func MigrateSampleData(db sqlx.DB) error {
if len(files) == 0 { if len(files) == 0 {
println("No sample data files found") println("No sample data files found")
} }
tr := db.MustBegin() tr := d.MustBegin()
// Iterate over each SQL file and execute it // Iterate over each SQL file and execute it
for _, file := range files { for _, file := range files {
@ -618,68 +440,3 @@ func MigrateSampleData(db sqlx.DB) error {
return nil return nil
} }
// GetProjectTimes retrieves a map with times per "Activity" for a given project
func (d *Db) GetProjectTimes(projectName string) (map[string]int, error) {
query := `
SELECT development_time, meeting_time, admin_time, own_work_time, study_time, testing_time
FROM weekly_reports
JOIN projects ON weekly_reports.project_id = projects.id
WHERE projects.name = ?
`
rows, err := d.Query(query, projectName)
if err != nil {
return nil, err
}
defer rows.Close()
totalTime := make(map[string]int)
for rows.Next() {
var developmentTime, meetingTime, adminTime, ownWorkTime, studyTime, testingTime int
if err := rows.Scan(&developmentTime, &meetingTime, &adminTime, &ownWorkTime, &studyTime, &testingTime); err != nil {
return nil, err
}
totalTime["development"] += developmentTime
totalTime["meeting"] += meetingTime
totalTime["admin"] += adminTime
totalTime["own_work"] += ownWorkTime
totalTime["study"] += studyTime
totalTime["testing"] += testingTime
}
if err := rows.Err(); err != nil {
return nil, err
}
return totalTime, nil
}
func (d *Db) RemoveProject(projectname string) error {
_, err := d.Exec("DELETE FROM projects WHERE name = ?", projectname)
return err
}
func (d *Db) GetUserName(id int) (string, error) {
var username string
err := d.Get(&username, "SELECT username FROM users WHERE id = ?", id)
return username, err
}
func (d *Db) DeleteReport(reportID int) error {
_, err := d.Exec("DELETE FROM weekly_reports WHERE report_id = ?", reportID)
return err
}
// ChangeProjectName is a handler that changes the name of a project
func (d *Db) ChangeProjectName(projectName string, newProjectName string) error {
_, err := d.Exec("UPDATE projects SET name = ? WHERE name = ?", newProjectName, projectName)
return err
}
func (d *Db) ChangeUserPassword(username string, password string) error {
_, err := d.Exec("UPDATE users SET password = ? WHERE username = ?", password, username)
return err
}

View file

@ -1,79 +1,26 @@
package database package database
import ( import (
"fmt"
"testing" "testing"
) )
// Tests are not guaranteed to be sequential // Tests are not guaranteed to be sequential
// setupState initializes a database instance with necessary setup for testing
func setupState() (Database, error) { func setupState() (Database, error) {
db := DbConnect(":memory:") db := DbConnect(":memory:")
err := Migrate(db) err := db.Migrate()
if err != nil { if err != nil {
return nil, err return nil, err
} }
db_iface := Db{db.MustBegin()}
return &db_iface, nil
}
// This is a more advanced setup that includes more data in the database.
// This is useful for more complex testing scenarios.
func setupAdvancedState() (Database, error) {
db, err := setupState()
if err != nil {
return nil, err
}
// Add a user
if err = db.AddUser("demouser", "password"); err != nil {
return nil, err
}
// Add a project
if err = db.AddProject("projecttest", "description", "demouser"); err != nil {
return nil, err
}
// Add a weekly report
if err = db.AddWeeklyReport("projecttest", "demouser", 1, 1, 1, 1, 1, 1, 1); err != nil {
return nil, err
}
return db, nil return db, nil
} }
// TestDbConnect tests the connection to the database
func TestDbConnect(t *testing.T) { func TestDbConnect(t *testing.T) {
db := DbConnect(":memory:") db := DbConnect(":memory:")
_ = db _ = db
} }
func TestSetupAdvancedState(t *testing.T) {
db, err := setupAdvancedState()
if err != nil {
t.Error("setupAdvancedState failed:", err)
}
// Check if the user was added
if _, err = db.GetUserId("demouser"); err != nil {
t.Error("GetUserId failed:", err)
}
// Check if the project was added
projects, err := db.GetAllProjects()
if err != nil {
t.Error("GetAllProjects failed:", err)
}
if len(projects) != 1 {
t.Error("GetAllProjects failed: expected 1, got", len(projects))
}
// To be continued...
}
// TestDbAddUser tests the AddUser function of the database
func TestDbAddUser(t *testing.T) { func TestDbAddUser(t *testing.T) {
db, err := setupState() db, err := setupState()
if err != nil { if err != nil {
@ -85,7 +32,6 @@ func TestDbAddUser(t *testing.T) {
} }
} }
// TestDbGetUserId tests the GetUserID function of the database
func TestDbGetUserId(t *testing.T) { func TestDbGetUserId(t *testing.T) {
db, err := setupState() db, err := setupState()
if err != nil { if err != nil {
@ -106,20 +52,18 @@ func TestDbGetUserId(t *testing.T) {
} }
} }
// TestDbAddProject tests the AddProject function of the database
func TestDbAddProject(t *testing.T) { func TestDbAddProject(t *testing.T) {
db, err := setupAdvancedState() db, err := setupState()
if err != nil { if err != nil {
t.Error("setupState failed:", err) t.Error("setupState failed:", err)
} }
err = db.AddProject("test", "description", "demouser") err = db.AddProject("test", "description", "test")
if err != nil { if err != nil {
t.Error("AddProject failed:", err) t.Error("AddProject failed:", err)
} }
} }
// TestDbRemoveUser tests the RemoveUser function of the database
func TestDbRemoveUser(t *testing.T) { func TestDbRemoveUser(t *testing.T) {
db, err := setupState() db, err := setupState()
if err != nil { if err != nil {
@ -132,7 +76,6 @@ func TestDbRemoveUser(t *testing.T) {
} }
} }
// TestPromoteToAdmin tests the PromoteToAdmin function of the database
func TestPromoteToAdmin(t *testing.T) { func TestPromoteToAdmin(t *testing.T) {
db, err := setupState() db, err := setupState()
if err != nil { if err != nil {
@ -150,7 +93,6 @@ func TestPromoteToAdmin(t *testing.T) {
} }
} }
// TestAddWeeklyReport tests the AddWeeklyReport function of the database
func TestAddWeeklyReport(t *testing.T) { func TestAddWeeklyReport(t *testing.T) {
db, err := setupState() db, err := setupState()
if err != nil { if err != nil {
@ -173,7 +115,6 @@ func TestAddWeeklyReport(t *testing.T) {
} }
} }
// TestAddUserToProject tests the AddUseToProject function of the database
func TestAddUserToProject(t *testing.T) { func TestAddUserToProject(t *testing.T) {
db, err := setupState() db, err := setupState()
if err != nil { if err != nil {
@ -201,7 +142,6 @@ func TestAddUserToProject(t *testing.T) {
} }
} }
// TestChangeUserRole tests the ChangeUserRole function of the database
func TestChangeUserRole(t *testing.T) { func TestChangeUserRole(t *testing.T) {
db, err := setupState() db, err := setupState()
if err != nil { if err != nil {
@ -218,15 +158,20 @@ func TestChangeUserRole(t *testing.T) {
t.Error("AddProject failed:", err) 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") role, err := db.GetUserRole("testuser", "testproject")
if err != nil { if err != nil {
t.Error("GetUserRole failed:", err) t.Error("GetUserRole failed:", err)
} }
if role != "project_manager" { if role != "user" {
t.Error("GetUserRole failed: expected project_manager, got", role) t.Error("GetUserRole failed: expected user, got", role)
} }
err = db.ChangeUserRole("testuser", "testproject", "member") err = db.ChangeUserRole("testuser", "testproject", "admin")
if err != nil { if err != nil {
t.Error("ChangeUserRole failed:", err) t.Error("ChangeUserRole failed:", err)
} }
@ -235,13 +180,12 @@ func TestChangeUserRole(t *testing.T) {
if err != nil { if err != nil {
t.Error("GetUserRole failed:", err) t.Error("GetUserRole failed:", err)
} }
if role != "member" { if role != "admin" {
t.Error("GetUserRole failed: expected member, got", role) t.Error("GetUserRole failed: expected admin, got", role)
} }
} }
// TestGetAllUsersProject tests the GetAllUsersProject function of the database
func TestGetAllUsersProject(t *testing.T) { func TestGetAllUsersProject(t *testing.T) {
db, err := setupState() db, err := setupState()
if err != nil { if err != nil {
@ -308,7 +252,6 @@ func TestGetAllUsersProject(t *testing.T) {
} }
} }
// TestGetAllUsersApplication tests the GetAllUsersApplicsation function of the database
func TestGetAllUsersApplication(t *testing.T) { func TestGetAllUsersApplication(t *testing.T) {
db, err := setupState() db, err := setupState()
if err != nil { if err != nil {
@ -355,7 +298,6 @@ func TestGetAllUsersApplication(t *testing.T) {
} }
} }
// TestGetProjectsForUser tests the GetProjectsForUser function of the database
func TestGetProjectsForUser(t *testing.T) { func TestGetProjectsForUser(t *testing.T) {
db, err := setupState() db, err := setupState()
if err != nil { if err != nil {
@ -396,7 +338,6 @@ func TestGetProjectsForUser(t *testing.T) {
} }
} }
// TestAddProject tests AddProject function of the database
func TestAddProject(t *testing.T) { func TestAddProject(t *testing.T) {
db, err := setupState() db, err := setupState()
if err != nil { if err != nil {
@ -432,7 +373,6 @@ func TestAddProject(t *testing.T) {
} }
} }
// TestGetWeeklyReport tests GetWeeklyReport function of the database
func TestGetWeeklyReport(t *testing.T) { func TestGetWeeklyReport(t *testing.T) {
db, err := setupState() db, err := setupState()
if err != nil { if err != nil {
@ -472,48 +412,6 @@ func TestGetWeeklyReport(t *testing.T) {
// Check other fields similarly // Check other fields similarly
} }
func TestGetUnsignedWeeklyReports(t *testing.T) {
db, err := setupAdvancedState()
if err != nil {
t.Error("setupState failed:", err)
}
err = db.AddUser("testuser", "password")
if err != nil {
t.Error("AddUser failed:", err)
}
err = db.AddUser("testuser1", "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.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1)
if err != nil {
t.Error("AddWeeklyReport failed:", err)
}
err = db.AddWeeklyReport("testproject", "testuser1", 1, 1, 1, 1, 1, 1, 1)
if err != nil {
t.Error("AddWeeklyReport failed:", err)
}
reports, err := db.GetUnsignedWeeklyReports("testproject")
if err != nil {
t.Error("GetUnsignedWeeklyReports failed:", err)
}
if reports == nil {
t.Error("Expected non-nil reports, got nil")
}
}
// TestSignWeeklyReport tests SignWeeklyReport function of the database
func TestSignWeeklyReport(t *testing.T) { func TestSignWeeklyReport(t *testing.T) {
db, err := setupState() db, err := setupState()
if err != nil { if err != nil {
@ -566,6 +464,7 @@ func TestSignWeeklyReport(t *testing.T) {
if err != nil { if err != nil {
t.Error("GetUserId failed:", err) t.Error("GetUserId failed:", err)
} }
fmt.Println("Project Manager's ID:", projectManagerID)
// Sign the report with the project manager // Sign the report with the project manager
err = db.SignWeeklyReport(report.ReportId, projectManagerID) err = db.SignWeeklyReport(report.ReportId, projectManagerID)
@ -585,7 +484,7 @@ func TestSignWeeklyReport(t *testing.T) {
} }
} }
func TestUnsignWeeklyReport(t *testing.T) { func TestSignWeeklyReportByAnotherProjectManager(t *testing.T) {
db, err := setupState() db, err := setupState()
if err != nil { if err != nil {
t.Error("setupState failed:", err) t.Error("setupState failed:", err)
@ -609,95 +508,6 @@ func TestUnsignWeeklyReport(t *testing.T) {
t.Error("AddProject failed:", err) t.Error("AddProject failed:", err)
} }
// Add both regular users as members to the project
err = db.AddUserToProject("testuser", "testproject", "member")
if err != nil {
t.Error("AddUserToProject failed:", err)
}
err = db.AddUserToProject("projectManager", "testproject", "project_manager")
if err != nil {
t.Error("AddUserToProject failed:", err)
}
// Add a weekly report for one of the regular users
err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1)
if err != nil {
t.Error("AddWeeklyReport failed:", err)
}
// Retrieve the added report
report, err := db.GetWeeklyReport("testuser", "testproject", 1)
if err != nil {
t.Error("GetWeeklyReport failed:", err)
}
// Print project manager's ID
projectManagerID, err := db.GetUserId("projectManager")
if err != nil {
t.Error("GetUserId failed:", err)
}
// Sign the report with the project manager
err = db.SignWeeklyReport(report.ReportId, projectManagerID)
if err != nil {
t.Error("SignWeeklyReport failed:", err)
}
// Retrieve the report again to check if it's signed
signedReport, err := db.GetWeeklyReport("testuser", "testproject", 1)
if err != nil {
t.Error("GetWeeklyReport failed:", err)
}
// Ensure the report is signed by the project manager
if *signedReport.SignedBy != projectManagerID {
t.Errorf("Expected SignedBy to be %d, got %d", projectManagerID, *signedReport.SignedBy)
}
// Unsign the report
err = db.UnsignWeeklyReport(report.ReportId, projectManagerID)
if err != nil {
t.Error("UnsignWeeklyReport failed:", err)
}
// Retrieve the report again to check if it's unsigned
unsignedReport, err := db.GetWeeklyReport("testuser", "testproject", 1)
if err != nil {
t.Error("GetWeeklyReport failed:", err)
}
// Ensure the report is unsigned
if unsignedReport.SignedBy != nil {
t.Error("Expected SignedBy to be nil, got", unsignedReport.SignedBy)
}
}
// TestSignWeeklyReportByAnotherProjectManager tests the scenario where a project manager attempts to sign a weekly report for a user who is not assigned to their project
func TestSignWeeklyReportByAnotherProjectManager(t *testing.T) {
db, err := setupState()
if err != nil {
t.Error("setupState failed:", err)
}
// Add project manager
err = db.AddUser("projectManager", "password")
if err != nil {
t.Error("AddUser failed:", err)
}
// Add a regular user
err = db.AddUser("testuser", "password")
if err != nil {
t.Error("AddUser failed:", err)
}
// Add project, projectManager is the owner
err = db.AddProject("testproject", "description", "projectManager")
if err != nil {
t.Error("AddProject failed:", err)
}
// Add the regular user as a member to the project // Add the regular user as a member to the project
err = db.AddUserToProject("testuser", "testproject", "member") err = db.AddUserToProject("testuser", "testproject", "member")
if err != nil { if err != nil {
@ -716,29 +526,17 @@ func TestSignWeeklyReportByAnotherProjectManager(t *testing.T) {
t.Error("GetWeeklyReport failed:", err) t.Error("GetWeeklyReport failed:", err)
} }
managerID, err := db.GetUserId("projectManager") anotherManagerID, err := db.GetUserId("projectManager")
if err != nil { if err != nil {
t.Error("GetUserId failed:", err) t.Error("GetUserId failed:", err)
} }
err = db.SignWeeklyReport(report.ReportId, managerID) err = db.SignWeeklyReport(report.ReportId, anotherManagerID)
if err != nil { if err == nil {
t.Error("SignWeeklyReport failed:", err) t.Error("Expected SignWeeklyReport to fail with a project manager who is not in the project, but it didn't")
}
// Retrieve the report again to check if it's signed
signedReport, err := db.GetWeeklyReport("testuser", "testproject", 1)
if err != nil {
t.Error("GetWeeklyReport failed:", err)
}
// Ensure the report is signed by the project manager
if *signedReport.SignedBy != managerID {
t.Errorf("Expected SignedBy to be %d, got %d", managerID, *signedReport.SignedBy)
} }
} }
// TestGetProject tests GetProject function of the database
func TestGetProject(t *testing.T) { func TestGetProject(t *testing.T) {
db, err := setupState() db, err := setupState()
if err != nil { if err != nil {
@ -768,377 +566,3 @@ func TestGetProject(t *testing.T) {
t.Errorf("Expected Name to be testproject, got %s", project.Name) t.Errorf("Expected Name to be testproject, got %s", project.Name)
} }
} }
func TestGetWeeklyReportsUser(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.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1)
if err != nil {
t.Error("AddWeeklyReport failed:", err)
}
err = db.AddWeeklyReport("testproject", "testuser", 2, 1, 1, 1, 1, 1, 1)
if err != nil {
t.Error("AddWeeklyReport failed:", err)
}
reports, err := db.GetAllWeeklyReports("testuser", "testproject")
if err != nil {
t.Error("GetWeeklyReportsUser failed:", err)
}
// Check if the retrieved reports match the expected values
if len(reports) != 2 {
t.Errorf("Expected 1 report, got %d", len(reports))
}
}
func TestIsProjectManager(t *testing.T) {
db, err := setupState()
if err != nil {
t.Error("setupState failed:", err)
}
// Add a project manager
err = db.AddUser("projectManager", "password")
if err != nil {
t.Error("AddUser failed:", err)
}
// Add a regular user
err = db.AddUser("testuser", "password")
if err != nil {
t.Error("AddUser failed:", err)
}
// Add project
err = db.AddProject("testproject", "description", "projectManager")
if err != nil {
t.Error("AddProject failed:", err)
}
// Add both regular users as members to the project
err = db.AddUserToProject("testuser", "testproject", "member")
if err != nil {
t.Error("AddUserToProject failed:", err)
}
err = db.AddUserToProject("projectManager", "testproject", "project_manager")
if err != nil {
t.Error("AddUserToProject failed:", err)
}
// Check if the regular user is not a project manager
isManager, err := db.IsProjectManager("testuser", "testproject")
if err != nil {
t.Error("IsProjectManager failed:", err)
}
if isManager {
t.Error("Expected testuser not to be a project manager, but it is.")
}
// Check if the project manager is indeed a project manager
isManager, err = db.IsProjectManager("projectManager", "testproject")
if err != nil {
t.Error("IsProjectManager failed:", err)
}
if !isManager {
t.Error("Expected projectManager to be a project manager, but it's not.")
}
}
func TestGetProjectTimes(t *testing.T) {
// Initialize
db, err := setupState()
if err != nil {
t.Error("setupState failed:", err)
return
}
// Create a user
user := "TeaUser"
password := "Vanilla"
err = db.AddUser(user, password)
if err != nil {
t.Error("AddUser failed:", err)
return
}
// Create a project
projectName := "ProjectVanilla"
projectDescription := "When tea tastes its best"
err = db.AddProject(projectName, projectDescription, user) // Fix the variable name here
if err != nil {
t.Error("AddProject failed:", err)
return
}
// Tests the func in db.go
totalTime, err := db.GetProjectTimes(projectName)
if err != nil {
t.Error("GetTotalTimePerActivity failed:", err)
return
}
// Check if the totalTime map is not nil
if totalTime == nil {
t.Error("Expected non-nil totalTime map, got nil")
return
}
// Define the expected valeus
expectedTotalTime := map[string]int{
"development": 0,
"meeting": 0,
"admin": 0,
"own_work": 0,
"study": 0,
"testing": 0,
}
// Compare the expectedTotalTime with the totalTime retrieved from the database
for activity, expectedTime := range expectedTotalTime {
if totalTime[activity] != expectedTime {
t.Errorf("Expected %s time to be %d, got %d", activity, expectedTime, totalTime[activity])
}
}
// Insert some data into the database for different activities
err = db.AddWeeklyReport(projectName, user, 1, 1, 3, 2, 1, 4, 5)
if err != nil {
t.Error("Failed to insert data into the database:", err)
return
}
newTotalTime, err := db.GetProjectTimes(projectName)
if err != nil {
t.Error("GetTotalTimePerActivity failed:", err)
return
}
newExpectedTotalTime := map[string]int{
"development": 1,
"meeting": 3,
"admin": 2,
"own_work": 1,
"study": 4,
"testing": 5,
}
for activity, newExpectedTime := range newExpectedTotalTime {
if newTotalTime[activity] != newExpectedTime {
t.Errorf("Expected %s time to be %d, got %d", activity, newExpectedTime, newTotalTime[activity])
}
}
}
func TestEnsureManagerOfCreatedProject(t *testing.T) {
db, err := setupState()
if err != nil {
t.Error("setupState failed:", err)
}
// Add a user
err = db.AddUser("testuser", "password")
if err != nil {
t.Error("AddUser failed:", err)
}
// Add a project
err = db.AddProject("testproject", "description", "testuser")
if err != nil {
t.Error("AddProject failed:", err)
}
// Set user to a project manager
// err = db.AddUserToProject("testuser", "testproject", "project_manager")
// if err != nil {
// t.Error("AddUserToProject failed:", err)
// }
managerState, err := db.IsProjectManager("testuser", "testproject")
if err != nil {
t.Error("IsProjectManager failed:", err)
}
if !managerState {
t.Error("Expected testuser to be a project manager, but it's not.")
}
}
// TestUpdateWeeklyReport tests the UpdateWeeklyReport function of the database
func TestUpdateWeeklyReport(t *testing.T) {
db, err := setupState()
if err != nil {
t.Error("setupState failed:", err)
}
// Add a user
err = db.AddUser("testuser", "password")
if err != nil {
t.Error("AddUser failed:", err)
}
// Add a project
err = db.AddProject("testproject", "description", "testuser")
if err != nil {
t.Error("AddProject failed:", err)
}
// Add a weekly report
err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1)
if err != nil {
t.Error("AddWeeklyReport failed:", err)
}
// Update the weekly report
err = db.UpdateWeeklyReport("testproject", "testuser", 1, 2, 2, 2, 2, 2, 2)
if err != nil {
t.Error("UpdateWeeklyReport failed:", err)
}
// Retrieve the updated report
updatedReport, err := db.GetWeeklyReport("testuser", "testproject", 1)
if err != nil {
t.Error("GetWeeklyReport failed:", err)
}
// Check if the report was updated correctly
if updatedReport.DevelopmentTime != 2 ||
updatedReport.MeetingTime != 2 ||
updatedReport.AdminTime != 2 ||
updatedReport.OwnWorkTime != 2 ||
updatedReport.StudyTime != 2 ||
updatedReport.TestingTime != 2 {
t.Error("UpdateWeeklyReport failed: report not updated correctly")
}
}
func TestRemoveProject(t *testing.T) {
db, err := setupAdvancedState()
if err != nil {
t.Error("setupState failed:", err)
}
// Promote user to Admin
err = db.PromoteToAdmin("demouser")
if err != nil {
t.Error("PromoteToAdmin failed:", err)
}
// Remove project
err = db.RemoveProject("projecttest")
if err != nil {
t.Error("RemoveProject failed:", err)
}
// Check if the project was removed
projects, err := db.GetAllProjects()
if err != nil {
t.Error("GetAllProjects failed:", err)
}
if len(projects) != 0 {
t.Error("RemoveProject failed: expected 0, got", len(projects))
}
}
func TestDeleteReport(t *testing.T) {
db, err := setupAdvancedState()
if err != nil {
t.Error("setupState failed:", err)
}
// Promote user to Admin
err = db.PromoteToAdmin("demouser")
if err != nil {
t.Error("PromoteToAdmin failed:", err)
}
// create a weekly report
err = db.AddWeeklyReport("projecttest", "demouser", 16, 1, 1, 1, 1, 1, 1)
if err != nil {
t.Error("AddWeeklyReport failed:", err)
}
// Check if the report was added
report, err := db.GetWeeklyReport("demouser", "projecttest", 16)
if err != nil {
t.Error("GetWeeklyReport failed:", err)
}
// Remove report
err = db.DeleteReport(report.ReportId)
if err != nil {
t.Error("RemoveReport failed:", err)
}
// Check if the report was removed
report, err = db.GetWeeklyReport("demouser", "projecttest", 16)
if err == nil {
t.Error("RemoveReport failed: report not removed")
}
}
func TestChangeProjectName(t *testing.T) {
db, err := setupAdvancedState()
if err != nil {
t.Error("setupState failed:", err)
}
// Promote user to Admin
err = db.PromoteToAdmin("demouser")
if err != nil {
t.Error("PromoteToAdmin failed:", err)
}
// Change project name
err = db.ChangeProjectName("projecttest", "newprojectname")
if err != nil {
t.Error("ChangeProjectName failed:", err)
}
// Check if the project name was changed
projects, err := db.GetAllProjects()
if err != nil {
t.Error("GetAllProjects failed:", err)
}
if projects[0].Name != "newprojectname" {
t.Error("ChangeProjectName failed: expected newprojectname, got", projects[0].Name)
}
}
func TestChangeUserPassword(t *testing.T) {
db, err := setupState()
if err != nil {
t.Error("setupState failed:", err)
}
// Add a user
_ = db.AddUser("testuser", "password")
// Change user password
err = db.ChangeUserPassword("testuser", "newpassword")
if err != nil {
t.Error("ChangeUserPassword failed:", err)
}
// Check if the password was changed
if !db.CheckUser("testuser", "newpassword") {
t.Error("ChangeUserPassword failed: password not changed")
}
}

View file

@ -1,34 +0,0 @@
package database
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/jmoiron/sqlx"
)
// Simple middleware that provides a transaction as a local key "db"
func DbMiddleware(db *sqlx.DB) func(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error {
tx := db.MustBegin()
defer func() {
if err := tx.Commit(); err != nil {
if err = tx.Rollback(); err != nil {
log.Error("Failed to rollback transaction: ", err)
}
return
}
}()
var db_iface Database = &Db{tx}
c.Locals("db", &db_iface)
return c.Next()
}
}
// Helper function to get the database from the context, without fiddling with casts
func GetDb(c *fiber.Ctx) Database {
// Dereference a pointer to a local, casted to a pointer to a Database
return *c.Locals("db").(*Database)
}

View file

@ -10,7 +10,6 @@ CREATE TABLE IF NOT EXISTS weekly_reports (
study_time INTEGER, study_time INTEGER,
testing_time INTEGER, testing_time INTEGER,
signed_by INTEGER, signed_by INTEGER,
UNIQUE(user_id, project_id, week),
FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (project_id) REFERENCES projects(id), FOREIGN KEY (project_id) REFERENCES projects(id),
FOREIGN KEY (signed_by) REFERENCES users(id) FOREIGN KEY (signed_by) REFERENCES users(id)

View file

@ -1,220 +1,39 @@
INSERT OR IGNORE INTO users(username, password) INSERT
VALUES ("admin", "123"), OR IGNORE INTO users(username, password)
("user", "123"), VALUES ("admin", "123");
("user2", "123"), INSERT
("John", "123"), OR IGNORE INTO users(username, password)
("Emma", "123"), VALUES ("user", "123");
("Michael", "123"), INSERT
("Liam", "123"), OR IGNORE INTO users(username, password)
("Oliver", "123"), VALUES ("user2", "123");
("Amelia", "123"), INSERT
("Benjamin", "123"), OR IGNORE INTO users(username, password)
("Mia", "123"), VALUES ("martin", "genPass123");
("Elijah", "123"), INSERT
("Charlotte", "123"), OR IGNORE INTO projects(name, description, owner_user_id)
("Henry", "123"), VALUES ("projecttest", "test project", 1);
("Harper", "123"), INSERT
("Lucas", "123"), OR IGNORE INTO projects(name, description, owner_user_id)
("Emily", "123"), VALUES ("projecttest2", "test project2", 1);
("Alexander", "123"), INSERT
("Daniel", "123"), OR IGNORE INTO projects(name, description, owner_user_id)
("Ella", "123"), VALUES ("projecttest3", "test project3", 1);
("Matthew", "123"), INSERT
("Madison", "123"), OR IGNORE INTO user_roles(user_id, project_id, p_role)
("Samuel", "123"), VALUES (1, 1, "project_manager");
("Avery", "123"), INSERT
("Sofia", "123"), OR IGNORE INTO user_roles(user_id, project_id, p_role)
("David", "123"), VALUES (2, 1, "member");
("Victoria", "123"), INSERT
("Jackson", "123"), OR IGNORE INTO user_roles(user_id, project_id, p_role)
("Abigail", "123"), VALUES (3, 1, "member");
("Gabriel", "123"), INSERT
("Luna", "123"), OR IGNORE INTO user_roles(user_id, project_id, p_role)
("Wyatt", "123"), VALUES (3, 2, "member");
("Chloe", "123"), INSERT
("Nora", "123"), OR IGNORE INTO user_roles(user_id, project_id, p_role)
("Joshua", "123"), VALUES (3, 3, "member");
("Hazel", "123"), INSERT
("Riley", "123"), OR IGNORE INTO user_roles(user_id, project_id, p_role)
("Scarlett", "123"), VALUES (2, 1, "project_manager");
("Aria", "123"),
("Carter", "123"),
("Grace", "123"),
("Jayden", "123"),
("Hannah", "123"),
("Zoe", "123"),
("Luke", "123"),
("Sophia", "123"),
("Jack", "123"),
("Isabella", "123"),
("William", "123"),
("Mason", "123"),
("Evelyn", "123"),
("James", "123"),
("Cynthia", "123"),
("Abraham", "123"),
("Ava", "123"),
("Aiden", "123"),
("Natalie", "123"),
("Lily", "123"),
("Olivia", "123"),
("Alexander", "123"),
("Ethan", "123"),
("Mila", "123"),
("Evelyn", "123"),
("Logan", "123"),
("Riley", "123"),
("Grace", "123"),
("Arnold", "123"),
("Connor", "123"),
("Samantha", "123"),
("Emma", "123"),
("Sarah", "123"),
("Nathan", "123"),
("Layla", "123"),
("Ryan", "123"),
("Zoey", "123"),
("Megan", "123"),
("Christian", "123"),
("Eva", "123"),
("Isaac", "123"),
("Michaela", "123"),
("Caroline", "123"),
("Elijah", "123"),
("Elena", "123"),
("Julian", "123"),
("Sophie", "123"),
("Gabriella", "123"),
("Cole", "123"),
("Hannah", "123"),
("Lucy", "123"),
("Katherine", "123"),
("Benjamin", "123"),
("Ella", "123"),
("Evan", "123");
INSERT OR IGNORE INTO projects(name, description, owner_user_id)
VALUES ("projecttest1", "Description for projecttest1", 1),
("projecttest2", "Description for projecttest2", 1),
("projecttest3", "Description for projecttest3", 1),
("projecttest4", "Description for projecttest4", 1),
("projecttest5", "Description for projecttest5", 1),
("projecttest6", "Description for projecttest6", 1),
("projecttest7", "Description for projecttest7", 1),
("projecttest8", "Description for projecttest8", 1),
("projecttest9", "Description for projecttest9", 1),
("projecttest10", "Description for projecttest10", 1),
("projecttest11", "Description for projecttest11", 1),
("projecttest12", "Description for projecttest12", 1),
("projecttest13", "Description for projecttest13", 1),
("projecttest14", "Description for projecttest14", 1),
("projecttest15", "Description for projecttest15", 1);
INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role)
VALUES (1,1,"project_manager"),
(1,2,"project_manager"),
(1,3,"project_manager"),
(1,4,"project_manager"),
(1,5,"project_manager"),
(1,6,"project_manager"),
(1,7,"project_manager"),
(1,8,"project_manager"),
(1,9,"project_manager"),
(1,10,"project_manager"),
(1,11,"project_manager"),
(1,12,"project_manager"),
(1,13,"project_manager"),
(1,14,"project_manager"),
(1,15,"project_manager"),
(2,1,"project_manager"),
(2,2,"member"),
(2,3,"member"),
(2,4,"member"),
(2,5,"member"),
(2,6,"member"),
(2,7,"member"),
(2,8,"member"),
(2,9,"member"),
(2,10,"member"),
(2,11,"member"),
(2,12,"member"),
(2,13,"member"),
(2,14,"member"),
(2,15,"member"),
(3,1,"member"),
(3,2,"member"),
(3,3,"member"),
(3,4,"member"),
(3,5,"member"),
(3,6,"member"),
(3,7,"member"),
(3,8,"member"),
(3,9,"member"),
(3,10,"member"),
(3,11,"member"),
(3,12,"member"),
(3,13,"member"),
(3,14,"member"),
(3,15,"member"),
(4,1,"member"),
(4,2,"member"),
(4,3,"member"),
(4,4,"member"),
(4,5,"member"),
(4,6,"member"),
(4,7,"member"),
(4,8,"member"),
(4,9,"member"),
(4,10,"member"),
(4,11,"member"),
(4,12,"member"),
(4,13,"member"),
(4,14,"member"),
(4,15,"member"),
(5,1,"member"),
(5,2,"member"),
(5,3,"member"),
(5,4,"member"),
(5,5,"member"),
(5,6,"member"),
(5,7,"member"),
(5,8,"member"),
(5,9,"member"),
(5,10,"member"),
(5,11,"member"),
(5,12,"member"),
(5,13,"member"),
(5,14,"member"),
(5,15,"member");
INSERT OR IGNORE INTO weekly_reports (user_id, project_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time, signed_by)
VALUES (2, 1, 12, 100, 50, 30, 150, 80, 20, NULL),
(3, 1, 12, 200, 80, 20, 200, 100, 30, NULL),
(3, 1, 14, 150, 70, 40, 180, 90, 25, NULL),
(3, 2, 12, 120, 60, 35, 160, 85, 15, NULL),
(3, 3, 12, 180, 90, 25, 190, 110, 40, NULL),
(2, 1, 13, 130, 70, 40, 170, 95, 35, NULL),
(3, 1, 15, 140, 60, 50, 200, 120, 30, NULL),
(2, 2, 11, 110, 50, 45, 140, 70, 25, NULL),
(3, 3, 14, 170, 80, 30, 180, 100, 35, NULL),
(3, 3, 15, 200, 100, 20, 220, 130, 45, NULL),
(2, 4, 12, 120, 60, 40, 160, 80, 30, NULL),
(3, 5, 14, 150, 70, 30, 180, 90, 25, NULL),
(3, 5, 15, 180, 90, 20, 190, 110, 35, NULL),
(2, 6, 11, 100, 50, 35, 130, 60, 20, NULL),
(3, 7, 14, 170, 80, 25, 180, 100, 30, NULL),
(2, 8, 12, 130, 70, 30, 170, 90, 25, NULL),
(2, 8, 13, 150, 80, 20, 180, 110, 35, NULL),
(3, 9, 12, 140, 60, 40, 180, 100, 30, NULL),
(3, 10, 11, 120, 50, 45, 150, 70, 25, NULL),
(2, 11, 13, 110, 60, 35, 140, 80, 30, NULL),
(3, 12, 12, 160, 70, 30, 180, 100, 35, NULL),
(3, 12, 13, 180, 90, 25, 190, 110, 40, NULL),
(3, 12, 14, 200, 100, 20, 220, 130, 45, NULL),
(2, 13, 11, 100, 50, 45, 130, 60, 20, NULL),
(2, 13, 12, 120, 60, 40, 160, 80, 30, NULL),
(3, 14, 13, 140, 70, 30, 160, 90, 35, NULL),
(3, 15, 12, 150, 80, 25, 180, 100, 30, NULL),
(3, 15, 13, 170, 90, 20, 190, 110, 35, NULL);
INSERT OR IGNORE INTO site_admin VALUES (1);

View file

@ -0,0 +1,50 @@
package handlers
import (
"ttime/internal/database"
"github.com/gofiber/fiber/v2"
)
// 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
GetUserProjects(c *fiber.Ctx) error // To get all projects
SubmitWeeklyReport(c *fiber.Ctx) error
GetWeeklyReport(c *fiber.Ctx) error
SignReport(c *fiber.Ctx) error
GetProject(c *fiber.Ctx) error
AddUserToProjectHandler(c *fiber.Ctx) error
PromoteToAdmin(c *fiber.Ctx) error
// 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
// CreateTask(c *fiber.Ctx) error // To create a new task
// GetTasks(c *fiber.Ctx) error // To get all tasks
// GetTask(c *fiber.Ctx) error // To get a specific task
// UpdateTask(c *fiber.Ctx) error // To update a task
// DeleteTask(c *fiber.Ctx) error // To delete a task
// CreateCollection(c *fiber.Ctx) error // To create a new collection
// GetCollections(c *fiber.Ctx) error // To get all collections
// GetCollection(c *fiber.Ctx) error // To get a specific collection
// UpdateCollection(c *fiber.Ctx) error // To update a collection
// DeleteCollection(c *fiber.Ctx) error // To delete a collection
// SignCollection(c *fiber.Ctx) error // To sign a collection
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"
func NewGlobalState(db database.Database) GlobalState {
return &GState{Db: db}
}
// The global state, which implements all the handlers
type GState struct {
Db database.Database
}

View file

@ -0,0 +1,15 @@
package handlers
import (
"testing"
"ttime/internal/database"
)
// The actual interface that we will use
func TestGlobalState(t *testing.T) {
db := database.DbConnect(":memory:")
gs := NewGlobalState(db)
if gs == nil {
t.Error("NewGlobalState returned nil")
}
}

View file

@ -0,0 +1,156 @@
package handlers
import (
"strconv"
"ttime/internal/types"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
// 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)
owner := claims["name"].(string)
if err := gs.Db.AddProject(p.Name, p.Description, owner); err != nil {
return c.Status(500).SendString(err.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)
}
// 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)
}
// GetProject retrieves a specific project by its ID
func (gs *GState) GetProject(c *fiber.Ctx) error {
// Extract the project ID from the request parameters or body
projectID := c.Params("projectID")
if projectID == "" {
log.Info("No project ID provided")
return c.Status(400).SendString("No project ID provided")
}
log.Info("Getting project with ID: ", projectID)
// Parse the project ID into an integer
projectIDInt, err := strconv.Atoi(projectID)
if err != nil {
log.Info("Invalid project ID")
return c.Status(400).SendString("Invalid project ID")
}
// Get the project from the database by its ID
project, err := gs.Db.GetProject(projectIDInt)
if err != nil {
log.Info("Error getting project:", err)
return c.Status(500).SendString(err.Error())
}
// Return the project as JSON
log.Info("Returning project: ", project.Name)
return c.JSON(project)
}
func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error {
// Extract the project name from the request parameters or body
projectName := c.Params("projectName")
if projectName == "" {
log.Info("No project name provided")
return c.Status(400).SendString("No project name provided")
}
// Get all users associated with the project from the database
users, err := gs.Db.GetAllUsersProject(projectName)
if err != nil {
log.Info("Error getting users for project:", err)
return c.Status(500).SendString(err.Error())
}
log.Info("Returning users for project: ", projectName)
// Return the list of users as JSON
return c.JSON(users)
}
// AddUserToProjectHandler is a handler that adds a user to a project with a specified role
func (gs *GState) AddUserToProjectHandler(c *fiber.Ctx) error {
// Extract necessary parameters from the request
var requestData struct {
Username string `json:"username"`
ProjectName string `json:"projectName"`
Role string `json:"role"`
}
if err := c.BodyParser(&requestData); err != nil {
log.Info("Error parsing request body:", err)
return c.Status(400).SendString("Bad request")
}
// Check if the user adding another user to the project is a site admin
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
adminUsername := claims["name"].(string)
log.Info("Admin username from claims:", adminUsername)
isAdmin, err := gs.Db.IsSiteAdmin(adminUsername)
if err != nil {
log.Info("Error checking admin status:", err)
return c.Status(500).SendString(err.Error())
}
if !isAdmin {
log.Info("User is not a site admin:", adminUsername)
return c.Status(403).SendString("User is not a site admin")
}
// Add the user to the project with the specified role
err = gs.Db.AddUserToProject(requestData.Username, requestData.ProjectName, requestData.Role)
if err != nil {
log.Info("Error adding user to project:", err)
return c.Status(500).SendString(err.Error())
}
// Return success message
log.Info("User added to project successfully:", requestData.Username)
return c.SendStatus(fiber.StatusOK)
}

View file

@ -0,0 +1,116 @@
package handlers
import (
"strconv"
"ttime/internal/types"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
func (gs *GState) SubmitWeeklyReport(c *fiber.Ctx) error {
// Extract the necessary parameters from the token
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["name"].(string)
report := new(types.NewWeeklyReport)
if err := c.BodyParser(report); err != nil {
log.Info("Error parsing weekly report")
return c.Status(400).SendString(err.Error())
}
// Make sure all the fields of the report are valid
if report.Week < 1 || report.Week > 52 {
log.Info("Invalid week number")
return c.Status(400).SendString("Invalid week number")
}
if report.DevelopmentTime < 0 || report.MeetingTime < 0 || report.AdminTime < 0 || report.OwnWorkTime < 0 || report.StudyTime < 0 || report.TestingTime < 0 {
log.Info("Invalid time report")
return c.Status(400).SendString("Invalid time report")
}
if err := gs.Db.AddWeeklyReport(report.ProjectName, username, report.Week, report.DevelopmentTime, report.MeetingTime, report.AdminTime, report.OwnWorkTime, report.StudyTime, report.TestingTime); err != nil {
log.Info("Error adding weekly report")
return c.Status(500).SendString(err.Error())
}
log.Info("Weekly report added")
return c.Status(200).SendString("Time report added")
}
// Handler for retrieving weekly report
func (gs *GState) GetWeeklyReport(c *fiber.Ctx) error {
// Extract the necessary parameters from the request
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["name"].(string)
log.Info("Getting weekly report for: ", username)
// Extract project name and week from query parameters
projectName := c.Query("projectName")
week := c.Query("week")
if projectName == "" || week == "" {
log.Info("Missing project name or week number")
return c.Status(400).SendString("Missing project name or week number")
}
// Convert week to integer
weekInt, err := strconv.Atoi(week)
if err != nil {
log.Info("Invalid week number")
return c.Status(400).SendString("Invalid week number")
}
// Call the database function to get the weekly report
report, err := gs.Db.GetWeeklyReport(username, projectName, weekInt)
if err != nil {
log.Info("Error getting weekly report from db:", err)
return c.Status(500).SendString(err.Error())
}
log.Info("Returning weekly report")
// Return the retrieved weekly report
return c.JSON(report)
}
type ReportId struct {
ReportId int
}
func (gs *GState) SignReport(c *fiber.Ctx) error {
// Extract the necessary parameters from the token
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
projectManagerUsername := claims["name"].(string)
log.Info("Signing report for: ", projectManagerUsername)
// Extract report ID from the request query parameters
// reportID := c.Query("reportId")
rid := new(ReportId)
if err := c.BodyParser(rid); err != nil {
return err
}
log.Info("Signing report for: ", rid.ReportId)
// Get the project manager's ID
projectManagerID, err := gs.Db.GetUserId(projectManagerUsername)
if err != nil {
log.Info("Failed to get project manager ID")
return c.Status(500).SendString("Failed to get project manager ID")
}
log.Info("Project manager ID: ", projectManagerID)
// Call the database function to sign the weekly report
err = gs.Db.SignWeeklyReport(rid.ReportId, projectManagerID)
if err != nil {
log.Info("Error signing weekly report:", err)
return c.Status(500).SendString(err.Error())
}
return c.Status(200).SendString("Weekly report signed successfully")
}

View file

@ -0,0 +1,216 @@
package handlers
import (
"time"
"ttime/internal/types"
"github.com/gofiber/fiber/v2/log"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
)
// Register is a simple handler that registers a new user
//
// @Summary Register
// @Description Register a new user
// @Tags User
// @Accept json
// @Produce plain
// @Param NewUser body types.NewUser true "User to register"
// @Success 200 {string} string "User added"
// @Failure 400 {string} string "Bad request"
// @Failure 500 {string} string "Internal server error"
// @Router /register [post]
func (gs *GState) Register(c *fiber.Ctx) error {
u := new(types.NewUser)
if err := c.BodyParser(u); err != nil {
log.Warn("Error parsing body")
return c.Status(400).SendString(err.Error())
}
log.Info("Adding user:", u.Username)
if err := gs.Db.AddUser(u.Username, u.Password); err != nil {
log.Warn("Error adding user:", err)
return c.Status(500).SendString(err.Error())
}
log.Info("User added:", u.Username)
return c.Status(200).SendString("User added")
}
// This path should obviously be protected in the future
// UserDelete deletes a user from the database
//
// @Summary UserDelete
// @Description UserDelete deletes a user from the database
// @Tags User
// @Accept json
// @Produce plain
// @Success 200 {string} string "User deleted"
// @Failure 403 {string} string "You can only delete yourself"
// @Failure 500 {string} string "Internal server error"
// @Failure 401 {string} string "Unauthorized"
// @Router /userdelete/{username} [delete]
func (gs *GState) UserDelete(c *fiber.Ctx) error {
// Read from path parameters
username := c.Params("username")
// Read username from Locals
auth_username := c.Locals("user").(*jwt.Token).Claims.(jwt.MapClaims)["name"].(string)
if username != auth_username {
log.Info("User tried to delete another user")
return c.Status(403).SendString("You can only delete yourself")
}
if err := gs.Db.RemoveUser(username); err != nil {
log.Warn("Error deleting user:", err)
return c.Status(500).SendString(err.Error())
}
log.Info("User deleted:", username)
return c.Status(200).SendString("User deleted")
}
// Login is a simple login handler that returns a JWT token
//
// @Summary login
// @Description logs the user in and returns a jwt token
// @Tags User
// @Accept json
// @Param NewUser body types.NewUser true "login info"
// @Produce plain
// @Success 200 Token types.Token "Successfully signed token for user"
// @Failure 400 {string} string "Bad request"
// @Failure 401 {string} string "Unauthorized"
// @Failure 500 {string} string "Internal server error"
// @Router /login [post]
func (gs *GState) Login(c *fiber.Ctx) error {
// The body type is identical to a NewUser
u := new(types.NewUser)
if err := c.BodyParser(u); err != nil {
log.Warn("Error parsing body")
return c.Status(400).SendString(err.Error())
}
log.Info("Username logging in:", u.Username)
if !gs.Db.CheckUser(u.Username, u.Password) {
log.Info("User not found")
return c.SendStatus(fiber.StatusUnauthorized)
}
// Create the Claims
claims := jwt.MapClaims{
"name": u.Username,
"admin": false,
"exp": time.Now().Add(time.Hour * 72).Unix(),
}
// Create token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
log.Info("Token created for user:", u.Username)
// Generate encoded token and send it as response.
t, err := token.SignedString([]byte("secret"))
if err != nil {
log.Warn("Error signing token")
return c.SendStatus(fiber.StatusInternalServerError)
}
println("Successfully signed token for user:", u.Username)
return c.JSON(types.Token{Token: t})
}
// LoginRenew is a simple handler that renews the token
//
// @Summary LoginRenews
// @Description renews the users token
// @Security bererToken
// @Tags User
// @Accept json
// @Produce plain
// @Success 200 Token types.Token "Successfully signed token for user"
// @Failure 401 {string} string "Unauthorized"
// @Failure 500 {string} string "Internal server error"
// @Router /loginerenew [post]
func (gs *GState) LoginRenew(c *fiber.Ctx) error {
user := c.Locals("user").(*jwt.Token)
log.Info("Renewing token for user:", user.Claims.(jwt.MapClaims)["name"])
claims := user.Claims.(jwt.MapClaims)
claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
renewed := jwt.MapClaims{
"name": claims["name"],
"admin": claims["admin"],
"exp": claims["exp"],
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, renewed)
t, err := token.SignedString([]byte("secret"))
if err != nil {
log.Warn("Error signing token")
return c.SendStatus(fiber.StatusInternalServerError)
}
log.Info("Successfully renewed token for user:", user.Claims.(jwt.MapClaims)["name"])
return c.JSON(types.Token{Token: t})
}
// ListAllUsers is a handler that returns a list of all users in the application database
//
// @Summary ListsAllUsers
// @Description lists all users
// @Tags User
// @Accept json
// @Produce plain
// @Success 200 {json} json "Successfully signed token for user"
// @Failure 401 {string} string "Unauthorized"
// @Failure 500 {string} string "Internal server error"
// @Router /users/all [get]
func (gs *GState) ListAllUsers(c *fiber.Ctx) error {
// Get all users from the database
users, err := gs.Db.GetAllUsersApplication()
if err != nil {
log.Info("Error getting users from db:", err) // Debug print
return c.Status(500).SendString(err.Error())
}
log.Info("Returning all users")
// Return the list of users as JSON
return c.JSON(users)
}
// @Summary PromoteToAdmin
// @Description promote chosen user to admin
// @Tags User
// @Accept json
// @Produce plain
// @Param NewUser body types.NewUser true "user info"
// @Success 200 {json} json "Successfully prometed user"
// @Failure 400 {string} string "bad request"
// @Failure 401 {string} string "Unauthorized"
// @Failure 500 {string} string "Internal server error"
// @Router /promoteToAdmin [post]
func (gs *GState) PromoteToAdmin(c *fiber.Ctx) error {
// Extract the username from the request body
var newUser types.NewUser
if err := c.BodyParser(&newUser); err != nil {
return c.Status(400).SendString("Bad request")
}
username := newUser.Username
log.Info("Promoting user to admin:", username) // Debug print
// Promote the user to a site admin in the database
if err := gs.Db.PromoteToAdmin(username); err != nil {
log.Info("Error promoting user to admin:", err) // Debug print
return c.Status(500).SendString(err.Error())
}
log.Info("User promoted to admin successfully:", username) // Debug print
// Return a success message
return c.SendStatus(fiber.StatusOK)
}

View file

@ -1,42 +0,0 @@
package projects
import (
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
// AddUserToProjectHandler is a handler that adds a user to a project with a specified role
func AddUserToProjectHandler(c *fiber.Ctx) error {
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
pm_name := claims["name"].(string)
project := c.Params("projectName")
username := c.Query("userName")
// Check if the user is a project manager
isPM, err := db.GetDb(c).IsProjectManager(pm_name, project)
if err != nil {
log.Info("Error checking if user is project manager:", err)
return c.Status(500).SendString(err.Error())
}
if !isPM {
log.Info("User: ", pm_name, " is not a project manager in project: ", project)
return c.Status(403).SendString("User is not a project manager")
}
// Add the user to the project with the specified role
err = db.GetDb(c).AddUserToProject(username, project, "member")
if err != nil {
log.Info("Error adding user to project:", err)
return c.Status(500).SendString(err.Error())
}
// Return success message
log.Info("User : ", username, " added to project: ", project)
return c.SendStatus(fiber.StatusOK)
}

View file

@ -1,43 +0,0 @@
package projects
import (
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
// ChangeProjectName is a handler that changes the name of a project
func ChangeProjectName(c *fiber.Ctx) error {
//check token and get username of current user
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["name"].(string)
// Extract the necessary parameters from the request
projectName := c.Params("projectName")
newProjectName := c.Query("newProjectName")
// Check if user is site admin
issiteadmin, err := db.GetDb(c).IsSiteAdmin(username)
if err != nil {
log.Warn("Error checking if siteadmin:", err)
return c.Status(500).SendString(err.Error())
} else if !issiteadmin {
log.Warn("User is not siteadmin")
return c.Status(401).SendString("User is not siteadmin")
}
// Perform the project name change
err = db.GetDb(c).ChangeProjectName(projectName, newProjectName)
if err != nil {
log.Warn("Error changing project name:", err)
return c.Status(500).SendString(err.Error())
}
// Return a success message
return c.Status(200).SendString("Project name changed successfully")
}

View file

@ -1,30 +0,0 @@
package projects
import (
db "ttime/internal/database"
"ttime/internal/types"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
)
// CreateProject is a simple handler that creates a new project
func 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)
owner := claims["name"].(string)
if err := db.GetDb(c).AddProject(p.Name, p.Description, owner); err != nil {
return c.Status(500).SendString(err.Error())
}
return c.Status(200).SendString("Project added")
}

View file

@ -1,19 +0,0 @@
package projects
import (
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
)
func DeleteProject(c *fiber.Ctx) error {
projectID := c.Params("projectID")
username := c.Params("username")
if err := db.GetDb(c).DeleteProject(projectID, username); err != nil {
return c.Status(500).SendString((err.Error()))
}
return c.Status(200).SendString("Project deleted")
}

View file

@ -1,38 +0,0 @@
package projects
import (
"strconv"
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
)
// GetProject retrieves a specific project by its ID
func GetProject(c *fiber.Ctx) error {
// Extract the project ID from the request parameters or body
projectID := c.Params("projectID")
if projectID == "" {
log.Info("No project ID provided")
return c.Status(400).SendString("No project ID provided")
}
log.Info("Getting project with ID: ", projectID)
// Parse the project ID into an integer
projectIDInt, err := strconv.Atoi(projectID)
if err != nil {
log.Info("Invalid project ID")
return c.Status(400).SendString("Invalid project ID")
}
// Get the project from the database by its ID
project, err := db.GetDb(c).GetProject(projectIDInt)
if err != nil {
log.Info("Error getting project:", err)
return c.Status(500).SendString(err.Error())
}
// Return the project as JSON
log.Info("Returning project: ", project.Name)
return c.JSON(project)
}

View file

@ -1,63 +0,0 @@
package projects
import (
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
func GetProjectTimesHandler(c *fiber.Ctx) error {
// Get the username from the token
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["name"].(string)
// Get project
projectName := c.Params("projectName")
if projectName == "" {
log.Info("No project name provided")
return c.Status(400).SendString("No project name provided")
}
// Get all users in the project and roles
userProjects, err := db.GetDb(c).GetAllUsersProject(projectName)
if err != nil {
log.Info("Error getting users in project:", err)
return c.Status(500).SendString(err.Error())
}
// If the user is member
isMember := false
for _, userProject := range userProjects {
if userProject.Username == username {
isMember = true
break
}
}
// If the user is admin
if !isMember {
isAdmin, err := db.GetDb(c).IsSiteAdmin(username)
if err != nil {
log.Info("Error checking admin status:", err)
return c.Status(500).SendString(err.Error())
}
if !isAdmin {
log.Info("User is neither a project member nor a site admin:", username)
return c.Status(403).SendString("User is neither a project member nor a site admin")
}
}
// Get project times
projectTimes, err := db.GetDb(c).GetProjectTimes(projectName)
if err != nil {
log.Info("Error getting project times:", err)
return c.Status(500).SendString(err.Error())
}
// Return project times as JSON
log.Info("Returning project times for project:", projectName)
return c.JSON(projectTimes)
}

View file

@ -1,26 +0,0 @@
package projects
import (
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
)
// GetUserProjects returns all projects that the user is a member of
func GetUserProjects(c *fiber.Ctx) error {
username := c.Params("username")
if username == "" {
log.Info("No username provided")
return c.Status(400).SendString("No username provided")
}
// Then dip into the database to get the projects
projects, err := db.GetDb(c).GetProjectsForUser(username)
if err != nil {
return c.Status(500).SendString(err.Error())
}
// Return a json serialized list of projects
return c.JSON(projects)
}

View file

@ -1,32 +0,0 @@
package projects
import (
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
// IsProjectManagerHandler is a handler that checks if a user is a project manager for a given project
func IsProjectManagerHandler(c *fiber.Ctx) error {
// Get the username from the token
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["name"].(string)
// Extract necessary parameters from the request query string
projectName := c.Params("projectName")
log.Info("Checking if user ", username, " is a project manager for project ", projectName)
// Check if the user is a project manager for the specified project
isManager, err := db.GetDb(c).IsProjectManager(username, projectName)
if err != nil {
log.Info("Error checking project manager status:", err)
return c.Status(500).SendString(err.Error())
}
// Return the result as JSON
return c.JSON(fiber.Map{"isProjectManager": isManager})
}

View file

@ -1,55 +0,0 @@
package projects
import (
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
func ListAllUsersProject(c *fiber.Ctx) error {
// Extract the project name from the request parameters or body
projectName := c.Params("projectName")
if projectName == "" {
log.Info("No project name provided")
return c.Status(400).SendString("No project name provided")
}
// Get the user token
userToken := c.Locals("user").(*jwt.Token)
claims := userToken.Claims.(jwt.MapClaims)
username := claims["name"].(string)
// Check if the user is a project manager for the specified project
isManager, err := db.GetDb(c).IsProjectManager(username, projectName)
if err != nil {
log.Info("Error checking project manager status:", err)
return c.Status(500).SendString(err.Error())
}
// If the user is not a project manager, check if the user is a site admin
if !isManager {
isAdmin, err := db.GetDb(c).IsSiteAdmin(username)
if err != nil {
log.Info("Error checking admin status:", err)
return c.Status(500).SendString(err.Error())
}
if !isAdmin {
log.Info("User is neither a project manager nor a site admin:", username)
return c.Status(403).SendString("User is neither a project manager nor a site admin")
}
}
// Get all users associated with the project from the database
users, err := db.GetDb(c).GetAllUsersProject(projectName)
if err != nil {
log.Info("Error getting users for project:", err)
return c.Status(500).SendString(err.Error())
}
log.Info("Returning users for project: ", projectName)
// Return the list of users as JSON
return c.JSON(users)
}

View file

@ -1,51 +0,0 @@
package projects
import (
db "ttime/internal/database"
"ttime/internal/types"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
// ProjectRoleChange is a handler that changes a user's role within a project
func ProjectRoleChange(c *fiber.Ctx) error {
//check token and get username of current user
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["name"].(string)
// Extract the necessary parameters from the request
data := new(types.RoleChange)
if err := c.BodyParser(data); err != nil {
log.Info("error parsing username, project or role")
return c.Status(400).SendString(err.Error())
}
// Check if user is trying to change its own role
if username == data.UserName {
log.Info("Can't change your own role")
return c.Status(403).SendString("Can't change your own role")
}
log.Info("Changing role for user: ", data.UserName, " in project: ", data.Projectname, " to: ", data.Role)
// Dubble diping and checcking if current user is
if ismanager, err := db.GetDb(c).IsProjectManager(username, data.Projectname); err != nil {
log.Warn("Error checking if projectmanager:", err)
return c.Status(500).SendString(err.Error())
} else if !ismanager {
log.Warn("User is not projectmanager")
return c.Status(401).SendString("User is not projectmanager")
}
// Change the user's role within the project in the database
if err := db.GetDb(c).ChangeUserRole(data.UserName, data.Projectname, data.Role); err != nil {
return c.Status(500).SendString(err.Error())
}
// Return a success message
return c.SendStatus(fiber.StatusOK)
}

View file

@ -1,55 +0,0 @@
package projects
import (
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
// @Summary Promote to project manager
// @Description Promote a user to project manager
// @Tags Auth
// @Security JWT
// @Accept plain
// @Produce plain
// @Param projectName path string true "Project name"
// @Param userName query string true "User name"
// @Failure 500 {string} string "Internal server error"
// @Failure 403 {string} string "Forbidden"
// @Router /promote/{projectName} [put]
//
// Login logs in a user and returns a JWT token
// Promote to project manager
func PromoteToPm(c *fiber.Ctx) error {
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
pm_name := claims["name"].(string)
project := c.Params("projectName")
new_pm_name := c.Query("userName")
// Check if the user is a project manager
isPM, err := db.GetDb(c).IsProjectManager(pm_name, project)
if err != nil {
log.Info("Error checking if user is project manager:", err)
return c.Status(500).SendString(err.Error())
}
if !isPM {
log.Info("User: ", pm_name, " is not a project manager in project: ", project)
return c.Status(403).SendString("User is not a project manager")
}
// Add the user to the project with the specified role
err = db.GetDb(c).ChangeUserRole(new_pm_name, project, "project_manager")
if err != nil {
log.Info("Error promoting user to project manager:", err)
return c.Status(500).SendString(err.Error())
}
// Return success message
log.Info("User : ", new_pm_name, " promoted to project manager in project: ", project)
return c.SendStatus(fiber.StatusOK)
}

View file

@ -1,35 +0,0 @@
package projects
import (
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
func RemoveProject(c *fiber.Ctx) error {
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["name"].(string)
// Check if the user is a site admin
isAdmin, err := db.GetDb(c).IsSiteAdmin(username)
if err != nil {
log.Info("Error checking admin status:", err)
return c.Status(500).SendString(err.Error())
}
if !isAdmin {
log.Info("User is not a site admin:", username)
return c.Status(403).SendString("User is not a site admin")
}
projectName := c.Params("projectName")
if err := db.GetDb(c).RemoveProject(projectName); err != nil {
return c.Status(500).SendString((err.Error()))
}
return c.Status(200).SendString("Project deleted")
}

View file

@ -1,40 +0,0 @@
package projects
import (
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
func RemoveUserFromProject(c *fiber.Ctx) error {
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
pm_name := claims["name"].(string)
project := c.Params("projectName")
username := c.Query("userName")
// Check if the user is a project manager
isPM, err := db.GetDb(c).IsProjectManager(pm_name, project)
if err != nil {
log.Info("Error checking if user is project manager:", err)
return c.Status(500).SendString(err.Error())
}
if !isPM {
log.Info("User: ", pm_name, " is not a project manager in project: ", project)
return c.Status(403).SendString("User is not a project manager")
}
// Remove the user from the project
if err = db.GetDb(c).RemoveUserFromProject(username, project); err != nil {
log.Info("Error removing user from project:", err)
return c.Status(500).SendString(err.Error())
}
// Return success message
log.Info("User : ", username, " removed from project: ", project)
return c.SendStatus(fiber.StatusOK)
}

View file

@ -1,22 +0,0 @@
package reports
import (
"strconv"
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
)
func DeleteReport(c *fiber.Ctx) error {
reportID := c.Params("reportID")
reportIDInt, err := strconv.Atoi(reportID)
if err != nil {
return c.Status(400).SendString("Invalid report ID")
}
if err := db.GetDb(c).DeleteReport(reportIDInt); err != nil {
return c.Status(500).SendString((err.Error()))
}
return c.Status(200).SendString("Weekly report deleted")
}

View file

@ -1,56 +0,0 @@
package reports
import (
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
// GetAllWeeklyReports retrieves all weekly reports for a user in a specific project
func GetAllWeeklyReports(c *fiber.Ctx) error {
// Extract the necessary parameters from the token
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["name"].(string)
// Extract project name and week from query parameters
projectName := c.Params("projectName")
target_user := c.Query("targetUser") // The user whose reports are being requested
// If the target user is not empty, use it as the username
if target_user == "" {
target_user = username
}
log.Info(username, " trying to get all weekly reports for: ", target_user)
if projectName == "" {
log.Info("Missing project name")
return c.Status(400).SendString("Missing project name")
}
// If the user is not a project manager, they can only view their own reports
pm, err := db.GetDb(c).IsProjectManager(username, projectName)
if err != nil {
log.Info("Error checking if user is project manager:", err)
return c.Status(500).SendString(err.Error())
}
if !(pm || target_user == username) {
log.Info("Unauthorized access")
return c.Status(403).SendString("Unauthorized access")
}
// Retrieve weekly reports for the user in the project from the database
reports, err := db.GetDb(c).GetAllWeeklyReports(target_user, projectName)
if err != nil {
log.Error("Error getting weekly reports for user:", target_user, "in project:", projectName, ":", err)
return c.Status(500).SendString(err.Error())
}
log.Info("Returning weekly report")
// Return the retrieved weekly report
return c.JSON(reports)
}

View file

@ -1,45 +0,0 @@
package reports
import (
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
func GetUnsignedReports(c *fiber.Ctx) error {
// Extract the necessary parameters from the token
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
projectManagerUsername := claims["name"].(string)
// Extract project name and week from query parameters
projectName := c.Params("projectName")
log.Info("Getting unsigned reports for")
if projectName == "" {
log.Info("Missing project name")
return c.Status(400).SendString("Missing project name")
}
// Get the project manager's ID
isProjectManager, err := db.GetDb(c).IsProjectManager(projectManagerUsername, projectName)
if err != nil {
log.Info("Failed to get project manager ID")
return c.Status(500).SendString("Failed to get project manager ID")
}
log.Info("User is Project Manager: ", isProjectManager)
// Call the database function to get the unsigned weekly reports
reports, err := db.GetDb(c).GetUnsignedWeeklyReports(projectName)
if err != nil {
log.Info("Error getting unsigned weekly reports:", err)
return c.Status(500).SendString(err.Error())
}
log.Info("Returning unsigned reports")
// Return the list of unsigned reports
return c.JSON(reports)
}

View file

@ -1,65 +0,0 @@
package reports
import (
"strconv"
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
// Handler for retrieving weekly report
func GetWeeklyReport(c *fiber.Ctx) error {
// Extract the necessary parameters from the request
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["name"].(string)
// Extract project name and week from query parameters
projectName := c.Query("projectName")
week := c.Query("week")
target_user := c.Query("targetUser") // The user whose report is being requested
// If the target user is not empty, use it as the username
if target_user == "" {
target_user = username
}
log.Info(username, " trying to get weekly report for: ", target_user)
if projectName == "" || week == "" {
log.Info("Missing project name or week number")
return c.Status(400).SendString("Missing project name or week number")
}
// Convert week to integer
weekInt, err := strconv.Atoi(week)
if err != nil {
log.Info("Invalid week number")
return c.Status(400).SendString("Invalid week number")
}
// If the token user is not an admin, check if the target user is the same as the token user
pm, err := db.GetDb(c).IsProjectManager(username, projectName)
if err != nil {
log.Info("Error checking if user is project manager:", err)
return c.Status(500).SendString(err.Error())
}
if !(pm || target_user == username) {
log.Info("Unauthorized access")
return c.Status(403).SendString("Unauthorized access")
}
// Call the database function to get the weekly report
report, err := db.GetDb(c).GetWeeklyReport(target_user, projectName, weekInt)
if err != nil {
log.Info("Error getting weekly report from db:", err)
return c.Status(500).SendString(err.Error())
}
log.Info("Returning weekly report")
// Return the retrieved weekly report
return c.JSON(report)
}

View file

@ -1,41 +0,0 @@
package reports
import (
"strconv"
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
func SignReport(c *fiber.Ctx) error {
// Extract the necessary parameters from the token
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
projectManagerUsername := claims["name"].(string)
// Extract report ID from the path
reportId, err := strconv.Atoi(c.Params("reportId"))
if err != nil {
log.Info("Invalid report ID")
return c.Status(400).SendString("Invalid report ID")
}
// Get the project manager's ID
projectManagerID, err := db.GetDb(c).GetUserId(projectManagerUsername)
if err != nil {
log.Info("Failed to get project manager ID for user: ", projectManagerUsername)
return c.Status(500).SendString("Failed to get project manager ID")
}
// Call the database function to sign the weekly report
err = db.GetDb(c).SignWeeklyReport(reportId, projectManagerID)
if err != nil {
log.Info("Error signing weekly report:", err)
return c.Status(500).SendString(err.Error())
}
log.Info("Project manager ID: ", projectManagerID, " signed report ID: ", reportId)
return c.Status(200).SendString("Weekly report signed successfully")
}

View file

@ -1,56 +0,0 @@
package reports
import (
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
func GetStatistics(c *fiber.Ctx) error {
// Extract the necessary parameters from the token
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["name"].(string)
// Extract project name from query parameters
projectName := c.Query("projectName")
userNameParam := c.Query("userName")
log.Info(username, " trying to get statistics for project: ", projectName)
if projectName == "" {
log.Info("Missing project name")
return c.Status(400).SendString("Missing project name")
}
// Check if the user is a project manager
pm, err := db.GetDb(c).IsProjectManager(username, projectName)
if err != nil {
log.Info("Error checking if user is project manager:", err)
return c.Status(500).SendString(err.Error())
}
// Bail if the user is not a PM or checking its own statistics
if !pm && userNameParam != "" && userNameParam != username {
log.Info("Unauthorized access for user: ", username, "trying to access project: ", projectName, "statistics for user: ", userNameParam)
return c.Status(403).SendString("Unauthorized access")
}
if pm && userNameParam != "" {
username = userNameParam
}
// Retrieve statistics for the project from the database
statistics, err := db.GetDb(c).ReportStatistics(username, projectName)
if err != nil {
log.Error("Error getting statistics for project:", projectName, ":", err)
return c.Status(500).SendString(err.Error())
}
log.Info("Returning statistics")
// Return the retrieved statistics
return c.JSON(statistics)
}

View file

@ -1,41 +0,0 @@
package reports
import (
db "ttime/internal/database"
"ttime/internal/types"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
func SubmitWeeklyReport(c *fiber.Ctx) error {
// Extract the necessary parameters from the token
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["name"].(string)
report := new(types.NewWeeklyReport)
if err := c.BodyParser(report); err != nil {
log.Info("Error parsing weekly report")
return c.Status(400).SendString(err.Error())
}
// Make sure all the fields of the report are valid
if report.Week < 1 || report.Week > 52 {
log.Info("Invalid week number")
return c.Status(400).SendString("Invalid week number")
}
if report.DevelopmentTime < 0 || report.MeetingTime < 0 || report.AdminTime < 0 || report.OwnWorkTime < 0 || report.StudyTime < 0 || report.TestingTime < 0 {
log.Info("Invalid time report")
return c.Status(400).SendString("Invalid time report")
}
if err := db.GetDb(c).AddWeeklyReport(report.ProjectName, username, report.Week, report.DevelopmentTime, report.MeetingTime, report.AdminTime, report.OwnWorkTime, report.StudyTime, report.TestingTime); err != nil {
log.Info("Error adding weekly report to db:", err)
return c.Status(500).SendString(err.Error())
}
log.Info("Weekly report added")
return c.Status(200).SendString("Time report added")
}

View file

@ -1,41 +0,0 @@
package reports
import (
"strconv"
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
func UnsignReport(c *fiber.Ctx) error {
// Extract the necessary parameters from the token
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
projectManagerUsername := claims["name"].(string)
// Extract report ID from the path
reportId, err := strconv.Atoi(c.Params("reportId"))
if err != nil {
log.Info("Invalid report ID")
return c.Status(400).SendString("Invalid report ID")
}
// Get the project manager's ID
projectManagerID, err := db.GetDb(c).GetUserId(projectManagerUsername)
if err != nil {
log.Info("Failed to get project manager ID for user: ", projectManagerUsername)
return c.Status(500).SendString("Failed to get project manager ID")
}
// Call the database function to sign the weekly report
err = db.GetDb(c).UnsignWeeklyReport(reportId, projectManagerID)
if err != nil {
log.Info("Error Unsigning weekly report:", err)
return c.Status(500).SendString(err.Error())
}
log.Info("Project manager ID: ", projectManagerID, " unsigned report ID: ", reportId)
return c.Status(200).SendString("Weekly report unsigned successfully")
}

View file

@ -1,44 +0,0 @@
package reports
import (
db "ttime/internal/database"
"ttime/internal/types"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
func UpdateWeeklyReport(c *fiber.Ctx) error {
// Extract the necessary parameters from the token
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["name"].(string)
// Parse the request body into an UpdateWeeklyReport struct
var updateReport types.UpdateWeeklyReport
if err := c.BodyParser(&updateReport); err != nil {
log.Info("Error parsing weekly report")
return c.Status(400).SendString(err.Error())
}
// Make sure all the fields of the report are valid
if updateReport.Week < 1 || updateReport.Week > 52 {
log.Info("Invalid week number")
return c.Status(400).SendString("Invalid week number")
}
if updateReport.DevelopmentTime < 0 || updateReport.MeetingTime < 0 || updateReport.AdminTime < 0 || updateReport.OwnWorkTime < 0 || updateReport.StudyTime < 0 || updateReport.TestingTime < 0 {
log.Info("Invalid time report")
return c.Status(400).SendString("Invalid time report")
}
// Update the weekly report in the database
if err := db.GetDb(c).UpdateWeeklyReport(updateReport.ProjectName, username, updateReport.Week, updateReport.DevelopmentTime, updateReport.MeetingTime, updateReport.AdminTime, updateReport.OwnWorkTime, updateReport.StudyTime, updateReport.TestingTime); err != nil {
log.Info("Error updating weekly report in db:", err)
return c.Status(500).SendString(err.Error())
}
log.Info("Weekly report updated")
return c.Status(200).SendString("Weekly report updated")
}

View file

@ -1,44 +0,0 @@
package users
import (
db "ttime/internal/database"
"ttime/internal/types"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
// ChangeUserName changes a user's username in the database
func ChangeUserName(c *fiber.Ctx) error {
// Check token and get username of current user
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
adminUsername := claims["name"].(string)
log.Info(adminUsername)
// Extract the necessary parameters from the request
data := new(types.StrNameChange)
if err := c.BodyParser(data); err != nil {
log.Info("Error parsing username")
return c.Status(400).SendString(err.Error())
}
// Check if the current user is an admin
isAdmin, err := db.GetDb(c).IsSiteAdmin(adminUsername)
if err != nil {
log.Warn("Error checking if admin:", err)
return c.Status(500).SendString(err.Error())
} else if !isAdmin {
log.Warn("Tried changing name when not admin")
return c.Status(401).SendString("You cannot change name unless you are an admin")
}
// Change the user's name in the database
if err := db.GetDb(c).ChangeUserName(data.PrevName, data.NewName); err != nil {
return c.Status(500).SendString(err.Error())
}
// Return a success message
return c.SendStatus(fiber.StatusOK)
}

View file

@ -1,42 +0,0 @@
package users
import (
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
// ChangeUserPassword is a handler that changes the password of a user
func ChangeUserPassword(c *fiber.Ctx) error {
//Check token and get username of current user
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
admin := claims["name"].(string)
// Extract the necessary parameters from the request
username := c.Params("username")
newPassword := c.Query("newPassword")
// Check if user is site admin
issiteadmin, err := db.GetDb(c).IsSiteAdmin(admin)
if err != nil {
log.Warn("Error checking if siteadmin:", err)
return c.Status(500).SendString(err.Error())
} else if !issiteadmin {
log.Warn("User is not siteadmin")
return c.Status(401).SendString("User is not siteadmin")
}
// Perform the password change
err = db.GetDb(c).ChangeUserPassword(username, newPassword)
if err != nil {
log.Warn("Error changing password:", err)
return c.Status(500).SendString(err.Error())
}
// Return a success message
return c.Status(200).SendString("Password changed successfully")
}

View file

@ -1,32 +0,0 @@
package users
import (
"strconv"
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
)
// Return the username of a user given their user id
func GetUserName(c *fiber.Ctx) error {
// Check the query params for userId
user_id_string := c.Query("userId")
if user_id_string == "" {
return c.Status(400).SendString("Missing user id")
}
// Convert to int
user_id, err := strconv.Atoi(user_id_string)
if err != nil {
return c.Status(400).SendString("Invalid user id")
}
// Get the username from the database
username, err := db.GetDb(c).GetUserName(user_id)
if err != nil {
return c.Status(500).SendString(err.Error())
}
// Send the nuclear launch codes to north korea
return c.JSON(fiber.Map{"username": username})
}

View file

@ -1,22 +0,0 @@
package users
import (
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
)
func GetAllUsersProject(c *fiber.Ctx) error {
// Get all users from a project
projectName := c.Params("projectName")
users, err := db.GetDb(c).GetAllUsersProject(projectName)
if err != nil {
log.Info("Error getting users from project:", err) // Debug print
return c.Status(500).SendString(err.Error())
}
log.Info("Returning all users")
// Return the list of users as JSON
return c.JSON(users)
}

View file

@ -1,32 +0,0 @@
package users
import (
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
)
// @Summary ListsAllUsers
// @Description lists all users
// @Tags User
// @Produce json
// @Security JWT
// @Success 200 {array} string "Successfully returned all users"
// @Failure 401 {string} string "Unauthorized"
// @Failure 500 {string} string "Internal server error"
// @Router /users/all [get]
//
// ListAllUsers returns a list of all users in the application database
func ListAllUsers(c *fiber.Ctx) error {
// Get all users from the database
users, err := db.GetDb(c).GetAllUsersApplication()
if err != nil {
log.Info("Error getting users from db:", err) // Debug print
return c.Status(500).SendString(err.Error())
}
log.Info("Returning all users")
// Return the list of users as JSON
return c.JSON(users)
}

View file

@ -1,66 +0,0 @@
package users
import (
"time"
db "ttime/internal/database"
"ttime/internal/types"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
// @Summary Login
// @Description Logs in a user and returns a JWT token
// @Tags Auth
// @Accept json
// @Produce json
// @Param body body types.NewUser true "User credentials"
// @Success 200 {object} types.Token "JWT token"
// @Failure 400 {string} string "Bad request"
// @Failure 401 {string} string "Unauthorized"
// @Failure 500 {string} string "Internal server error"
// @Router /login [post]
//
// Login logs in a user and returns a JWT token
func Login(c *fiber.Ctx) error {
// The body type is identical to a NewUser
u := new(types.NewUser)
if err := c.BodyParser(u); err != nil {
log.Warn("Error parsing body")
return c.Status(400).SendString(err.Error())
}
log.Info("Username logging in:", u.Username)
if !db.GetDb(c).CheckUser(u.Username, u.Password) {
log.Info("User not found")
return c.SendStatus(fiber.StatusUnauthorized)
}
isAdmin, err := db.GetDb(c).IsSiteAdmin(u.Username)
if err != nil {
log.Info("Error checking admin status:", err)
return c.Status(500).SendString(err.Error())
}
// Create the Claims
claims := jwt.MapClaims{
"name": u.Username,
"admin": isAdmin,
"exp": time.Now().Add(time.Hour * 72).Unix(),
}
// Create token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
log.Info("Token created for user:", u.Username)
// Generate encoded token and send it as response.
t, err := token.SignedString([]byte("secret"))
if err != nil {
log.Warn("Error signing token")
return c.SendStatus(fiber.StatusInternalServerError)
}
println("Successfully signed token for user:", u.Username)
return c.JSON(types.Token{Token: t})
}

View file

@ -1,50 +0,0 @@
package users
import (
"time"
"ttime/internal/types"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
// @Summary LoginRenews
// @Description Renews the users token.
// @Tags Auth
// @Produce json
// @Security JWT
// @Success 200 {object} types.Token "Successfully signed token for user"
// @Failure 401 {string} string "Unauthorized"
// @Failure 500 {string} string "Internal server error"
// @Router /loginrenew [post]
//
// LoginRenew renews the users token
func LoginRenew(c *fiber.Ctx) error {
user := c.Locals("user").(*jwt.Token)
log.Info("Renewing token for user:", user.Claims.(jwt.MapClaims)["name"])
// Renewing the token means we trust whatever is already in the token
claims := user.Claims.(jwt.MapClaims)
// 72 hour expiration time
claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
// Create token with old claims, but new expiration time
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"name": claims["name"],
"admin": claims["admin"],
"exp": claims["exp"],
})
// Sign it with top secret key
t, err := token.SignedString([]byte("secret"))
if err != nil {
log.Warn("Error signing token")
return c.SendStatus(fiber.StatusInternalServerError) // 500
}
log.Info("Successfully renewed token for user:", user.Claims.(jwt.MapClaims)["name"])
return c.JSON(types.Token{Token: t})
}

View file

@ -1,45 +0,0 @@
package users
import (
db "ttime/internal/database"
"ttime/internal/types"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
)
// @Summary PromoteToAdmin
// @Description Promote chosen user to site admin
// @Tags User
// @Accept json
// @Produce plain
// @Security JWT
// @Param NewUser body types.NewUser true "user info"
// @Success 200 {object} types.Token "Successfully promoted user"
// @Failure 400 {string} string "Bad request"
// @Failure 401 {string} string "Unauthorized"
// @Failure 500 {string} string "Internal server error"
// @Router /promoteToAdmin [post]
//
// PromoteToAdmin promotes a user to a site admin
func PromoteToAdmin(c *fiber.Ctx) error {
// Extract the username from the request body
var newUser types.NewUser
if err := c.BodyParser(&newUser); err != nil {
return c.Status(400).SendString("Bad request")
}
username := newUser.Username
log.Info("Promoting user to admin:", username) // Debug print
// Promote the user to a site admin in the database
if err := db.GetDb(c).PromoteToAdmin(username); err != nil {
log.Info("Error promoting user to admin:", err) // Debug print
return c.Status(500).SendString(err.Error())
}
log.Info("User promoted to admin successfully:", username) // Debug print
// Return a success message
return c.SendStatus(fiber.StatusOK)
}

View file

@ -1,38 +0,0 @@
package users
import (
db "ttime/internal/database"
"ttime/internal/types"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
)
// @Summary Register
// @Description Register a new user
// @Tags Auth
// @Accept json
// @Produce plain
// @Param NewUser body types.NewUser true "User to register"
// @Success 200 {string} string "User added"
// @Failure 400 {string} string "Bad request"
// @Failure 500 {string} string "Internal server error"
// @Router /register [post]
//
// Register is a simple handler that registers a new user
func Register(c *fiber.Ctx) error {
u := new(types.NewUser)
if err := c.BodyParser(u); err != nil {
log.Warn("Error parsing body")
return c.Status(400).SendString(err.Error())
}
log.Info("Adding user:", u.Username)
if err := db.GetDb(c).AddUser(u.Username, u.Password); err != nil {
log.Warn("Error adding user:", err)
return c.Status(500).SendString(err.Error())
}
log.Info("User added:", u.Username)
return c.Status(200).SendString("User added")
}

View file

@ -1,43 +0,0 @@
package users
import (
db "ttime/internal/database"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5"
)
// @Summary UserDelete
// @Description UserDelete deletes a user from the database
// @Tags User
// @Accept json
// @Produce plain
// @Security JWT
// @Success 200 {string} string "User deleted"
// @Failure 403 {string} string "You can only delete yourself"
// @Failure 500 {string} string "Internal server error"
// @Failure 401 {string} string "Unauthorized"
// @Router /userdelete/{username} [delete]
//
// UserDelete deletes a user from the database
func UserDelete(c *fiber.Ctx) error {
// Read from path parameters
username := c.Params("username")
// Read username from Locals
auth_username := c.Locals("user").(*jwt.Token).Claims.(jwt.MapClaims)["name"].(string)
if username == auth_username {
log.Info("User tried to delete itself")
return c.Status(403).SendString("You can't delete yourself")
}
if err := db.GetDb(c).RemoveUser(username); err != nil {
log.Warn("Error deleting user:", err)
return c.Status(500).SendString(err.Error())
}
log.Info("User deleted:", username)
return c.Status(200).SendString("User deleted")
}

View file

@ -20,27 +20,6 @@ type NewWeeklyReport struct {
TestingTime int `json:"testingTime"` TestingTime int `json:"testingTime"`
} }
type WeeklyReportList struct {
// The name of the project, as it appears in the database
ProjectName string `json:"projectName" db:"project_name"`
// The week number
Week int `json:"week" db:"week"`
// Total time spent on development
DevelopmentTime int `json:"developmentTime" db:"development_time"`
// Total time spent in meetings
MeetingTime int `json:"meetingTime" db:"meeting_time"`
// Total time spent on administrative tasks
AdminTime int `json:"adminTime" db:"admin_time"`
// Total time spent on personal projects
OwnWorkTime int `json:"ownWorkTime" db:"own_work_time"`
// Total time spent on studying
StudyTime int `json:"studyTime" db:"study_time"`
// Total time spent on testing
TestingTime int `json:"testingTime" db:"testing_time"`
// The project manager who signed it
SignedBy *int `json:"signedBy" db:"signed_by"`
}
type WeeklyReport struct { type WeeklyReport struct {
// The ID of the report // The ID of the report
ReportId int `json:"reportId" db:"report_id"` ReportId int `json:"reportId" db:"report_id"`
@ -65,33 +44,3 @@ type WeeklyReport struct {
// The project manager who signed it // The project manager who signed it
SignedBy *int `json:"signedBy" db:"signed_by"` SignedBy *int `json:"signedBy" db:"signed_by"`
} }
type Statistics struct {
TotalDevelopmentTime int `json:"totalDevelopmentTime" db:"total_development_time"`
TotalMeetingTime int `json:"totalMeetingTime" db:"total_meeting_time"`
TotalAdminTime int `json:"totalAdminTime" db:"total_admin_time"`
TotalOwnWorkTime int `json:"totalOwnWorkTime" db:"total_own_work_time"`
TotalStudyTime int `json:"totalStudyTime" db:"total_study_time"`
TotalTestingTime int `json:"totalTestingTime" db:"total_testing_time"`
}
type UpdateWeeklyReport struct {
// The name of the project, as it appears in the database
ProjectName string `json:"projectName"`
// The name of the user
UserName string `json:"userName"`
// The week number
Week int `json:"week"`
// Total time spent on development
DevelopmentTime int `json:"developmentTime"`
// Total time spent in meetings
MeetingTime int `json:"meetingTime"`
// Total time spent on administrative tasks
AdminTime int `json:"adminTime"`
// Total time spent on personal projects
OwnWorkTime int `json:"ownWorkTime"`
// Total time spent on studying
StudyTime int `json:"studyTime"`
// Total time spent on testing
TestingTime int `json:"testingTime"`
}

View file

@ -13,17 +13,3 @@ type NewProject struct {
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
} }
// Used to change the role of a user in a project.
// If name is identical to the name contained in the token, the role can be changed.
// If the name is different, only a project manager can change the role.
type RoleChange struct {
UserName string `json:"username"`
Role string `json:"role" tstype:"'project_manager' | 'user'"`
Projectname string `json:"projectname"`
}
type NameChange struct {
ID int `json:"id" db:"id"`
Name string `json:"name" db:"name"`
}

View file

@ -18,8 +18,8 @@ func (u *User) ToPublicUser() (*PublicUser, error) {
// Should be used when registering, for example // Should be used when registering, for example
type NewUser struct { type NewUser struct {
Username string `json:"username" example:"username123"` Username string `json:"username"`
Password string `json:"password" example:"password123"` Password string `json:"password"`
} }
// PublicUser represents a user that is safe to send over the API (no password) // PublicUser represents a user that is safe to send over the API (no password)
@ -32,8 +32,3 @@ type PublicUser struct {
type Token struct { type Token struct {
Token string `json:"token"` Token string `json:"token"`
} }
type StrNameChange struct {
PrevName string `json:"prevName" db:"prevName"`
NewName string `json:"newName" db:"newName"`
}

View file

@ -6,9 +6,7 @@ import (
_ "ttime/docs" _ "ttime/docs"
"ttime/internal/config" "ttime/internal/config"
"ttime/internal/database" "ttime/internal/database"
"ttime/internal/handlers/projects" "ttime/internal/handlers"
"ttime/internal/handlers/reports"
"ttime/internal/handlers/users"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@ -25,10 +23,9 @@ import (
// @license.name AGPL // @license.name AGPL
// @license.url https://www.gnu.org/licenses/agpl-3.0.html // @license.url https://www.gnu.org/licenses/agpl-3.0.html
// @securityDefinitions.apikey JWT //@securityDefinitions.apikey bererToken
// @in header //@in header
// @name Authorization //@name Authorization
// @description Use the JWT token provided by the login endpoint to authenticate requests. **Prefix the token with "Bearer ".**
// @host localhost:8080 // @host localhost:8080
// @BasePath /api // @BasePath /api
@ -36,12 +33,6 @@ import (
// @externalDocs.description OpenAPI // @externalDocs.description OpenAPI
// @externalDocs.url https://swagger.io/resources/open-api/ // @externalDocs.url https://swagger.io/resources/open-api/
/**
Main function for starting the server and initializing configurations.
Reads configuration from file, pretty prints it, connects to the database,
migrates it, and sets up routes for the server.
*/
func main() { func main() {
conf, err := config.ReadConfigFromFile("config.toml") conf, err := config.ReadConfigFromFile("config.toml")
if err != nil { if err != nil {
@ -57,28 +48,24 @@ func main() {
// Connect to the database // Connect to the database
db := database.DbConnect(conf.DbPath) db := database.DbConnect(conf.DbPath)
// Migrate the database // Migrate the database
if err = database.Migrate(db); err != nil { if err = db.Migrate(); err != nil {
fmt.Println("Error migrating database: ", err) fmt.Println("Error migrating database: ", err)
os.Exit(1) os.Exit(1)
} }
// Migrate sample data, should not be used in production if err = db.MigrateSampleData(); err != nil {
if err = database.MigrateSampleData(db); err != nil {
fmt.Println("Error migrating sample data: ", err) fmt.Println("Error migrating sample data: ", err)
os.Exit(1) os.Exit(1)
} }
// Get our global state
gs := handlers.NewGlobalState(db)
// Create the server // Create the server
server := fiber.New() server := fiber.New()
// We want some logs
server.Use(logger.New()) server.Use(logger.New())
// Sets up db middleware, accessed as Local "db" key
server.Use(database.DbMiddleware(&db))
// Mounts the swagger documentation, this is available at /swagger/index.html // Mounts the swagger documentation, this is available at /swagger/index.html
server.Get("/swagger/*", swagger.HandlerDefault) server.Get("/swagger/*", swagger.HandlerDefault)
@ -86,60 +73,27 @@ func main() {
// This will likely be replaced by an embedded filesystem in the future // This will likely be replaced by an embedded filesystem in the future
server.Static("/", "./static") server.Static("/", "./static")
// Create a group for our API
api := server.Group("/api")
// Register our unprotected routes // Register our unprotected routes
api.Post("/register", users.Register) server.Post("/api/register", gs.Register)
api.Post("/login", users.Login) server.Post("/api/login", gs.Login)
// Every route from here on will require a valid // Every route from here on will require a valid JWT
// JWT bearer token authentication in the header
server.Use(jwtware.New(jwtware.Config{ server.Use(jwtware.New(jwtware.Config{
SigningKey: jwtware.SigningKey{Key: []byte("secret")}, SigningKey: jwtware.SigningKey{Key: []byte("secret")},
})) }))
// All user related routes // Protected routes (require a valid JWT bearer token authentication header)
// userGroup := api.Group("/user") // Not currently in use server.Post("/api/submitWeeklyReport", gs.SubmitWeeklyReport)
api.Get("/users/all", users.ListAllUsers) server.Get("/api/getUserProjects", gs.GetUserProjects)
api.Get("/project/getAllUsers", users.GetAllUsersProject) server.Post("/api/loginrenew", gs.LoginRenew)
api.Get("/username", users.GetUserName) server.Delete("/api/userdelete/:username", gs.UserDelete) // Perhaps just use POST to avoid headaches
api.Post("/login", users.Login) server.Post("/api/project", gs.CreateProject)
api.Post("/register", users.Register) server.Get("/api/project/:projectId", gs.GetProject)
api.Post("/loginrenew", users.LoginRenew) server.Get("/api/getWeeklyReport", gs.GetWeeklyReport)
api.Post("/promoteToAdmin", users.PromoteToAdmin) server.Post("/api/signReport", gs.SignReport)
api.Put("/changeUserName", users.ChangeUserName) server.Put("/api/addUserToProject", gs.AddUserToProjectHandler)
api.Delete("/userdelete/:username", users.UserDelete) // Perhaps just use POST to avoid headaches server.Post("/api/promoteToAdmin", gs.PromoteToAdmin)
api.Put("/changeUserPassword/:username", users.ChangeUserPassword) server.Get("/api/users/all", gs.ListAllUsers)
// All project related routes
// projectGroup := api.Group("/project") // Not currently in use
api.Get("/getProjectTimes/:projectName", projects.GetProjectTimesHandler)
api.Get("/getUserProjects/:username", projects.GetUserProjects)
api.Get("/project/:projectId", projects.GetProject)
api.Get("/checkIfProjectManager/:projectName", projects.IsProjectManagerHandler)
api.Get("/getUsersProject/:projectName", projects.ListAllUsersProject)
api.Post("/project", projects.CreateProject)
api.Post("/ProjectRoleChange", projects.ProjectRoleChange)
api.Put("/promoteToPm/:projectName", projects.PromoteToPm)
api.Put("/addUserToProject/:projectName", projects.AddUserToProjectHandler)
api.Delete("/removeUserFromProject/:projectName", projects.RemoveUserFromProject)
api.Delete("/removeProject/:projectName", projects.RemoveProject)
api.Delete("/project/:projectID", projects.DeleteProject)
api.Put("/changeProjectName/:projectName", projects.ChangeProjectName)
// All report related routes
// reportGroup := api.Group("/report") // Not currently in use
api.Get("/getWeeklyReport", reports.GetWeeklyReport)
api.Get("/getUnsignedReports/:projectName", reports.GetUnsignedReports)
api.Get("/getAllWeeklyReports/:projectName", reports.GetAllWeeklyReports)
api.Get("/getStatistics", reports.GetStatistics)
api.Post("/submitWeeklyReport", reports.SubmitWeeklyReport)
api.Put("/signReport/:reportId", reports.SignReport)
api.Put("/updateWeeklyReport", reports.UpdateWeeklyReport)
api.Put("/unsignReport/:reportId", reports.UnsignReport)
api.Delete("/deleteReport/:reportId", reports.DeleteReport)
// Announce the port we are listening on and start the server // Announce the port we are listening on and start the server
err = server.Listen(fmt.Sprintf(":%d", conf.Port)) err = server.Listen(fmt.Sprintf(":%d", conf.Port))
if err != nil { if err != nil {

View file

@ -13,6 +13,7 @@ FROM docker.io/golang:alpine as go
RUN apk add gcompat RUN apk add gcompat
RUN apk add gcc RUN apk add gcc
RUN apk add musl-dev RUN apk add musl-dev
RUN apk add make
RUN apk add sqlite RUN apk add sqlite
WORKDIR /build WORKDIR /build
ADD backend/go.mod backend/go.sum ./ ADD backend/go.mod backend/go.sum ./
@ -23,7 +24,9 @@ RUN go mod download
# Add the source code # Add the source code
ADD backend . ADD backend .
RUN go build -o server RUN make migrate
# RUN go build -o server
RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o ./server ./main.go RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o ./server ./main.go
# Strip the binary for a smaller image # Strip the binary for a smaller image
@ -34,7 +37,6 @@ FROM docker.io/alpine:latest as runner
RUN adduser -D nonroot RUN adduser -D nonroot
RUN addgroup nonroot nonroot RUN addgroup nonroot nonroot
WORKDIR /app WORKDIR /app
RUN chown nonroot:nonroot /app
# Copy the frontend SPA build into public # Copy the frontend SPA build into public
COPY --from=client /build/dist static COPY --from=client /build/dist static
@ -42,6 +44,9 @@ COPY --from=client /build/dist static
# Copy the server binary # Copy the server binary
COPY --from=go /build/server server COPY --from=go /build/server server
# Copy the database
COPY --from=go /build/db.sqlite3 db.sqlite3
# Expose port 8080 # Expose port 8080
EXPOSE 8080 EXPOSE 8080

View file

@ -1,2 +0,0 @@
goTypes.ts
GenApi.ts

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,358 +0,0 @@
/* eslint-disable */
/* tslint:disable */
/*
* ---------------------------------------------------------------
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
* ## ##
* ## AUTHOR: acacode ##
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
* ---------------------------------------------------------------
*/
export interface TypesNewUser {
/** @example "password123" */
password?: string;
/** @example "username123" */
username?: string;
}
export interface TypesToken {
token?: string;
}
export type QueryParamsType = Record<string | number, any>;
export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;
export interface FullRequestParams extends Omit<RequestInit, "body"> {
/** set parameter to `true` for call `securityWorker` for this request */
secure?: boolean;
/** request path */
path: string;
/** content type of request body */
type?: ContentType;
/** query params */
query?: QueryParamsType;
/** format of response (i.e. response.json() -> format: "json") */
format?: ResponseFormat;
/** request body */
body?: unknown;
/** base url */
baseUrl?: string;
/** request cancellation token */
cancelToken?: CancelToken;
}
export type RequestParams = Omit<FullRequestParams, "body" | "method" | "query" | "path">;
export interface ApiConfig<SecurityDataType = unknown> {
baseUrl?: string;
baseApiParams?: Omit<RequestParams, "baseUrl" | "cancelToken" | "signal">;
securityWorker?: (securityData: SecurityDataType | null) => Promise<RequestParams | void> | RequestParams | void;
customFetch?: typeof fetch;
}
export interface HttpResponse<D extends unknown, E extends unknown = unknown> extends Response {
data: D;
error: E;
}
type CancelToken = Symbol | string | number;
export enum ContentType {
Json = "application/json",
FormData = "multipart/form-data",
UrlEncoded = "application/x-www-form-urlencoded",
Text = "text/plain",
}
export class HttpClient<SecurityDataType = unknown> {
public baseUrl: string = "//localhost:8080/api";
private securityData: SecurityDataType | null = null;
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
private abortControllers = new Map<CancelToken, AbortController>();
private customFetch = (...fetchParams: Parameters<typeof fetch>) => fetch(...fetchParams);
private baseApiParams: RequestParams = {
credentials: "same-origin",
headers: {},
redirect: "follow",
referrerPolicy: "no-referrer",
};
constructor(apiConfig: ApiConfig<SecurityDataType> = {}) {
Object.assign(this, apiConfig);
}
public setSecurityData = (data: SecurityDataType | null) => {
this.securityData = data;
};
protected encodeQueryParam(key: string, value: any) {
const encodedKey = encodeURIComponent(key);
return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`;
}
protected addQueryParam(query: QueryParamsType, key: string) {
return this.encodeQueryParam(key, query[key]);
}
protected addArrayQueryParam(query: QueryParamsType, key: string) {
const value = query[key];
return value.map((v: any) => this.encodeQueryParam(key, v)).join("&");
}
protected toQueryString(rawQuery?: QueryParamsType): string {
const query = rawQuery || {};
const keys = Object.keys(query).filter((key) => "undefined" !== typeof query[key]);
return keys
.map((key) => (Array.isArray(query[key]) ? this.addArrayQueryParam(query, key) : this.addQueryParam(query, key)))
.join("&");
}
protected addQueryParams(rawQuery?: QueryParamsType): string {
const queryString = this.toQueryString(rawQuery);
return queryString ? `?${queryString}` : "";
}
private contentFormatters: Record<ContentType, (input: any) => any> = {
[ContentType.Json]: (input: any) =>
input !== null && (typeof input === "object" || typeof input === "string") ? JSON.stringify(input) : input,
[ContentType.Text]: (input: any) => (input !== null && typeof input !== "string" ? JSON.stringify(input) : input),
[ContentType.FormData]: (input: any) =>
Object.keys(input || {}).reduce((formData, key) => {
const property = input[key];
formData.append(
key,
property instanceof Blob
? property
: typeof property === "object" && property !== null
? JSON.stringify(property)
: `${property}`,
);
return formData;
}, new FormData()),
[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input),
};
protected mergeRequestParams(params1: RequestParams, params2?: RequestParams): RequestParams {
return {
...this.baseApiParams,
...params1,
...(params2 || {}),
headers: {
...(this.baseApiParams.headers || {}),
...(params1.headers || {}),
...((params2 && params2.headers) || {}),
},
};
}
protected createAbortSignal = (cancelToken: CancelToken): AbortSignal | undefined => {
if (this.abortControllers.has(cancelToken)) {
const abortController = this.abortControllers.get(cancelToken);
if (abortController) {
return abortController.signal;
}
return void 0;
}
const abortController = new AbortController();
this.abortControllers.set(cancelToken, abortController);
return abortController.signal;
};
public abortRequest = (cancelToken: CancelToken) => {
const abortController = this.abortControllers.get(cancelToken);
if (abortController) {
abortController.abort();
this.abortControllers.delete(cancelToken);
}
};
public request = async <T = any, E = any>({
body,
secure,
path,
type,
query,
format,
baseUrl,
cancelToken,
...params
}: FullRequestParams): Promise<HttpResponse<T, E>> => {
const secureParams =
((typeof secure === "boolean" ? secure : this.baseApiParams.secure) &&
this.securityWorker &&
(await this.securityWorker(this.securityData))) ||
{};
const requestParams = this.mergeRequestParams(params, secureParams);
const queryString = query && this.toQueryString(query);
const payloadFormatter = this.contentFormatters[type || ContentType.Json];
const responseFormat = format || requestParams.format;
return this.customFetch(`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`, {
...requestParams,
headers: {
...(requestParams.headers || {}),
...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}),
},
signal: (cancelToken ? this.createAbortSignal(cancelToken) : requestParams.signal) || null,
body: typeof body === "undefined" || body === null ? null : payloadFormatter(body),
}).then(async (response) => {
const r = response as HttpResponse<T, E>;
r.data = null as unknown as T;
r.error = null as unknown as E;
const data = !responseFormat
? r
: await response[responseFormat]()
.then((data) => {
if (r.ok) {
r.data = data;
} else {
r.error = data;
}
return r;
})
.catch((e) => {
r.error = e;
return r;
});
if (cancelToken) {
this.abortControllers.delete(cancelToken);
}
if (!response.ok) throw data;
return data;
});
};
}
/**
* @title TTime API
* @version 0.0.1
* @license AGPL (https://www.gnu.org/licenses/agpl-3.0.html)
* @baseUrl //localhost:8080/api
* @externalDocs https://swagger.io/resources/open-api/
* @contact
*
* This is the API for TTime, a time tracking application.
*/
export class GenApi<SecurityDataType extends unknown> extends HttpClient<SecurityDataType> {
login = {
/**
* @description Logs in a user and returns a JWT token
*
* @tags Auth
* @name LoginCreate
* @summary Login
* @request POST:/login
*/
loginCreate: (body: TypesNewUser, params: RequestParams = {}) =>
this.request<TypesToken, string>({
path: `/login`,
method: "POST",
body: body,
type: ContentType.Json,
format: "json",
...params,
}),
};
loginrenew = {
/**
* @description Renews the users token.
*
* @tags Auth
* @name LoginrenewCreate
* @summary LoginRenews
* @request POST:/loginrenew
* @secure
*/
loginrenewCreate: (params: RequestParams = {}) =>
this.request<TypesToken, string>({
path: `/loginrenew`,
method: "POST",
secure: true,
format: "json",
...params,
}),
};
promoteToAdmin = {
/**
* @description Promote chosen user to site admin
*
* @tags User
* @name PromoteToAdminCreate
* @summary PromoteToAdmin
* @request POST:/promoteToAdmin
* @secure
*/
promoteToAdminCreate: (NewUser: TypesNewUser, params: RequestParams = {}) =>
this.request<TypesToken, string>({
path: `/promoteToAdmin`,
method: "POST",
body: NewUser,
secure: true,
type: ContentType.Json,
...params,
}),
};
register = {
/**
* @description Register a new user
*
* @tags Auth
* @name RegisterCreate
* @summary Register
* @request POST:/register
*/
registerCreate: (NewUser: TypesNewUser, params: RequestParams = {}) =>
this.request<string, string>({
path: `/register`,
method: "POST",
body: NewUser,
type: ContentType.Json,
...params,
}),
};
userdelete = {
/**
* @description UserDelete deletes a user from the database
*
* @tags User
* @name UserdeleteDelete
* @summary UserDelete
* @request DELETE:/userdelete/{username}
* @secure
*/
userdeleteDelete: (username: string, params: RequestParams = {}) =>
this.request<string, string>({
path: `/userdelete/${username}`,
method: "DELETE",
secure: true,
type: ContentType.Json,
...params,
}),
};
users = {
/**
* @description lists all users
*
* @tags User
* @name GetUsers
* @summary ListsAllUsers
* @request GET:/users/all
* @secure
*/
getUsers: (params: RequestParams = {}) =>
this.request<string[], string>({
path: `/users/all`,
method: "GET",
secure: true,
format: "json",
...params,
}),
};
}

View file

@ -1,35 +0,0 @@
import { api } from "../API/API";
export interface AddMemberInfo {
userName: string;
projectName: string;
}
/**
* Tries to add a member to a project
* @param {AddMemberInfo} props.membertoAdd - Contains user's name and project's name
* @returns {Promise<void>}
*/
async function AddMember(props: { memberToAdd: AddMemberInfo }): Promise<void> {
if (props.memberToAdd.userName === "") {
alert("You must choose at least one user to add");
return;
}
try {
const response = await api.addUserToProject(
props.memberToAdd,
localStorage.getItem("accessToken") ?? "",
);
if (response.success) {
alert(`[${props.memberToAdd.userName}] added`);
} else {
alert(`[${props.memberToAdd.userName}] not added`);
console.error(response.message);
}
} catch (error) {
alert(`[${props.memberToAdd.userName}] not added`);
console.error("An error occurred during member add:", error);
}
}
export default AddMember;

View file

@ -1,100 +1,81 @@
import { useState } from "react"; import { useState } from "react";
import { api } from "../API/API"; import { APIResponse, api } from "../API/API";
import { NewProject } from "../Types/goTypes"; import { NewProject, Project } from "../Types/goTypes";
import InputField from "./InputField";
import Logo from "../assets/Logo.svg"; import Logo from "../assets/Logo.svg";
import Button from "./Button"; import Button from "./Button";
import { useNavigate } from "react-router-dom";
import ProjectNameInput from "./Inputs/ProjectNameInput";
import DescriptionInput from "./Inputs/DescriptionInput";
import { alphanumeric } from "../Data/regex";
import { projNameHighLimit, projNameLowLimit } from "../Data/constants";
/** /**
* Provides UI for adding a project to the system. * Tries to add a project to the system
* @returns {JSX.Element} - Returns the component UI for adding a project * @param props - Project name and description
* @returns {boolean} True if created, false if not
*/
function CreateProject(props: { name: string; description: string }): boolean {
const project: NewProject = {
name: props.name,
description: props.description,
};
let created = false;
api
.createProject(project, localStorage.getItem("accessToken") ?? "")
.then((response: APIResponse<Project>) => {
if (response.success) {
created = true;
} else {
console.error(response.message);
}
})
.catch((error) => {
console.error("An error occurred during creation:", error);
});
return created;
}
/**
* Tries to add a project to the system
* @returns {JSX.Element} UI for project adding
*/ */
function AddProject(): JSX.Element { function AddProject(): JSX.Element {
const [name, setName] = useState(""); const [name, setName] = useState("");
const [description, setDescription] = useState(""); const [description, setDescription] = useState("");
const navigate = useNavigate();
/**
* Tries to add a project to the system
*/
const handleCreateProject = async (): Promise<void> => {
if (
!alphanumeric.test(name) ||
name.length > projNameHighLimit ||
name.length < projNameLowLimit
) {
alert(
"Please provide valid project name: \n-Between 10-99 characters \n-No special characters (.-!?/*)",
);
return;
}
if (description.length > projNameHighLimit) {
alert("Please provide valid description: \n-Max 100 characters");
return;
}
const project: NewProject = {
name: name.replace(/ /g, ""),
description: description.trim(),
};
try {
const response = await api.createProject(
project,
localStorage.getItem("accessToken") ?? "",
);
if (response.success) {
alert(`${project.name} added!`);
setDescription("");
setName("");
navigate("/admin");
} else {
alert("Project not added, name could be taken");
console.error(response.message);
}
} catch (error) {
alert("Project not added");
console.error(error);
}
};
return ( return (
<div className="flex flex-col h-fit w-screen items-center justify-center"> <div className="flex flex-col h-fit w-screen items-center justify-center">
<div className="border-4 border-black bg-white flex flex-col items-center justify-center h-fit w-fit rounded-3xl content-center pl-20 pr-20"> <div className="border-4 border-black bg-white flex flex-col items-center justify-center h-fit w-fit rounded-3xl content-center pl-20 pr-20">
<form <form
className="bg-white rounded px-8 pt-6 pb-8 mb-4 justify-center flex flex-col w-fit h-fit" className="bg-white rounded px-8 pt-6 pb-8 mb-4 items-center justify-center flex flex-col w-fit h-fit"
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
void handleCreateProject(); CreateProject({ name: name, description: description });
}} }}
> >
<img <img
src={Logo} src={Logo}
className="logo w-[7vw] self-center mb-10 mt-10" className="logo w-[7vw] mb-10 mt-10"
alt="TTIME Logo" alt="TTIME Logo"
/> />
<h3 className="pb-4 mb-2 text-center font-bold text-[18px]"> <h3 className="pb-4 mb-2 text-center font-bold text-[18px]">
Create a new project Create a new project
</h3> </h3>
<ProjectNameInput <InputField
name={name} label="Name"
type="text"
value={name}
onChange={(e) => { onChange={(e) => {
e.preventDefault();
setName(e.target.value); setName(e.target.value);
}} }}
/> />
<div className="p-2"></div> <InputField
<DescriptionInput label="Description"
desc={description} type="text"
value={description}
onChange={(e) => { onChange={(e) => {
e.preventDefault();
setDescription(e.target.value); setDescription(e.target.value);
}} }}
placeholder={"Description (Optional)"}
/> />
<div className="flex self-center mt-4 justify-between"> <div className="flex items-center justify-between">
<Button <Button
text="Create" text="Create"
onClick={(): void => { onClick={(): void => {

View file

@ -1,125 +0,0 @@
import { useEffect, useState } from "react";
import Button from "./Button";
import AddMember, { AddMemberInfo } from "./AddMember";
import GetUsersInProject, { ProjectMember } from "./GetUsersInProject";
import GetAllUsers from "./GetAllUsers";
import InputField from "./InputField";
/**
* Provides UI for adding a member to a project.
* @returns {JSX.Element} - Returns the component UI for adding a member
*/
function AddUserToProject(props: { projectName: string }): JSX.Element {
const [names, setNames] = useState<string[]>([]);
const [users, setUsers] = useState<string[]>([]);
const [usersProj, setUsersProj] = useState<ProjectMember[]>([]);
const [search, setSearch] = useState("");
// Gets all users and project members for filtering
GetAllUsers({ setUsersProp: setUsers });
GetUsersInProject({
setUsersProp: setUsersProj,
projectName: props.projectName,
});
/*
* Filters the members from users so that users who are already
* members are not shown
*/
useEffect(() => {
setUsers((prevUsers) => {
const filteredUsers = prevUsers.filter(
(user) =>
!usersProj.some((projectUser) => projectUser.Username === user),
);
return filteredUsers;
});
}, [usersProj]);
// Attempts to add all of the selected users to the project
const handleAddClick = async (): Promise<void> => {
if (names.length === 0) {
alert("You have to choose at least one user to add");
return;
}
for (const name of names) {
const newMember: AddMemberInfo = {
userName: name,
projectName: props.projectName,
};
await AddMember({ memberToAdd: newMember });
}
setNames([]);
location.reload();
};
// Updates the names that have been selected
const handleUserClick = (user: string): void => {
setNames((prevNames): string[] => {
if (!prevNames.includes(user)) {
return [...prevNames, user];
}
return prevNames.filter((name) => name !== user);
});
};
return (
<div className="border-4 border-black bg-white flex flex-col items-center py-10 px-20 rounded-3xl content-center overflow-auto">
<h1 className="text-center font-bold text-[36px] pb-10">
{props.projectName}
</h1>
<p className="p-1 text-center font-bold text-[26px]">
Choose users to add:
</p>
<div>
<InputField
placeholder={"Search users"}
type={"Text"}
value={search}
onChange={(e) => {
setSearch(e.target.value);
}}
/>
<ul className="font-medium space-y-2 border-2 border-black mt-2 px-2 pb-2 rounded-2xl text-center overflow-auto h-[26vh] w-[34vh]">
<div></div>
{users
.filter((user) => {
return search.toLowerCase() === ""
? user
: user.toLowerCase().includes(search.toLowerCase());
})
.map((user) => (
<li
className={
names.includes(user)
? "items-start p-1 border-2 border-transparent rounded-full bg-orange-500 transition-all hover:bg-orange-600 text-white hover:cursor-pointer ring-2 ring-black"
: "items-start p-1 border-2 border-black rounded-full bg-orange-200 hover:bg-orange-400 transition-all hover:text-white hover:cursor-pointer"
}
key={user}
value={user}
onClick={() => {
handleUserClick(user);
}}
>
<span>{user}</span>
</li>
))}
</ul>
</div>
<p className="pt-10 pb-5 underline text-center font-bold text-[18px]">
Number of users to be added: {names.length}
</p>
<div className="space-x-10 items-center">
<Button
text="Add"
onClick={(): void => {
void handleAddClick();
}}
type="button"
/>
</div>
</div>
);
}
export default AddUserToProject;

View file

@ -1,71 +0,0 @@
//Info: This component is used to display all the time reports for a project. It will display the week number,
//total time spent, and if the report has been signed or not. The user can click on a report to edit it.
import { useEffect, useState } from "react";
import { WeeklyReport } from "../Types/goTypes";
import { Link, useParams } from "react-router-dom";
import { api } from "../API/API";
/**
* Renders a component that displays all the time reports for a specific project.
* @returns {JSX.Element} representing the component.
*/
function AllTimeReportsInProject(): JSX.Element {
const { projectName } = useParams();
const [weeklyReports, setWeeklyReports] = useState<WeeklyReport[]>([]);
// Call getProjects when the component mounts
useEffect(() => {
const getWeeklyReports = async (): Promise<void> => {
const token = localStorage.getItem("accessToken") ?? "";
const response = await api.getAllWeeklyReportsForUser(
projectName ?? "",
token,
);
console.log(response);
if (response.success) {
setWeeklyReports(response.data ?? []);
} else {
console.error(response.message);
}
};
void getWeeklyReports();
}, [projectName]);
return (
<>
<div className="border-4 border-black bg-white flex flex-col items-center justify-center min-h-[65vh] h-fit w-[50vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px] text-[30px]">
{weeklyReports.map((newWeeklyReport, index) => (
<Link
to={`/editTimeReport/${projectName}/${newWeeklyReport.week}/${newWeeklyReport.signedBy ? "signed" : "unsigned"}`}
key={index}
className="border-b-2 border-black w-full cursor-pointer hover:font-extrabold"
>
<div className="flex justify-between">
<h1>
<span className="font-bold">{"Week: "}</span>
{newWeeklyReport.week}
</h1>
<h1>
<span className="font-bold">{"Total Time: "}</span>
{newWeeklyReport.developmentTime +
newWeeklyReport.meetingTime +
newWeeklyReport.adminTime +
newWeeklyReport.ownWorkTime +
newWeeklyReport.studyTime +
newWeeklyReport.testingTime}{" "}
min
</h1>
<h1>
<span className="font-bold">{"Signed: "}</span>
{newWeeklyReport.signedBy ? "YES" : "NO"}
</h1>
</div>
</Link>
))}
</div>
</>
);
}
export default AllTimeReportsInProject;

View file

@ -1,73 +0,0 @@
//Info: This component is used to display all the time reports for a project. It will display the week number,
//total time spent, and if the report has been signed or not. The user can click on a report to edit it.
import { useEffect, useState } from "react";
import { WeeklyReport } from "../Types/goTypes";
import { Link, useParams } from "react-router-dom";
import { api } from "../API/API";
/**
* Renders a component that displays all the time reports for a specific project.
* @returns {JSX.Element} representing the component.
*/
function AllTimeReportsInProject(): JSX.Element {
const { username } = useParams();
const { projectName } = useParams();
const [weeklyReports, setWeeklyReports] = useState<WeeklyReport[]>([]);
useEffect(() => {
const getWeeklyReports = async (): Promise<void> => {
const token = localStorage.getItem("accessToken") ?? "";
const response = await api.getAllWeeklyReportsForUser(
projectName ?? "",
token,
username ?? "",
);
console.log(response);
if (response.success) {
setWeeklyReports(response.data ?? []);
} else {
console.error(response.message);
}
};
void getWeeklyReports();
}, [projectName, username]);
return (
<>
<h1 className="text-[30px] font-bold">{username}&apos;s Time Reports</h1>
<div className="border-4 border-black bg-white flex flex-col items-center justify-center min-h-[65vh] h-fit w-[50vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px] text-[30px]">
{weeklyReports.map((newWeeklyReport, index) => (
<Link
to={`/editOthersTR/${projectName}/${username}/${newWeeklyReport.week}/${newWeeklyReport.signedBy ? "signed" : "unsigned"}`}
key={index}
className="border-b-2 border-black w-full hover:font-extrabold"
>
<div className="flex justify-between">
<h1>
<span className="font-bold">{"Week: "}</span>
{newWeeklyReport.week}
</h1>
<h1>
<span className="font-bold">{"Total Time: "}</span>
{newWeeklyReport.developmentTime +
newWeeklyReport.meetingTime +
newWeeklyReport.adminTime +
newWeeklyReport.ownWorkTime +
newWeeklyReport.studyTime +
newWeeklyReport.testingTime}{" "}
min
</h1>
<h1>
<span className="font-bold">{"Signed: "}</span>
{newWeeklyReport.signedBy ? "YES" : "NO"}
</h1>
</div>
</Link>
))}
</div>
</>
);
}
export default AllTimeReportsInProject;

View file

@ -1,18 +0,0 @@
import { Navigate } from "react-router-dom";
import React from "react";
interface AuthorizedRouteProps {
children: React.ReactNode;
isAuthorized: boolean;
}
export function AuthorizedRoute({
children,
isAuthorized,
}: AuthorizedRouteProps): JSX.Element {
if (!isAuthorized) {
return <Navigate to="/unauthorized" />;
}
return children as React.ReactElement;
}

View file

@ -1,11 +1,5 @@
//info: Back button component to navigate back to the previous page
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
/**
* Renders a back button component.
*
* @returns The JSX element representing the back button.
*/
function BackButton(): JSX.Element { function BackButton(): JSX.Element {
const navigate = useNavigate(); const navigate = useNavigate();
const goBack = (): void => { const goBack = (): void => {

View file

@ -1,10 +1,5 @@
//info: Background animation component to animate the background of loginpage
import { useEffect } from "react"; import { useEffect } from "react";
/**
* Renders a background animation component.
* This component pre-loads images and starts a background transition animation.
*/
const BackgroundAnimation = (): JSX.Element => { const BackgroundAnimation = (): JSX.Element => {
useEffect(() => { useEffect(() => {
const images = [ const images = [

View file

@ -1,16 +1,6 @@
//info: Basic window component to display content and buttons of a page, inclduing header and footer
//content to insert is placed in the content prop, and buttons in the buttons prop
import Header from "./Header"; import Header from "./Header";
import Footer from "./Footer"; import Footer from "./Footer";
/**
* Renders a basic window component with a header, content, and footer.
*
* @param {Object} props - The component props.
* @param {React.ReactNode} props.content - The content to be rendered in the window.
* @param {React.ReactNode} props.buttons - The buttons to be rendered in the footer.
* @returns {JSX.Element} The rendered basic window component.
*/
function BasicWindow({ function BasicWindow({
content, content,
buttons, buttons,

View file

@ -1,12 +1,3 @@
/**
* Button component to display a button with text and onClick function.
*
* @param {Object} props - The component props.
* @param {string} props.text - The text to display on the button.
* @param {Function} props.onClick - The function to run when the button is clicked.
* @param {"submit" | "button" | "reset"} props.type - The type of button.
* @returns {JSX.Element} The rendered Button component.
*/
function Button({ function Button({
text, text,
onClick, onClick,

View file

@ -1,36 +0,0 @@
import { APIResponse, api } from "../API/API";
/**
* Changes the name of a project
* @param {string} props.projectName - Current project name
* @param {string} props.newProjectName - New project name
* @returns {void} - Nothing
*/
export default function ChangeProjectName(props: {
projectName: string;
newProjectName: string;
}): void {
if (props.projectName === "" || props.projectName === props.newProjectName) {
alert("You have to give a new name\n\nName not changed");
return;
}
api
.changeProjectName(
props.projectName,
props.newProjectName,
localStorage.getItem("accessToken") ?? "",
)
.then((response: APIResponse<string>) => {
if (response.success) {
alert("Name changed successfully");
location.reload();
} else {
alert("Name not changed, name could be taken");
console.error(response.message);
}
})
.catch((error) => {
alert("Name not changed");
console.error("An error occurred during change:", error);
});
}

View file

@ -1,37 +0,0 @@
import { APIResponse, api } from "../API/API";
export interface ProjectRoleChange {
username: string;
role: "project_manager" | "member" | "";
projectname: string;
}
export default function ChangeRole(roleChangeInfo: ProjectRoleChange): void {
if (
roleChangeInfo.username === "" ||
roleChangeInfo.role === "" ||
roleChangeInfo.projectname === ""
) {
// FOR DEBUG
// console.log(roleChangeInfo.role + ": Role");
// console.log(roleChangeInfo.projectname + ": P-Name");
// console.log(roleChangeInfo.username + ": U-name");
alert("You have to select a role");
return;
}
api
.changeUserRole(roleChangeInfo, localStorage.getItem("accessToken") ?? "")
.then((response: APIResponse<void>) => {
if (response.success) {
alert("Role changed successfully");
location.reload();
} else {
alert(response.message);
console.error(response.message);
}
})
.catch((error) => {
alert(error);
console.error("An error occurred during change:", error);
});
}

View file

@ -1,76 +0,0 @@
import { useState } from "react";
import Button from "./Button";
import ChangeRole, { ProjectRoleChange } from "./ChangeRole";
export default function ChangeRoleView(props: {
projectName: string;
username: string;
currentRole: string;
}): JSX.Element {
const [selectedRole, setSelectedRole] = useState<
"project_manager" | "member" | ""
>("");
const handleRoleChange = (
event: React.ChangeEvent<HTMLInputElement>,
): void => {
if (event.target.value === "member") {
setSelectedRole(event.target.value);
} else if (event.target.value === "project_manager") {
setSelectedRole(event.target.value);
}
};
const handleSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
console.log("Cur: " + props.currentRole + " " + "new: " + selectedRole);
event.preventDefault();
if (selectedRole === props.currentRole) {
alert(`Already ${props.currentRole}, nothing changed`);
return;
}
const roleChangeInfo: ProjectRoleChange = {
username: props.username,
projectname: props.projectName,
role: selectedRole,
};
ChangeRole(roleChangeInfo);
};
return (
<div className="overflow-auto">
<h1 className="font-bold text-[20px]">Select role:</h1>
<form onSubmit={handleSubmit}>
<div className="py-1 px-1 w-full self-start text-left font-medium overflow-auto border-2 border-black rounded-2xl">
<label className="hover:cursor-pointer hover:font-bold">
<input
type="radio"
value="project_manager"
checked={selectedRole === "project_manager"}
onChange={handleRoleChange}
className="m-2"
/>
Project manager
</label>
<br />
<label className="hover:cursor-pointer hover:font-bold">
<input
type="radio"
value="member"
checked={selectedRole === "member"}
onChange={handleRoleChange}
className="m-2 hover:cursor-pointer"
/>
Member
</label>
</div>
<Button
text="Change"
onClick={(): void => {
return;
}}
type="submit"
/>
</form>
</div>
);
}

View file

@ -1,83 +0,0 @@
import { useState } from "react";
import { useParams } from "react-router-dom";
import Button from "./Button";
export default function ChangeRoles(): JSX.Element {
const [selectedRole, setSelectedRole] = useState("");
const { username } = useParams();
const handleRoleChange = (
event: React.ChangeEvent<HTMLInputElement>,
): void => {
setSelectedRole(event.target.value);
};
// const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
// event.preventDefault();
// const response = await api.changeRole(username, selectedRole, token);
// if (response.success) {
// console.log("Role changed successfully");
// } else {
// console.error("Failed to change role:", response.message);
// }
// };
return (
<>
<h1 className="font-bold text-[30px] mb-[20px]">
Change roll for: {username}
</h1>
<form
className="text-[20px] font-bold border-4 border-black bg-white flex flex-col items-center justify-center min-h-[50vh] h-fit w-[30vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px]"
onSubmit={undefined}
>
<div className="self-start">
<div>
<label>
<input
type="radio"
value="System Manager"
checked={selectedRole === "System Manager"}
onChange={handleRoleChange}
className="ml-2 mr-2 mb-6"
/>
System Manager
</label>
</div>
<div>
<label>
<input
type="radio"
value="Developer"
checked={selectedRole === "Developer"}
onChange={handleRoleChange}
className="ml-2 mr-2 mb-6"
/>
Developer
</label>
</div>
<div>
<label>
<input
type="radio"
value="Tester"
checked={selectedRole === "Tester"}
onChange={handleRoleChange}
className="ml-2 mr-2 mb-6"
/>
Tester
</label>
</div>
</div>
<Button
text="Save"
onClick={(): void => {
return;
}}
type="submit"
/>
</form>
</>
);
}

View file

@ -1,36 +0,0 @@
import { APIResponse, api } from "../API/API";
/**
* Changes the password of a user
* @param {string} props.username - The username of the user
* @param {string} props.newPassword - The new password
* @returns {void} - Nothing
*/
export default function ChangeUserPassword(props: {
username: string;
newPassword: string;
}): void {
if (props.username === localStorage.getItem("username")) {
alert("You cannot change admin password");
return;
}
api
.changeUserPassword(
props.username,
props.newPassword,
localStorage.getItem("accessToken") ?? "",
)
.then((response: APIResponse<string>) => {
if (response.success) {
alert("Password changed successfully");
location.reload();
} else {
alert("Password not changed");
console.error(response.message);
}
})
.catch((error) => {
alert("Password not changed");
console.error("An error occurred during change:", error);
});
}

View file

@ -1,33 +0,0 @@
import { APIResponse, api } from "../API/API";
import { StrNameChange } from "../Types/goTypes";
function ChangeUsername(props: { nameChange: StrNameChange }): void {
if (
props.nameChange.newName === "" ||
props.nameChange.newName === props.nameChange.prevName
) {
alert("You have to give a new name\n\nName not changed");
return;
}
if (props.nameChange.prevName === localStorage.getItem("username")) {
alert("You cannot change admin name");
return;
}
api
.changeUserName(props.nameChange, localStorage.getItem("accessToken") ?? "")
.then((response: APIResponse<void>) => {
if (response.success) {
alert("Name changed successfully");
location.reload();
} else {
alert("Name not changed, name could be taken");
console.error(response.message);
}
})
.catch((error) => {
alert("Name not changed");
console.error("An error occurred during change:", error);
});
}
export default ChangeUsername;

View file

@ -0,0 +1,38 @@
import { useState, useEffect } from "react";
// Interface for the response from the server
// This should eventually reside in a dedicated file
interface CountResponse {
pressCount: number;
}
// Some constants for the button
const BUTTON_ENDPOINT = "/api/button";
// A simple button that counts how many times it's been pressed
export function CountButton(): JSX.Element {
const [count, setCount] = useState<number>(NaN);
// useEffect with a [] dependency array runs only once
useEffect(() => {
async function getCount(): Promise<void> {
const response = await fetch(BUTTON_ENDPOINT);
const data = (await response.json()) as CountResponse;
setCount(data.pressCount);
}
void getCount();
}, []);
// This is what runs on every button click
function press(): void {
async function pressPost(): Promise<void> {
const response = await fetch(BUTTON_ENDPOINT, { method: "POST" });
const data = (await response.json()) as CountResponse;
setCount(data.pressCount);
}
void pressPost();
}
// Return some JSX with the button and associated handler
return <button onClick={press}>count is {count}</button>;
}

View file

@ -1,33 +0,0 @@
import { api, APIResponse } from "../API/API";
/**
* Use to delete a project from the system
* @param {string} props.projectToDelete - The projectname of project to delete
* @returns {void} Nothing
* @example
* const exampleProjectName = "project";
* DeleteProject({ projectToDelete: exampleProjectName });
*/
function DeleteProject(props: { projectToDelete: string }): void {
api
.removeProject(
props.projectToDelete,
localStorage.getItem("accessToken") ?? "",
)
.then((response: APIResponse<string>) => {
if (response.success) {
alert("Project has been deleted!");
location.reload();
} else {
alert("Project has not been deleted");
console.error(response.message);
}
})
.catch((error) => {
alert("project has not been deleted");
console.error("An error occurred during deletion:", error);
});
}
export default DeleteProject;

View file

@ -3,7 +3,7 @@ import { api, APIResponse } from "../API/API";
/** /**
* Use to remove a user from the system * Use to remove a user from the system
* @param {string} props.usernameToDelete - The username of user to remove * @param props - The username of user to remove
* @returns {boolean} True if removed, false if not * @returns {boolean} True if removed, false if not
* @example * @example
* const exampleUsername = "user"; * const exampleUsername = "user";
@ -11,6 +11,7 @@ import { api, APIResponse } from "../API/API";
*/ */
function DeleteUser(props: { usernameToDelete: string }): boolean { function DeleteUser(props: { usernameToDelete: string }): boolean {
//console.log(props.usernameToDelete); FOR DEBUG
let removed = false; let removed = false;
api api
.removeUser( .removeUser(
@ -19,17 +20,13 @@ function DeleteUser(props: { usernameToDelete: string }): boolean {
) )
.then((response: APIResponse<User>) => { .then((response: APIResponse<User>) => {
if (response.success) { if (response.success) {
alert("User has been deleted!");
location.reload();
removed = true; removed = true;
} else { } else {
alert("User has not been deleted");
console.error(response.message); console.error(response.message);
} }
}) })
.catch((error) => { .catch((error) => {
alert("User has not been deleted"); console.error("An error occurred during creation:", error);
console.error("An error occurred during deletion:", error);
}); });
return removed; return removed;
} }

View file

@ -1,82 +0,0 @@
import { useState, useEffect } from "react";
import { Link, useParams } from "react-router-dom";
import { api } from "../API/API";
import { WeeklyReport } from "../Types/goTypes";
function DisplayUserProject(): JSX.Element {
const { projectName } = useParams();
const [unsignedReports, setUnsignedReports] = useState<WeeklyReport[]>([]);
const [usernames, setUsernames] = useState<string[]>([]);
const token = localStorage.getItem("accessToken") ?? "";
useEffect(() => {
const getUnsignedReports = async (): Promise<void> => {
const response = await api.getUnsignedReportsInProject(
projectName ?? "",
token,
);
console.log(response);
if (response.success) {
setUnsignedReports(response.data ?? []);
const usernamesPromises = (response.data ?? []).map((report) =>
api.getUsername(report.userId, token),
);
const usernamesResponses = await Promise.all(usernamesPromises);
const usernames = usernamesResponses.map(
(res) => (res.data as { username?: string }).username ?? "",
);
setUsernames(usernames);
} else {
console.error(response.message);
}
};
void getUnsignedReports();
}, [projectName, token]);
return (
<>
<h1 className="font-bold text-[30px] mb-[20px]">
All Unsigned Reports In: {projectName}{" "}
</h1>
<div className="border-4 border-black bg-white flex flex-col items-center justify-center min-h-[65vh] h-fit w-[70vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px] text-[20px]">
{unsignedReports.map((unsignedReport: WeeklyReport, index: number) => (
<h1 key={index} className="border-b-2 border-black w-full">
<div className="flex justify-between">
<div className="flex">
<span className="ml-6 mr-2 font-bold">Username:</span>
<h1>{usernames[index]}</h1>{" "}
<span className="ml-6 mr-2 font-bold">Week:</span>
<h1>{unsignedReport.week}</h1>
<span className="ml-6 mr-2 font-bold">Total Time:</span>
<h1>
{unsignedReport.developmentTime +
unsignedReport.meetingTime +
unsignedReport.adminTime +
unsignedReport.ownWorkTime +
unsignedReport.studyTime +
unsignedReport.testingTime}
</h1>
<span className="ml-6 mr-2 font-bold">Signed:</span>
<h1>NO</h1>
</div>
<div className="flex">
<div className="ml-auto flex space-x-4">
<Link
to={`/PMViewUnsignedReport/${projectName}/${usernames[index]}/${unsignedReport.week}`}
>
<h1 className="cursor-pointer font-bold hover:font-extrabold hover:underline">
View Report
</h1>
</Link>
</div>
</div>
</div>
</h1>
))}
</div>
</>
);
}
export default DisplayUserProject;

View file

@ -1,58 +0,0 @@
import { useState } from "react";
import { Project } from "../Types/goTypes";
import { useNavigate } from "react-router-dom";
import GetProjects from "./GetProjects";
import { api } from "../API/API";
/**
* Renders a component that displays the projects a user is a part of and links to the projects start-page.
* @returns The JSX element representing the component.
*/
function DisplayUserProject(): JSX.Element {
const [projects, setProjects] = useState<Project[]>([]);
const navigate = useNavigate();
GetProjects({
setProjectsProp: setProjects,
username: localStorage.getItem("username") ?? "",
});
const handleProjectClick = async (projectName: string): Promise<void> => {
const token = localStorage.getItem("accessToken") ?? "";
const response = await api.checkIfProjectManager(projectName, token);
console.log(response.data);
if (response.success) {
if (
(response.data as unknown as { isProjectManager: boolean })
.isProjectManager
) {
navigate(`/PMProjectPage/${projectName}`);
} else {
navigate(`/project/${projectName}`);
}
} else {
// handle error
console.error(response.message);
}
};
return (
<>
<h1 className="font-bold text-[30px] mb-[20px]">Your Projects</h1>
<div className="border-4 border-black bg-white flex flex-col items-center justify-center min-h-[65vh] h-fit w-[50vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px]">
{projects.map((project) => (
<div
onClick={() => void handleProjectClick(project.name)}
key={project.id}
>
<h1 className="font-bold hover:underline text-[30px] cursor-pointer hover:font-extrabold">
{project.name}
</h1>
</div>
))}
</div>
</>
);
}
export default DisplayUserProject;

View file

@ -1,14 +1,11 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { WeeklyReport, UpdateWeeklyReport } from "../Types/goTypes"; import { NewWeeklyReport } from "../Types/goTypes";
import { api } from "../API/API"; import { api } from "../API/API";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import Button from "./Button"; import Button from "./Button";
/**
* Renders the component for editing a weekly report.
* @returns JSX.Element
*/
export default function GetWeeklyReport(): JSX.Element { export default function GetWeeklyReport(): JSX.Element {
const [projectName, setProjectName] = useState("");
const [week, setWeek] = useState(0); const [week, setWeek] = useState(0);
const [developmentTime, setDevelopmentTime] = useState(0); const [developmentTime, setDevelopmentTime] = useState(0);
const [meetingTime, setMeetingTime] = useState(0); const [meetingTime, setMeetingTime] = useState(0);
@ -18,27 +15,20 @@ export default function GetWeeklyReport(): JSX.Element {
const [testingTime, setTestingTime] = useState(0); const [testingTime, setTestingTime] = useState(0);
const token = localStorage.getItem("accessToken") ?? ""; const token = localStorage.getItem("accessToken") ?? "";
const { projectName, fetchedWeek, signedOrUnsigned } = useParams<{ const username = localStorage.getItem("username") ?? "";
projectName: string;
fetchedWeek: string;
signedOrUnsigned: string;
}>();
const username = localStorage.getItem("userName") ?? "";
console.log(projectName, fetchedWeek, signedOrUnsigned);
useEffect(() => { useEffect(() => {
const fetchWeeklyReport = async (): Promise<void> => { const fetchWeeklyReport = async (): Promise<void> => {
const response = await api.getWeeklyReport( const response = await api.getWeeklyReport(
projectName ?? "", username,
fetchedWeek ?? "", projectName,
week.toString(),
token, token,
); );
if (response.success) { if (response.success) {
const report: WeeklyReport = response.data ?? { const report: NewWeeklyReport = response.data ?? {
reportId: 0, projectName: "",
userId: 0,
projectId: 0,
week: 0, week: 0,
developmentTime: 0, developmentTime: 0,
meetingTime: 0, meetingTime: 0,
@ -47,6 +37,7 @@ export default function GetWeeklyReport(): JSX.Element {
studyTime: 0, studyTime: 0,
testingTime: 0, testingTime: 0,
}; };
setProjectName(report.projectName);
setWeek(report.week); setWeek(report.week);
setDevelopmentTime(report.developmentTime); setDevelopmentTime(report.developmentTime);
setMeetingTime(report.meetingTime); setMeetingTime(report.meetingTime);
@ -60,12 +51,11 @@ export default function GetWeeklyReport(): JSX.Element {
}; };
void fetchWeeklyReport(); void fetchWeeklyReport();
}, [projectName, fetchedWeek, signedOrUnsigned, token]); }, [projectName, token, username, week]);
const handleUpdateWeeklyReport = async (): Promise<void> => { const handleNewWeeklyReport = async (): Promise<void> => {
const updateWeeklyReport: UpdateWeeklyReport = { const newWeeklyReport: NewWeeklyReport = {
userName: username, projectName,
projectName: projectName ?? "",
week, week,
developmentTime, developmentTime,
meetingTime, meetingTime,
@ -75,14 +65,13 @@ export default function GetWeeklyReport(): JSX.Element {
testingTime, testingTime,
}; };
await api.updateWeeklyReport(updateWeeklyReport, token); await api.submitWeeklyReport(newWeeklyReport, token);
}; };
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<> <>
<h1 className="font-bold text-[30px] mb-[20px]">Edit Time Report</h1>
<div className="border-4 border-black bg-white flex flex-col justify-start min-h-[65vh] h-fit w-[50vw] rounded-3xl overflow-scroll space-y-[2vh] p-[30px] items-center"> <div className="border-4 border-black bg-white flex flex-col justify-start min-h-[65vh] h-fit w-[50vw] rounded-3xl overflow-scroll space-y-[2vh] p-[30px] items-center">
<form <form
onSubmit={(e) => { onSubmit={(e) => {
@ -92,16 +81,29 @@ export default function GetWeeklyReport(): JSX.Element {
return; return;
} }
e.preventDefault(); e.preventDefault();
void handleUpdateWeeklyReport(); void handleNewWeeklyReport();
alert("Changes submitted"); navigate("/project");
navigate(-1);
}} }}
> >
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="flex flex-col w-1/2 border-b-2 border-black items-center justify-center"> <input
<h1 className="font-bold text-[30px]"> Week: {week}</h1> className="w-fill h-[5vh] font-sans text-[3vh] pl-[1vw] rounded-full text-center pt-[1vh] pb-[1vh] border-2 border-black"
</div> type="week"
placeholder="Week"
value={
week === 0 ? "" : `2024-W${week.toString().padStart(2, "0")}`
}
onChange={(e) => {
const weekNumber = parseInt(e.target.value.split("-W")[1]);
setWeek(weekNumber);
}}
onKeyDown={(event) => {
event.preventDefault();
}}
onPaste={(event) => {
event.preventDefault();
}}
/>
<table className="w-full text-center divide-y divide-x divide-white text-[30px]"> <table className="w-full text-center divide-y divide-x divide-white text-[30px]">
<thead> <thead>
<tr> <tr>
@ -121,31 +123,15 @@ export default function GetWeeklyReport(): JSX.Element {
type="number" type="number"
min="0" min="0"
className="border-2 border-black rounded-md text-center w-1/2" className="border-2 border-black rounded-md text-center w-1/2"
value={developmentTime === 0 ? "" : developmentTime} value={developmentTime}
onChange={(e) => { onChange={(e) => {
if (e.target.value === "") {
setDevelopmentTime(0);
return;
} else {
setDevelopmentTime(parseInt(e.target.value)); setDevelopmentTime(parseInt(e.target.value));
}
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
const keyValue = event.key; const keyValue = event.key;
if ( if (!/\d/.test(keyValue) && keyValue !== "Backspace")
!/\d/.test(keyValue) &&
keyValue !== "Backspace" &&
keyValue !== "ArrowLeft" &&
keyValue !== "ArrowRight"
)
event.preventDefault(); event.preventDefault();
}} }}
onClick={() => {
if (signedOrUnsigned === "signed") {
alert("You cannot edit a signed report.");
}
}}
readOnly={signedOrUnsigned === "signed"}
/> />
</td> </td>
</tr> </tr>
@ -156,31 +142,15 @@ export default function GetWeeklyReport(): JSX.Element {
type="number" type="number"
min="0" min="0"
className="border-2 border-black rounded-md text-center w-1/2" className="border-2 border-black rounded-md text-center w-1/2"
value={meetingTime === 0 ? "" : meetingTime} value={meetingTime}
onChange={(e) => { onChange={(e) => {
if (e.target.value === "") {
setMeetingTime(0);
return;
} else {
setMeetingTime(parseInt(e.target.value)); setMeetingTime(parseInt(e.target.value));
}
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
const keyValue = event.key; const keyValue = event.key;
if ( if (!/\d/.test(keyValue) && keyValue !== "Backspace")
!/\d/.test(keyValue) &&
keyValue !== "Backspace" &&
keyValue !== "ArrowLeft" &&
keyValue !== "ArrowRight"
)
event.preventDefault(); event.preventDefault();
}} }}
onClick={() => {
if (signedOrUnsigned === "signed") {
alert("You cannot edit a signed report.");
}
}}
readOnly={signedOrUnsigned === "signed"}
/> />
</td> </td>
</tr> </tr>
@ -191,31 +161,15 @@ export default function GetWeeklyReport(): JSX.Element {
type="number" type="number"
min="0" min="0"
className="border-2 border-black rounded-md text-center w-1/2" className="border-2 border-black rounded-md text-center w-1/2"
value={adminTime === 0 ? "" : adminTime} value={adminTime}
onChange={(e) => { onChange={(e) => {
if (e.target.value === "") {
setAdminTime(0);
return;
} else {
setAdminTime(parseInt(e.target.value)); setAdminTime(parseInt(e.target.value));
}
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
const keyValue = event.key; const keyValue = event.key;
if ( if (!/\d/.test(keyValue) && keyValue !== "Backspace")
!/\d/.test(keyValue) &&
keyValue !== "Backspace" &&
keyValue !== "ArrowLeft" &&
keyValue !== "ArrowRight"
)
event.preventDefault(); event.preventDefault();
}} }}
onClick={() => {
if (signedOrUnsigned === "signed") {
alert("You cannot edit a signed report.");
}
}}
readOnly={signedOrUnsigned === "signed"}
/> />
</td> </td>
</tr> </tr>
@ -226,31 +180,15 @@ export default function GetWeeklyReport(): JSX.Element {
type="number" type="number"
min="0" min="0"
className="border-2 border-black rounded-md text-center w-1/2" className="border-2 border-black rounded-md text-center w-1/2"
value={ownWorkTime === 0 ? "" : ownWorkTime} value={ownWorkTime}
onChange={(e) => { onChange={(e) => {
if (e.target.value === "") {
setOwnWorkTime(0);
return;
} else {
setOwnWorkTime(parseInt(e.target.value)); setOwnWorkTime(parseInt(e.target.value));
}
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
const keyValue = event.key; const keyValue = event.key;
if ( if (!/\d/.test(keyValue) && keyValue !== "Backspace")
!/\d/.test(keyValue) &&
keyValue !== "Backspace" &&
keyValue !== "ArrowLeft" &&
keyValue !== "ArrowRight"
)
event.preventDefault(); event.preventDefault();
}} }}
onClick={() => {
if (signedOrUnsigned === "signed") {
alert("You cannot edit a signed report.");
}
}}
readOnly={signedOrUnsigned === "signed"}
/> />
</td> </td>
</tr> </tr>
@ -261,31 +199,15 @@ export default function GetWeeklyReport(): JSX.Element {
type="number" type="number"
min="0" min="0"
className="border-2 border-black rounded-md text-center w-1/2" className="border-2 border-black rounded-md text-center w-1/2"
value={studyTime === 0 ? "" : studyTime} value={studyTime}
onChange={(e) => { onChange={(e) => {
if (e.target.value === "") {
setStudyTime(0);
return;
} else {
setStudyTime(parseInt(e.target.value)); setStudyTime(parseInt(e.target.value));
}
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
const keyValue = event.key; const keyValue = event.key;
if ( if (!/\d/.test(keyValue) && keyValue !== "Backspace")
!/\d/.test(keyValue) &&
keyValue !== "Backspace" &&
keyValue !== "ArrowLeft" &&
keyValue !== "ArrowRight"
)
event.preventDefault(); event.preventDefault();
}} }}
onClick={() => {
if (signedOrUnsigned === "signed") {
alert("You cannot edit a signed report.");
}
}}
readOnly={signedOrUnsigned === "signed"}
/> />
</td> </td>
</tr> </tr>
@ -296,45 +218,27 @@ export default function GetWeeklyReport(): JSX.Element {
type="number" type="number"
min="0" min="0"
className="border-2 border-black rounded-md text-center w-1/2" className="border-2 border-black rounded-md text-center w-1/2"
value={testingTime === 0 ? "" : testingTime} value={testingTime}
onChange={(e) => { onChange={(e) => {
if (e.target.value === "") {
setTestingTime(0);
return;
} else {
setTestingTime(parseInt(e.target.value)); setTestingTime(parseInt(e.target.value));
}
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
const keyValue = event.key; const keyValue = event.key;
if ( if (!/\d/.test(keyValue) && keyValue !== "Backspace")
!/\d/.test(keyValue) &&
keyValue !== "Backspace" &&
keyValue !== "ArrowLeft" &&
keyValue !== "ArrowRight"
)
event.preventDefault(); event.preventDefault();
}} }}
onClick={() => {
if (signedOrUnsigned === "signed") {
alert("You cannot edit a signed report.");
}
}}
readOnly={signedOrUnsigned === "signed"}
/> />
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
{signedOrUnsigned !== "signed" && (
<Button <Button
text="Submit changes" text="Submit"
onClick={(): void => { onClick={(): void => {
return; return;
}} }}
type="submit" type="submit"
/> />
)}
</div> </div>
</form> </form>
</div> </div>

View file

@ -1,13 +1,5 @@
//info: Footer component to display the footer of a page where the buttons are placed
import React from "react"; import React from "react";
/**
* Footer component.
*
* @param {Object} props - The component props.
* @param {React.ReactNode} props.children - The children elements to render inside the footer (buttons).
* @returns {JSX.Element} The rendered footer component.
*/
function Footer({ children }: { children: React.ReactNode }): JSX.Element { function Footer({ children }: { children: React.ReactNode }): JSX.Element {
return ( return (
<footer className="bg-white"> <footer className="bg-white">

View file

@ -1,35 +0,0 @@
import { Dispatch, useEffect } from "react";
import { api } from "../API/API";
/**
* Gets all usernames in the system and puts them in an array
* @param props - A setStateAction for the array you want to put users in
* @returns {void} Nothing
* @example
* const [users, setUsers] = useState<string[]>([]);
* GetAllUsers({ setUsersProp: setUsers });
*/
function GetAllUsers(props: {
setUsersProp: Dispatch<React.SetStateAction<string[]>>;
}): void {
const setUsers: Dispatch<React.SetStateAction<string[]>> = props.setUsersProp;
useEffect(() => {
const fetchUsers = async (): Promise<void> => {
try {
const token = localStorage.getItem("accessToken") ?? "";
const response = await api.getAllUsers(token);
if (response.success) {
setUsers(response.data ?? []);
} else {
console.error("Failed to fetch users:", response.message);
}
} catch (error) {
console.error("Error fetching users:", error);
}
};
void fetchUsers();
}, [setUsers]);
}
export default GetAllUsers;

View file

@ -1,59 +0,0 @@
import { Dispatch, SetStateAction, useEffect } from "react";
import { api } from "../API/API";
/**
* Interface for reported time per category + total time reported
*/
export interface projectTimes {
admin: number;
development: number;
meeting: number;
own_work: number;
study: number;
testing: number;
totalTime?: number;
}
/**
* Gets all reported times for this project
* @param {Dispatch} props.setTimesProp - A setStateAction for the map you want to put times in
* @param {string} props.projectName - Username
* @returns {void} Nothing
* @example
* const projectName = "Example";
* const [times, setTimes] = useState<Times>();
* GetProjectTimes({ setTimesProp: setTimes, projectName: projectName });
*/
function GetProjectTimes(props: {
setTimesProp: Dispatch<SetStateAction<projectTimes | undefined>>;
projectName: string;
}): void {
const setTimes: Dispatch<SetStateAction<projectTimes | undefined>> =
props.setTimesProp;
useEffect(() => {
const fetchUsers = async (): Promise<void> => {
try {
const token = localStorage.getItem("accessToken") ?? "";
const response = await api.getProjectTimes(props.projectName, token);
if (response.success && response.data) {
// Calculates total time reported
response.data.totalTime = response.data.admin;
response.data.totalTime += response.data.development;
response.data.totalTime += response.data.meeting;
response.data.totalTime += response.data.own_work;
response.data.totalTime += response.data.study;
response.data.totalTime += response.data.testing;
setTimes(response.data);
} else {
console.error("Failed to fetch project times:", response.message);
}
} catch (error) {
console.error("Error fetching times:", error);
}
};
void fetchUsers();
}, [props.projectName, setTimes]);
}
export default GetProjectTimes;

View file

@ -1,40 +0,0 @@
import { Dispatch, useEffect } from "react";
import { Project } from "../Types/goTypes";
import { api } from "../API/API";
/**
* Gets all projects that user is a member of
* @param {Dispatch} props.setProjectsProp - A setStateAction for the array you want to put projects in
* @param {string} props.username - Username
* @returns {void} Nothing
* @example
* const username = "Example";
* const [projects, setProjects] = useState<Project[]>([]);
* GetProjects({ setProjectsProp: setProjects, username: username });
*/
function GetProjects(props: {
setProjectsProp: Dispatch<React.SetStateAction<Project[]>>;
username: string;
}): void {
const setProjects: Dispatch<React.SetStateAction<Project[]>> =
props.setProjectsProp;
useEffect(() => {
const fetchUsers = async (): Promise<void> => {
try {
const token = localStorage.getItem("accessToken") ?? "";
const response = await api.getUserProjects(props.username, token);
if (response.success) {
setProjects(response.data ?? []);
} else {
console.error("Failed to fetch projects:", response.message);
}
} catch (error) {
console.error("Error fetching projects:", error);
}
};
void fetchUsers();
}, [props.username, setProjects]);
}
export default GetProjects;

View file

@ -1,42 +0,0 @@
import { Dispatch, useEffect } from "react";
import { api } from "../API/API";
export interface ProjectMember {
Username: string;
UserRole: string;
}
/**
* Gets all members of a project
* @param string - The project's name
* @param Dispatch - A setStateAction for the array you want to put members in
* @returns {void} Nothing
* @example
* const [users, setUsers] = useState<User[]>([]);
* GetUsersInProject({ projectName: props.projectname, setUsersProp: setUsers });
*/
function GetUsersInProject(props: {
projectName: string;
setUsersProp: Dispatch<React.SetStateAction<ProjectMember[]>>;
}): void {
const setUsers: Dispatch<React.SetStateAction<ProjectMember[]>> =
props.setUsersProp;
useEffect(() => {
const fetchUsers = async (): Promise<void> => {
try {
const token = localStorage.getItem("accessToken") ?? "";
const response = await api.getAllUsersProject(props.projectName, token);
if (response.success) {
setUsers(response.data ?? []);
} else {
console.error("Failed to fetch members:", response.message);
}
} catch (error) {
console.error("Error fetching members:", error);
}
};
void fetchUsers();
}, [props.projectName, setUsers]);
}
export default GetUsersInProject;

View file

@ -1,41 +1,26 @@
//info: Header component to display the header of the page including the logo and user information where thr user can logout
import { useState } from "react"; import { useState } from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link } from "react-router-dom";
import backgroundImage from "../assets/1.jpg"; import backgroundImage from "../assets/1.jpg";
/**
* Renders the header component.
* @returns JSX.Element representing the header component.
*/
function Header(): JSX.Element { function Header(): JSX.Element {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const username = localStorage.getItem("username");
const navigate = useNavigate();
const handleLogout = (): void => { const handleLogout = (): void => {
localStorage.clear(); localStorage.clear();
}; };
const handleNavigation = (): void => {
if (username === "admin") {
navigate("/admin");
} else {
navigate("/yourProjects");
}
};
return ( return (
<header <header
className="fixed top-0 left-0 right-0 border-[1.75px] border-black text-black p-3 pl-5 flex items-center justify-between bg-cover" className="fixed top-0 left-0 right-0 border-[1.75px] border-black text-black p-3 pl-5 flex items-center justify-between bg-cover"
style={{ backgroundImage: `url(${backgroundImage})` }} style={{ backgroundImage: `url(${backgroundImage})` }}
> >
<div onClick={handleNavigation}> <Link to="/your-projects">
<img <img
src="/src/assets/Logo.svg" src="/src/assets/Logo.svg"
alt="TTIME Logo" alt="TTIME Logo"
className="w-11 h-14 cursor-pointer" className="w-11 h-14 cursor-pointer"
/> />
</div> </Link>
<div <div
className="relative" className="relative"

View file

@ -4,24 +4,22 @@
* @returns {JSX.Element} The input field * @returns {JSX.Element} The input field
* @example * @example
* <InputField * <InputField
* label="Example"
* placeholder="New placeholder"
* type="text" * type="text"
* value={example} * label="Example"
* onChange={(e) => { * onChange={(e) => {
* setExample(e.target.value); * setExample(e.target.value);
* }} * }}
* value={example}
* /> * />
*/ */
function InputField(props: { function InputField(props: {
label?: string; label: string;
placeholder?: string; type: string;
type?: string; value: string;
value?: string; onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
}): JSX.Element { }): JSX.Element {
return ( return (
<div className=""> <div className="mb-4">
<label <label
className="block text-gray-700 text-sm font-sans font-bold mb-2" className="block text-gray-700 text-sm font-sans font-bold mb-2"
htmlFor={props.label} htmlFor={props.label}
@ -32,7 +30,7 @@ function InputField(props: {
className="appearance-none border-2 border-black rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" className="appearance-none border-2 border-black rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
id={props.label} id={props.label}
type={props.type} type={props.type}
placeholder={props.placeholder} placeholder={props.label}
value={props.value} value={props.value}
onChange={props.onChange} onChange={props.onChange}
/> />

View file

@ -1,38 +0,0 @@
import { projDescHighLimit, projDescLowLimit } from "../../Data/constants";
export default function DescriptionInput(props: {
desc: string;
placeholder: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}): JSX.Element {
return (
<>
<input
className={
props.desc.length <= 100
? "border-2 border-green-500 dark:border-green-500 focus-visible:border-green-500 outline-none rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight"
: "border-2 border-red-600 dark:border-red-600 focus:border-red-600 outline-none rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight"
}
spellCheck="true"
id="New desc"
type="text"
placeholder={props.placeholder}
value={props.desc}
onChange={props.onChange}
/>
<div className="my-1">
{props.desc.length > projDescHighLimit && (
<p className="text-red-600 pl-2 text-[13px] text-left">
Description must be under 100 characters
</p>
)}
{props.desc.length <= projDescHighLimit &&
props.desc.length > projDescLowLimit && (
<p className="text-green-500 pl-2 text-[13px] text-left">
Valid project description!
</p>
)}
</div>
</>
);
}

View file

@ -1,44 +0,0 @@
import { passwordLength } from "../../Data/constants";
import { lowercase } from "../../Data/regex";
export default function PasswordInput(props: {
password: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}): JSX.Element {
const password = props.password;
return (
<>
<input
className={
password.length === passwordLength && lowercase.test(password)
? "border-2 border-green-500 dark:border-green-500 focus-visible:border-green-500 outline-none rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight"
: "border-2 border-red-600 dark:border-red-600 focus:border-red-600 outline-none rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight"
}
spellCheck="false"
id="New password"
type="password"
placeholder="Password"
value={password}
onChange={props.onChange}
/>
<div className="my-1">
{password.length === passwordLength &&
lowercase.test(props.password) && (
<p className="text-green-500 pl-2 text-[13px] text-left">
Valid password!
</p>
)}
{password.length !== passwordLength && (
<p className="text-red-600 pl-2 text-[13px] text-left">
Password must be 6 characters
</p>
)}
{!lowercase.test(password) && password !== "" && (
<p className="text-red-600 pl-2 text-[13px] text-left">
No number, uppercase or special <br /> characters allowed
</p>
)}
</div>
</>
);
}

View file

@ -1,48 +0,0 @@
import { projNameHighLimit, projNameLowLimit } from "../../Data/constants";
import { alphanumeric } from "../../Data/regex";
export default function ProjectNameInput(props: {
name: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}): JSX.Element {
const name = props.name;
return (
<>
<input
className={
name.length >= projNameLowLimit &&
name.length <= projNameHighLimit &&
alphanumeric.test(name)
? "border-2 border-green-500 dark:border-green-500 focus-visible:border-green-500 outline-none rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight"
: "border-2 border-red-600 dark:border-red-600 focus:border-red-600 outline-none rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight"
}
spellCheck="false"
id="New name"
type="text"
placeholder="Project name"
value={name}
onChange={props.onChange}
/>
<div className="my-1">
{!alphanumeric.test(name) && name !== "" && (
<p className="text-red-600 pl-2 text-[13px] text-left">
No special characters allowed
</p>
)}
{(name.length < projNameLowLimit ||
name.length > projNameHighLimit) && (
<p className="text-red-600 pl-2 text-[13px] text-left">
Project name must be 10-99 characters
</p>
)}
{alphanumeric.test(props.name) &&
name.length >= projNameLowLimit &&
name.length <= projNameHighLimit && (
<p className="text-green-500 pl-2 text-[13px] text-left">
Valid project name!
</p>
)}
</div>
</>
);
}

View file

@ -1,50 +0,0 @@
import { usernameLowLimit, usernameUpLimit } from "../../Data/constants";
import { alphanumeric } from "../../Data/regex";
export default function UsernameInput(props: {
username: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}): JSX.Element {
const username = props.username;
return (
<>
<input
className={
username.length >= usernameLowLimit &&
username.length <= usernameUpLimit &&
alphanumeric.test(props.username)
? "border-2 border-green-500 dark:border-green-500 focus-visible:border-green-500 outline-none rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight"
: "border-2 border-red-600 dark:border-red-600 focus:border-red-600 outline-none rounded-2xl w-full py-2 px-3 text-gray-700 leading-tight"
}
spellCheck="false"
id="New username"
type="text"
placeholder="Username"
value={username}
onChange={props.onChange}
/>
<div className="my-1">
{alphanumeric.test(username) &&
username.length >= usernameLowLimit &&
username.length <= usernameUpLimit && (
<p className="text-green-500 pl-2 text-[13px] text-left">
Valid username!
</p>
)}
{!alphanumeric.test(username) && username !== "" && (
<p className="text-red-600 pl-2 text-[13px] text-left">
No special characters allowed
</p>
)}
{!(
username.length >= usernameLowLimit &&
username.length <= usernameUpLimit
) && (
<p className="text-red-600 pl-2 text-[13px] text-left">
Username must be 5-10 characters
</p>
)}
</div>
</>
);
}

View file

@ -11,10 +11,6 @@ function LoginCheck(props: {
password: string; password: string;
setAuthority: Dispatch<SetStateAction<number>>; setAuthority: Dispatch<SetStateAction<number>>;
}): void { }): void {
if (props.username === "" || props.password === "") {
alert("Please enter username and password to login");
return;
}
const user: NewUser = { const user: NewUser = {
username: props.username, username: props.username,
password: props.password, password: props.password,
@ -36,25 +32,22 @@ function LoginCheck(props: {
prevAuth = 1; prevAuth = 1;
return prevAuth; return prevAuth;
}); });
} else if (token !== "") { } else if (token !== "" && props.username === "pm") {
props.setAuthority((prevAuth) => { props.setAuthority((prevAuth) => {
prevAuth = 2; prevAuth = 2;
return prevAuth; return prevAuth;
}); });
} else if (token !== "" && props.username === "user") {
props.setAuthority((prevAuth) => {
prevAuth = 3;
return prevAuth;
});
} }
} else { } else {
console.error("Token was undefined"); console.error("Token was undefined");
} }
} else { } else {
if (response.data === "500") { console.error("Token could not be fetched/No such user");
console.error(response.message);
alert("No connection/Error");
} else {
console.error(
"Token could not be fetched/No such user" + response.message,
);
alert("Incorrect login information");
}
} }
}) })
.catch((error) => { .catch((error) => {

View file

@ -25,7 +25,6 @@ function Login(props: {
}): JSX.Element { }): JSX.Element {
return ( return (
<form className="flex flex-col items-center" onSubmit={props.handleSubmit}> <form className="flex flex-col items-center" onSubmit={props.handleSubmit}>
<div className="space-y-3">
<InputField <InputField
type="text" type="text"
label="Username" label="Username"
@ -33,7 +32,6 @@ function Login(props: {
props.setUsername(e.target.value); props.setUsername(e.target.value);
}} }}
value={props.username} value={props.username}
placeholder={"Username"}
/> />
<InputField <InputField
type="password" type="password"
@ -42,9 +40,7 @@ function Login(props: {
props.setPassword(e.target.value); props.setPassword(e.target.value);
}} }}
value={props.password} value={props.password}
placeholder={"Password"}
/> />
</div>
<Button <Button
text="Login" text="Login"
onClick={(): void => { onClick={(): void => {

View file

@ -1,78 +0,0 @@
import Button from "./Button";
import UserProjectListAdmin from "./UserProjectListAdmin";
import { useState } from "react";
import RemoveUserFromProj from "./RemoveUserFromProj";
import ChangeRoleInput from "./ChangeRoleView";
function MemberInfoModal(props: {
projectName: string;
username: string;
role: string;
onClose: () => void;
}): JSX.Element {
const [showRoles, setShowRoles] = useState(false);
const handleChangeRole = (): void => {
if (showRoles) {
setShowRoles(false);
} else {
setShowRoles(true);
}
};
return (
<div
className="fixed inset-0 bg-opacity-30 backdrop-blur-sm
flex justify-center items-center"
>
<div className="border-4 border-black bg-white rounded-2xl text-center flex flex-col">
<div className="mx-10">
<p className="font-bold text-[30px]">{props.username}</p>
<p className="font-bold text-[20px]">{props.role}</p>
<p
className="hover:font-bold hover:cursor-pointer underline mb-2 mt-1"
onClick={handleChangeRole}
>
(Change Role)
</p>
{showRoles && (
<ChangeRoleInput
projectName={props.projectName}
username={props.username}
currentRole={props.role}
/>
)}
<h2 className="font-bold text-[20px]">Member of these projects:</h2>
<UserProjectListAdmin username={props.username} />
<div className="items-center space-x-6">
<Button
text={"Remove"}
onClick={function (): void {
if (
window.confirm(
"Are you sure you want to remove this user from the project?",
)
) {
RemoveUserFromProj({
userToRemove: props.username,
projectName: props.projectName,
});
}
}}
type="button"
/>
<Button
text={"Close"}
onClick={function (): void {
setShowRoles(false);
props.onClose();
}}
type="button"
/>
</div>
</div>
</div>
</div>
);
}
export default MemberInfoModal;

View file

@ -1,24 +0,0 @@
import { useNavigate } from "react-router-dom";
/**
* Renders a navigation button component for navigating to
* different page
* @returns The JSX element representing the navigation button.
*/
export default function NavButton(props: {
navTo: string;
label: string;
}): JSX.Element {
const navigate = useNavigate();
const goBack = (): void => {
navigate(props.navTo);
};
return (
<button
onClick={goBack}
className="inline-block py-1 px-8 font-bold bg-orange-500 text-white border-2 border-black rounded-full cursor-pointer mt-5 mb-5 transition-colors duration-10 hover:bg-orange-600 hover:text-gray-300 font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-size: 4vh;"
>
{props.label}
</button>
);
}

View file

@ -1,85 +1,60 @@
//info: New weekly report form component to create a new weekly report to
//sumbit development time, meeting time, admin time, own work time, study time and testing time
import { useState } from "react"; import { useState } from "react";
import type { NewWeeklyReport } from "../Types/goTypes"; import type { NewWeeklyReport } from "../Types/goTypes";
import { api } from "../API/API"; import { api } from "../API/API";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import Button from "./Button"; import Button from "./Button";
/**
* Renders a form for creating a new weekly report.
* @returns The JSX element representing the new weekly report form.
*/
export default function NewWeeklyReport(): JSX.Element { export default function NewWeeklyReport(): JSX.Element {
const [week, setWeek] = useState<number>(0); const [week, setWeek] = useState<number>();
const [developmentTime, setDevelopmentTime] = useState<number>(0); const [developmentTime, setDevelopmentTime] = useState<number>();
const [meetingTime, setMeetingTime] = useState<number>(0); const [meetingTime, setMeetingTime] = useState<number>();
const [adminTime, setAdminTime] = useState<number>(0); const [adminTime, setAdminTime] = useState<number>();
const [ownWorkTime, setOwnWorkTime] = useState<number>(0); const [ownWorkTime, setOwnWorkTime] = useState<number>();
const [studyTime, setStudyTime] = useState<number>(0); const [studyTime, setStudyTime] = useState<number>();
const [testingTime, setTestingTime] = useState<number>(0); const [testingTime, setTestingTime] = useState<number>();
const { projectName } = useParams(); const { projectName } = useParams();
const token = localStorage.getItem("accessToken") ?? ""; const token = localStorage.getItem("accessToken") ?? "";
const handleNewWeeklyReport = async (): Promise<boolean> => { const handleNewWeeklyReport = async (): Promise<void> => {
const newWeeklyReport: NewWeeklyReport = { const newWeeklyReport: NewWeeklyReport = {
projectName: projectName ?? "", projectName: projectName ?? "",
week: week, week: week ?? 0,
developmentTime: developmentTime, developmentTime: developmentTime ?? 0,
meetingTime: meetingTime, meetingTime: meetingTime ?? 0,
adminTime: adminTime, adminTime: adminTime ?? 0,
ownWorkTime: ownWorkTime, ownWorkTime: ownWorkTime ?? 0,
studyTime: studyTime, studyTime: studyTime ?? 0,
testingTime: testingTime, testingTime: testingTime ?? 0,
}; };
const response = await api.submitWeeklyReport(newWeeklyReport, token); await api.submitWeeklyReport(newWeeklyReport, token);
console.log(response);
if (response.success) {
return true;
} else {
return false;
}
}; };
const navigate = useNavigate(); const navigate = useNavigate();
// Check if the browser is Chrome or Edge
const isChromeOrEdge = /Chrome|Edg/.test(navigator.userAgent);
return ( return (
<> <>
<div className="border-4 border-black bg-white flex flex-col justify-start min-h-[65vh] h-fit w-[50vw] rounded-3xl overflow-scroll space-y-[2vh] p-[30px] items-center"> <div className="border-4 border-black bg-white flex flex-col justify-start min-h-[65vh] h-fit w-[50vw] rounded-3xl overflow-scroll space-y-[2vh] p-[30px] items-center">
<form <form
onSubmit={(e) => { onSubmit={(e) => {
if (week === 0) {
alert("Please enter a week number");
e.preventDefault(); e.preventDefault();
void (async (): Promise<void> => {
if (week === 0 || week > 53 || week < 1) {
alert("Please enter a valid week number");
return; return;
} }
e.preventDefault();
const success = await handleNewWeeklyReport(); void handleNewWeeklyReport();
if (!success) { navigate("/project");
alert(
"Error occurred! Your connection to the server might be lost or a time report for this week already exists, please check your connection or go to the edit page to edit your report or change week number.",
);
return;
}
alert("Weekly report submitted successfully");
navigate(-1);
})();
}} }}
> >
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
{isChromeOrEdge ? (
<input <input
className="w-fill h-[5vh] font-sans text-[3vh] pl-[1vw] rounded-full text-center pt-[1vh] pb-[1vh] border-2 border-black" className="w-fill h-[5vh] font-sans text-[3vh] pl-[1vw] rounded-full text-center pt-[1vh] pb-[1vh] border-2 border-black"
type="week" type="week"
placeholder="Week" placeholder="Week"
onChange={(e) => { onChange={(e) => {
const weekNumber = parseInt(e.target.value.split("-W")[1]); setWeek(parseInt(e.target.value));
setWeek(weekNumber);
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
const keyValue = event.key; const keyValue = event.key;
@ -90,25 +65,6 @@ export default function NewWeeklyReport(): JSX.Element {
event.preventDefault(); event.preventDefault();
}} }}
/> />
) : (
<input
className="w-fill h-[5vh] font-sans text-[3vh] pl-[1vw] rounded-full text-center pt-[1vh] pb-[1vh] border-2 border-black"
type="text"
placeholder="Week (Numbers Only)"
onChange={(e) => {
const weekNumber = parseInt(e.target.value);
setWeek(weekNumber);
}}
onKeyDown={(event) => {
const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
event.preventDefault();
}}
onPaste={(event) => {
event.preventDefault();
}}
/>
)}
<table className="w-full text-center divide-y divide-x divide-white text-[30px]"> <table className="w-full text-center divide-y divide-x divide-white text-[30px]">
<thead> <thead>
<tr> <tr>
@ -128,23 +84,13 @@ export default function NewWeeklyReport(): JSX.Element {
type="number" type="number"
min="0" min="0"
className="border-2 border-black rounded-md text-center w-1/2" className="border-2 border-black rounded-md text-center w-1/2"
value={developmentTime === 0 ? "" : developmentTime} value={developmentTime}
onChange={(e) => { onChange={(e) => {
if (e.target.value === "") {
setDevelopmentTime(0);
return;
} else {
setDevelopmentTime(parseInt(e.target.value)); setDevelopmentTime(parseInt(e.target.value));
}
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
const keyValue = event.key; const keyValue = event.key;
if ( if (!/\d/.test(keyValue) && keyValue !== "Backspace")
!/\d/.test(keyValue) &&
keyValue !== "Backspace" &&
keyValue !== "ArrowLeft" &&
keyValue !== "ArrowRight"
)
event.preventDefault(); event.preventDefault();
}} }}
/> />
@ -157,23 +103,13 @@ export default function NewWeeklyReport(): JSX.Element {
type="number" type="number"
min="0" min="0"
className="border-2 border-black rounded-md text-center w-1/2" className="border-2 border-black rounded-md text-center w-1/2"
value={meetingTime === 0 ? "" : meetingTime} value={meetingTime}
onChange={(e) => { onChange={(e) => {
if (e.target.value === "") {
setMeetingTime(0);
return;
} else {
setMeetingTime(parseInt(e.target.value)); setMeetingTime(parseInt(e.target.value));
}
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
const keyValue = event.key; const keyValue = event.key;
if ( if (!/\d/.test(keyValue) && keyValue !== "Backspace")
!/\d/.test(keyValue) &&
keyValue !== "Backspace" &&
keyValue !== "ArrowLeft" &&
keyValue !== "ArrowRight"
)
event.preventDefault(); event.preventDefault();
}} }}
/> />
@ -186,23 +122,13 @@ export default function NewWeeklyReport(): JSX.Element {
type="number" type="number"
min="0" min="0"
className="border-2 border-black rounded-md text-center w-1/2" className="border-2 border-black rounded-md text-center w-1/2"
value={adminTime === 0 ? "" : adminTime} value={adminTime}
onChange={(e) => { onChange={(e) => {
if (e.target.value === "") {
setAdminTime(0);
return;
} else {
setAdminTime(parseInt(e.target.value)); setAdminTime(parseInt(e.target.value));
}
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
const keyValue = event.key; const keyValue = event.key;
if ( if (!/\d/.test(keyValue) && keyValue !== "Backspace")
!/\d/.test(keyValue) &&
keyValue !== "Backspace" &&
keyValue !== "ArrowLeft" &&
keyValue !== "ArrowRight"
)
event.preventDefault(); event.preventDefault();
}} }}
/> />
@ -215,23 +141,13 @@ export default function NewWeeklyReport(): JSX.Element {
type="number" type="number"
min="0" min="0"
className="border-2 border-black rounded-md text-center w-1/2" className="border-2 border-black rounded-md text-center w-1/2"
value={ownWorkTime === 0 ? "" : ownWorkTime} value={ownWorkTime}
onChange={(e) => { onChange={(e) => {
if (e.target.value === "") {
setOwnWorkTime(0);
return;
} else {
setOwnWorkTime(parseInt(e.target.value)); setOwnWorkTime(parseInt(e.target.value));
}
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
const keyValue = event.key; const keyValue = event.key;
if ( if (!/\d/.test(keyValue) && keyValue !== "Backspace")
!/\d/.test(keyValue) &&
keyValue !== "Backspace" &&
keyValue !== "ArrowLeft" &&
keyValue !== "ArrowRight"
)
event.preventDefault(); event.preventDefault();
}} }}
/> />
@ -244,23 +160,13 @@ export default function NewWeeklyReport(): JSX.Element {
type="number" type="number"
min="0" min="0"
className="border-2 border-black rounded-md text-center w-1/2" className="border-2 border-black rounded-md text-center w-1/2"
value={studyTime === 0 ? "" : studyTime} value={studyTime}
onChange={(e) => { onChange={(e) => {
if (e.target.value === "") {
setStudyTime(0);
return;
} else {
setStudyTime(parseInt(e.target.value)); setStudyTime(parseInt(e.target.value));
}
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
const keyValue = event.key; const keyValue = event.key;
if ( if (!/\d/.test(keyValue) && keyValue !== "Backspace")
!/\d/.test(keyValue) &&
keyValue !== "Backspace" &&
keyValue !== "ArrowLeft" &&
keyValue !== "ArrowRight"
)
event.preventDefault(); event.preventDefault();
}} }}
/> />
@ -273,23 +179,13 @@ export default function NewWeeklyReport(): JSX.Element {
type="number" type="number"
min="0" min="0"
className="border-2 border-black rounded-md text-center w-1/2" className="border-2 border-black rounded-md text-center w-1/2"
value={testingTime === 0 ? "" : testingTime} value={testingTime}
onChange={(e) => { onChange={(e) => {
if (e.target.value === "") {
setTestingTime(0);
return;
} else {
setTestingTime(parseInt(e.target.value)); setTestingTime(parseInt(e.target.value));
}
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
const keyValue = event.key; const keyValue = event.key;
if ( if (!/\d/.test(keyValue) && keyValue !== "Backspace")
!/\d/.test(keyValue) &&
keyValue !== "Backspace" &&
keyValue !== "ArrowLeft" &&
keyValue !== "ArrowRight"
)
event.preventDefault(); event.preventDefault();
}} }}
/> />

View file

@ -1,228 +0,0 @@
import { useState, useEffect } from "react";
import { WeeklyReport } from "../Types/goTypes";
import { api } from "../API/API";
import { useParams, useNavigate } from "react-router-dom";
import Button from "./Button";
/**
* Renders the component for editing a weekly report.
* @returns JSX.Element
*/
//This component does not yet work as intended. It is supposed to display the weekly report of a user in a project.
export default function OtherUsersTR(): JSX.Element {
const [week, setWeek] = useState(0);
const [developmentTime, setDevelopmentTime] = useState(0);
const [meetingTime, setMeetingTime] = useState(0);
const [adminTime, setAdminTime] = useState(0);
const [ownWorkTime, setOwnWorkTime] = useState(0);
const [studyTime, setStudyTime] = useState(0);
const [testingTime, setTestingTime] = useState(0);
const [reportId, setReportId] = useState(0);
const token = localStorage.getItem("accessToken") ?? "";
const { projectName } = useParams();
const { username } = useParams();
const { fetchedWeek } = useParams();
const { signedOrUnsigned } = useParams();
console.log(projectName, username, fetchedWeek, signedOrUnsigned);
useEffect(() => {
const fetchUsersWeeklyReport = async (): Promise<void> => {
const response = await api.getWeeklyReport(
projectName ?? "",
fetchedWeek?.toString() ?? "0",
token,
username ?? "",
);
if (response.success) {
const report: WeeklyReport = response.data ?? {
reportId: 0,
userId: 0,
projectId: 0,
week: 0,
developmentTime: 0,
meetingTime: 0,
adminTime: 0,
ownWorkTime: 0,
studyTime: 0,
testingTime: 0,
};
setReportId(report.reportId);
setWeek(report.week);
setDevelopmentTime(report.developmentTime);
setMeetingTime(report.meetingTime);
setAdminTime(report.adminTime);
setOwnWorkTime(report.ownWorkTime);
setStudyTime(report.studyTime);
setTestingTime(report.testingTime);
} else {
console.error("Failed to fetch weekly report:", response.message);
}
};
void fetchUsersWeeklyReport();
});
const handleUnsignWeeklyReport = async (): Promise<boolean> => {
const response = await api.unsignReport(reportId, token);
console.log(response);
console.log(reportId);
if (response.success) {
return true;
} else {
return false;
}
};
const handleDeleteWeeklyReport = async (): Promise<boolean> => {
const response = await api.deleteWeeklyReport(reportId, token);
console.log(response);
if (response.success) {
return true;
}
return false;
};
const navigate = useNavigate();
return (
<>
<h1 className="text-[30px] font-bold">{username}&apos;s Report</h1>
<div className="border-4 border-black bg-white flex flex-col justify-start min-h-[65vh] h-fit w-[50vw] rounded-3xl overflow-scroll space-y-[2vh] p-[30px] items-center">
<div className="flex flex-col items-center">
<div className="flex flex-col w-1/2 border-b-2 border-black items-center justify-center">
<h1 className="font-bold text-[30px]"> Week: {week}</h1>
</div>
<table className="w-full text-center divide-y divide-x divide-white text-[30px]">
<thead>
<tr>
<th className="w-1/2 py-2 border-b-2 border-black">Activity</th>
<th className="w-1/2 py-2 border-b-2 border-black">
Total Time (min)
</th>
</tr>
</thead>
<tbody className="divide-y divide-black">
<tr className="h-[10vh]">
<td>Development</td>
<td>
<input
type="text"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={developmentTime === 0 ? "" : developmentTime}
readOnly
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Meeting</td>
<td>
<input
type="text"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={meetingTime === 0 ? "" : meetingTime}
readOnly
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Administration</td>
<td>
<input
type="text"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={adminTime === 0 ? "" : adminTime}
readOnly
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Own Work</td>
<td>
<input
type="text"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={ownWorkTime === 0 ? "" : ownWorkTime}
readOnly
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Studies</td>
<td>
<input
type="text"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={studyTime === 0 ? "" : studyTime}
readOnly
/>
</td>
</tr>
<tr className="h-[10vh]">
<td>Testing</td>
<td>
<input
type="text"
min="0"
className="border-2 border-black rounded-md text-center w-1/2"
value={testingTime === 0 ? "" : testingTime}
readOnly
/>
</td>
</tr>
</tbody>
</table>
<div className="flex space-x-4">
{signedOrUnsigned === "signed" && (
<Button
text="Unsign Report"
onClick={(): void => {
void (async (): Promise<void> => {
const success = await handleUnsignWeeklyReport();
if (success) {
alert("Report successfully unsigned!");
navigate(-1);
} else {
alert("Failed to unsign report");
return;
}
})();
}}
type={"button"}
/>
)}
<Button
text="Delete Time Report"
onClick={(): void => {
void (async (): Promise<void> => {
const confirmDelete = window.confirm(
"Are you sure you want to delete this report? This action cannot be undone.",
);
if (!confirmDelete) {
return;
}
const success = await handleDeleteWeeklyReport();
if (success) {
alert("Report successfully deleted!");
navigate(-1);
} else {
alert("Failed to delete report");
return;
}
})();
}}
type={"button"}
/>
</div>
</div>
</div>
</>
);
}

View file

@ -1,34 +0,0 @@
import { Link, useParams } from "react-router-dom";
import { JSX } from "react/jsx-runtime";
function PMProjectMenu(): JSX.Element {
const { projectName } = useParams();
return (
<>
<h1 className="font-bold text-[30px] mb-[20px]">{projectName}</h1>
<div className="border-4 border-black bg-white flex flex-col items-center justify-center min-h-[65vh] h-fit w-[50vw] rounded-3xl content-center overflow-scroll space-y-[5vh] p-[30px]">
<Link to={`/timeReports/${projectName}/`}>
<h1 className="font-bold hover:underline text-[30px] cursor-pointer hover:font-extrabold">
Your Time Reports
</h1>
</Link>
<Link to={`/newTimeReport/${projectName}`}>
<h1 className="font-bold hover:underline text-[30px] cursor-pointer hover:font-extrabold">
New Time Report
</h1>
</Link>
<Link to={`/projectMembers/${projectName}`}>
<h1 className="font-bold hover:underline text-[30px] cursor-pointer hover:font-extrabold">
Statistics
</h1>
</Link>
<Link to={`/unsignedReports/${projectName}`}>
<h1 className="font-bold hover:underline text-[30px] cursor-pointer hover:font-extrabold">
Unsigned Time Reports
</h1>
</Link>
</div>
</>
);
}
export default PMProjectMenu;

Some files were not shown because too many files have changed in this diff Show more