This commit is contained in:
borean 2024-03-07 20:58:50 +01:00
parent ae9ee91bc4
commit 7ed986e4eb
21 changed files with 547 additions and 93 deletions

View file

@ -11,7 +11,7 @@ start-release: build-container-release remove-podman-containers
# Removes and stops any containers related to the project # Removes and stops any containers related to the project
[private] [private]
remove-podman-containers: remove-podman-containers:
podman container rm -f ttime podman container rm -fi ttime
# 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
@ -23,14 +23,14 @@ load-release file:
# Tests every part of the project # Tests every part of the project
testall: testall:
cd backend && make test
cd backend && make lint
cd frontend && npm test cd frontend && npm test
cd frontend && npm run lint cd frontend && npm run lint
cd backend && make test
cd backend && make lint
# Cleans up everything related to the project # Cleans up everything related to the project
clean: remove-podman-containers clean: remove-podman-containers
podman image rm -f ttime-server podman image rm -fi ttime-server
rm -rf frontend/dist rm -rf frontend/dist
rm -rf frontend/node_modules rm -rf frontend/node_modules
rm -f ttime-server.tar.gz rm -f ttime-server.tar.gz
@ -41,3 +41,6 @@ clean: remove-podman-containers
[confirm] [confirm]
podman-clean: podman-clean:
podman system reset --force podman system reset --force
install-linter:
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.56.2

41
Makefile Normal file
View file

@ -0,0 +1,41 @@
# Builds a release container
build-container-release:
podman build -t ttime-server -f container/Containerfile .
# Builds a release container and runs it
start-release: build-container-release remove-podman-containers
podman run -d -e DATABASE_URL=sqlite:release.db -p 8080:8080 --name ttime ttime-server
@echo "Started production ttime-server on http://localhost:8080"
# Removes and stops any containers related to the project
remove-podman-containers:
podman container rm -fi ttime
# Tests every part of the project
testall:
cd frontend && npm test
cd frontend && npm run lint
cd backend && make test
cd backend && make lint
# Cleans up everything related to the project
clean: remove-podman-containers
podman image rm -fi ttime-server
rm -rf frontend/dist
rm -rf frontend/node_modules
rm -f ttime-server.tar.gz
cd backend && make clean
@echo "Cleaned up!"
# Cleans up everything related to podman, not just the project. Make sure you understand what this means.
podman-clean:
podman system reset --force
# Installs the linter, which is not included in the ubuntu repo
install-linter:
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.56.2
# This installs just, a make alternative, which is slightly more ergonomic to use
install-just:
@echo "Installing just"
@curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin

View file

@ -59,7 +59,7 @@ My recommendation would be to make WSL your primary development environment if y
You should consult the [WSL documentation](https://docs.microsoft.com/en-us/windows/wsl/install), but for any recent version of windows, installation essentially boils down to running the following command in **PowerShell as an administrator**: You should consult the [WSL documentation](https://docs.microsoft.com/en-us/windows/wsl/install), but for any recent version of windows, installation essentially boils down to running the following command in **PowerShell as an administrator**:
```powershell ```powershell
wsl --install wsl --install -d Ubuntu-22.04 # To get a somewhat recent version of Go
``` ```
If you get any errors related to virtualization, you will need to enable virtualization in the BIOS. This is a common issue, and you can find a guide for your specific motherboard online. This is a one-time operation and will not affect your windows installation. This setting is usually called "VT-x" or "AMD-V" and is usually found in the CPU settings. If you can't find it, shoot me a message and I'll find it for you. If you get any errors related to virtualization, you will need to enable virtualization in the BIOS. This is a common issue, and you can find a guide for your specific motherboard online. This is a one-time operation and will not affect your windows installation. This setting is usually called "VT-x" or "AMD-V" and is usually found in the CPU settings. If you can't find it, shoot me a message and I'll find it for you.

View file

@ -69,3 +69,7 @@ lint:
# Default target # Default target
default: build default: build
install-just:
@echo "Installing just"
@curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin

View file

@ -9,6 +9,8 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
jwtware "github.com/gofiber/contrib/jwt"
) )
func main() { func main() {
@ -33,9 +35,23 @@ 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")
// Register our handlers // Register our unprotected routes
server.Post("/api/register", gs.Register) server.Post("/api/register", gs.Register)
// Register handlers for example button count
server.Get("/api/button", gs.GetButtonCount)
server.Post("/api/button", gs.IncrementButtonCount)
server.Post("/api/login", gs.Login)
// Every route from here on will require a valid JWT
server.Use(jwtware.New(jwtware.Config{
SigningKey: jwtware.SigningKey{Key: []byte("secret")},
}))
server.Post("/api/loginrenew", gs.LoginRenew)
server.Delete("/api/userdelete", gs.UserDelete) // Perhaps just use POST to avoid headaches
// 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

@ -8,18 +8,24 @@ require (
github.com/mattn/go-sqlite3 v1.14.22 github.com/mattn/go-sqlite3 v1.14.22
) )
require (
github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect
github.com/gofiber/contrib/jwt v1.0.8
github.com/golang-jwt/jwt/v5 v5.2.1
)
// These are all for fiber // These are all for fiber
require ( require (
github.com/andybalholm/brotli v1.0.5 // indirect github.com/andybalholm/brotli v1.1.0 // indirect
github.com/gofiber/fiber/v2 v2.52.1 // indirect github.com/gofiber/fiber/v2 v2.52.2
github.com/google/uuid v1.5.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.17.0 // 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
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.2.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.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.15.0 // indirect golang.org/x/sys v0.18.0 // indirect
) )

View file

@ -1,17 +1,31 @@
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k=
github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/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/go.mod h1:gWWBtBiLmKXRN7xy6a96QO0KGvPEyxdh8x496Ujtg84=
github.com/gofiber/fiber/v2 v2.52.1 h1:1RoU2NS+b98o1L77sdl5mboGPiW+0Ypsi5oLmcYlgHI= github.com/gofiber/fiber/v2 v2.52.1 h1:1RoU2NS+b98o1L77sdl5mboGPiW+0Ypsi5oLmcYlgHI=
github.com/gofiber/fiber/v2 v2.52.1/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= github.com/gofiber/fiber/v2 v2.52.1/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/gofiber/fiber/v2 v2.52.2 h1:b0rYH6b06Df+4NyrbdptQL8ifuxw/Tf2DgfkZkDaxEo=
github.com/gofiber/fiber/v2 v2.52.2/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@ -26,13 +40,19 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0=
github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
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.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

View file

@ -14,9 +14,13 @@ import (
type Database interface { type Database interface {
AddUser(username string, password string) error AddUser(username string, password string) error
RemoveUser(username string) error RemoveUser(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
Migrate(dirname string) error Migrate(dirname string) error
// AddTimeReport(projectname string, start time.Time, end time.Time) error
// AddUserToProject(username string, projectname string) error
// ChangeUserRole(username string, projectname string, role string) error
} }
// This struct is a wrapper type that holds the database connection // This struct is a wrapper type that holds the database connection
@ -30,6 +34,11 @@ var scripts embed.FS
const userInsert = "INSERT INTO users (username, password) VALUES (?, ?)" const userInsert = "INSERT INTO users (username, password) VALUES (?, ?)"
const projectInsert = "INSERT INTO projects (name, description, user_id) SELECT ?, ?, id FROM users WHERE username = ?" const projectInsert = "INSERT INTO projects (name, description, user_id) SELECT ?, ?, id FROM users WHERE username = ?"
const promoteToAdmin = "INSERT INTO site_admin (admin_id) SELECT id FROM users WHERE username = ?"
// const addTimeReport = ""
// const addUserToProject = ""
// const changeUserRole = ""
// DbConnect connects to the database // DbConnect connects to the database
func DbConnect(dbpath string) Database { func DbConnect(dbpath string) Database {
@ -48,6 +57,18 @@ func DbConnect(dbpath string) Database {
return &Db{db} return &Db{db}
} }
// func (d *Db) AddTimeReport(projectname string, start time.Time, end time.Time) error {
// }
// func (d *Db) AddUserToProject(username string, projectname string) error {
// }
// func (d *Db) ChangeUserRole(username string, projectname string, role string) error {
// }
// AddUser adds a user to the database // AddUser adds a user to the database
func (d *Db) AddUser(username string, password string) error { func (d *Db) AddUser(username string, password string) error {
_, err := d.Exec(userInsert, username, password) _, err := d.Exec(userInsert, username, password)
@ -60,6 +81,11 @@ func (d *Db) RemoveUser(username string) error {
return err return err
} }
func (d *Db) PromoteToAdmin(username string) error {
_, err := d.Exec(promoteToAdmin, username)
return err
}
func (d *Db) GetUserId(username string) (int, error) { func (d *Db) GetUserId(username string) (int, error) {
var id int var id int
err := d.Get(&id, "SELECT id FROM users WHERE username = ?", username) err := d.Get(&id, "SELECT id FROM users WHERE username = ?", username)

View file

@ -74,3 +74,32 @@ func TestDbRemoveUser(t *testing.T) {
t.Error("RemoveUser failed:", err) t.Error("RemoveUser failed:", err)
} }
} }
func TestPromoteToAdmin(t *testing.T) {
db, err := setupState()
if err != nil {
t.Error("setupState failed:", err)
}
err = db.AddUser("test", "password")
if err != nil {
t.Error("AddUser failed:", err)
}
err = db.PromoteToAdmin("test")
if err != nil {
t.Error("PromoteToAdmin failed:", err)
}
}
// func TestAddTimeReport(t *testing.T) {
// }
// func TestAddUserToProject(t *testing.T) {
// }
// func TestChangeUserRole(t *testing.T) {
// }

View file

@ -0,0 +1,4 @@
CREATE TABLE IF NOT EXISTS site_admin (
admin_id INTEGER PRIMARY KEY,
FOREIGN KEY (admin_id) REFERENCES users (id) ON DELETE CASCADE
)

View file

@ -1,25 +1,49 @@
package handlers package handlers
import ( import (
"time"
"ttime/internal/database" "ttime/internal/database"
"ttime/internal/types" "ttime/internal/types"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
) )
// The actual interface that we will use // The actual interface that we will use
type GlobalState interface { type GlobalState interface {
Register(c *fiber.Ctx) error Register(c *fiber.Ctx) error // To register a new user
UserDelete(c *fiber.Ctx) error // To delete a user
Login(c *fiber.Ctx) error // To get the token
LoginRenew(c *fiber.Ctx) error // To renew the token
// CreateProject(c *fiber.Ctx) error // To create a new project
// GetProjects(c *fiber.Ctx) error // To get all projects
// GetProject(c *fiber.Ctx) error // To get a specific project
// UpdateProject(c *fiber.Ctx) error // To update a project
// 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
GetButtonCount(c *fiber.Ctx) error // For demonstration purposes
IncrementButtonCount(c *fiber.Ctx) error // For demonstration purposes
} }
// "Constructor" // "Constructor"
func NewGlobalState(db database.Database) GlobalState { func NewGlobalState(db database.Database) GlobalState {
return &GState{Db: db} return &GState{Db: db, ButtonCount: 0}
} }
// The global state, which implements all the handlers // The global state, which implements all the handlers
type GState struct { type GState struct {
Db database.Database Db database.Database
ButtonCount int
} }
func (gs *GState) Register(c *fiber.Ctx) error { func (gs *GState) Register(c *fiber.Ctx) error {
@ -34,3 +58,76 @@ func (gs *GState) Register(c *fiber.Ctx) error {
return c.Status(200).SendString("User added") return c.Status(200).SendString("User added")
} }
// This path should obviously be protected in the future
// UserDelete deletes a user from the database
func (gs *GState) UserDelete(c *fiber.Ctx) error {
u := new(types.User)
if err := c.BodyParser(u); err != nil {
return c.Status(400).SendString(err.Error())
}
if err := gs.Db.RemoveUser(u.Username); err != nil {
return c.Status(500).SendString(err.Error())
}
return c.Status(200).SendString("User deleted")
}
func (gs *GState) GetButtonCount(c *fiber.Ctx) error {
return c.Status(200).JSON(fiber.Map{"pressCount": gs.ButtonCount})
}
func (gs *GState) IncrementButtonCount(c *fiber.Ctx) error {
gs.ButtonCount++
return c.Status(200).JSON(fiber.Map{"pressCount": gs.ButtonCount})
}
// Login is a simple login handler that returns a JWT token
func (gs *GState) Login(c *fiber.Ctx) error {
// To test: curl --data "user=user&pass=pass" http://localhost:8080/api/login
user := c.FormValue("user")
pass := c.FormValue("pass")
// Throws Unauthorized error
if user != "user" || pass != "pass" {
return c.SendStatus(fiber.StatusUnauthorized)
}
// Create the Claims
claims := jwt.MapClaims{
"name": user,
"admin": false,
"exp": time.Now().Add(time.Hour * 72).Unix(),
}
// Create token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Generate encoded token and send it as response.
t, err := token.SignedString([]byte("secret"))
if err != nil {
return c.SendStatus(fiber.StatusInternalServerError)
}
return c.JSON(fiber.Map{"token": t})
}
// LoginRenew is a simple handler that renews the token
func (gs *GState) LoginRenew(c *fiber.Ctx) error {
// For testing: curl localhost:3000/restricted -H "Authorization: Bearer <token>"
user := c.Locals("user").(*jwt.Token)
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 {
return c.SendStatus(fiber.StatusInternalServerError)
}
return c.JSON(fiber.Map{"token": t})
}

View file

@ -8,8 +8,12 @@
"name": "tmp", "name": "tmp",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"localforage": "^1.10.0",
"match-sorter": "^6.3.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0" "react-dom": "^18.2.0",
"react-router-dom": "^6.22.2",
"sort-by": "^0.0.2"
}, },
"devDependencies": { "devDependencies": {
"@swc/core": "^1.4.2", "@swc/core": "^1.4.2",
@ -670,6 +674,17 @@
"@babel/core": "^7.0.0-0" "@babel/core": "^7.0.0-0"
} }
}, },
"node_modules/@babel/runtime": {
"version": "7.24.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz",
"integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": { "node_modules/@babel/template": {
"version": "7.24.0", "version": "7.24.0",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz",
@ -1782,6 +1797,14 @@
"url": "https://opencollective.com/unts" "url": "https://opencollective.com/unts"
} }
}, },
"node_modules/@remix-run/router": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.2.tgz",
"integrity": "sha512-+Rnav+CaoTE5QJc4Jcwh5toUpnVLKYbpU6Ys0zqbakqbaLQHeglLVHPfxOiQqdNmUy5C2lXz5dwC6tQNX2JW2Q==",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.10.0", "version": "4.10.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.10.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.10.0.tgz",
@ -4644,6 +4667,11 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
},
"node_modules/import-fresh": { "node_modules/import-fresh": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -5898,6 +5926,14 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/lie": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
"integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lilconfig": { "node_modules/lilconfig": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
@ -5913,6 +5949,14 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true "dev": true
}, },
"node_modules/localforage": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
"integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
"dependencies": {
"lie": "3.1.1"
}
},
"node_modules/locate-path": { "node_modules/locate-path": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@ -5981,6 +6025,15 @@
"tmpl": "1.0.5" "tmpl": "1.0.5"
} }
}, },
"node_modules/match-sorter": {
"version": "6.3.4",
"resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.4.tgz",
"integrity": "sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==",
"dependencies": {
"@babel/runtime": "^7.23.8",
"remove-accents": "0.5.0"
}
},
"node_modules/merge-stream": { "node_modules/merge-stream": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@ -6849,6 +6902,36 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true "dev": true
}, },
"node_modules/react-router": {
"version": "6.22.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.2.tgz",
"integrity": "sha512-YD3Dzprzpcq+tBMHBS822tCjnWD3iIZbTeSXMY9LPSG541EfoBGyZ3bS25KEnaZjLcmQpw2AVLkFyfgXY8uvcw==",
"dependencies": {
"@remix-run/router": "1.15.2"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/react-router-dom": {
"version": "6.22.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.2.tgz",
"integrity": "sha512-WgqxD2qySEIBPZ3w0sHH+PUAiamDeszls9tzqMPBDA1YYVucTBXLU7+gtRfcSnhe92A3glPnvSxK2dhNoAVOIQ==",
"dependencies": {
"@remix-run/router": "1.15.2",
"react-router": "6.22.2"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/react-shallow-renderer": { "node_modules/react-shallow-renderer": {
"version": "16.15.0", "version": "16.15.0",
"resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz", "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz",
@ -6924,6 +7007,11 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/regexp.prototype.flags": { "node_modules/regexp.prototype.flags": {
"version": "1.5.2", "version": "1.5.2",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
@ -6942,6 +7030,11 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/remove-accents": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz",
"integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A=="
},
"node_modules/require-directory": { "node_modules/require-directory": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@ -7242,6 +7335,11 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/sort-by": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/sort-by/-/sort-by-0.0.2.tgz",
"integrity": "sha512-iOX5oHA4a0eqTMFiWrHYqv924UeRKFBLhym7iwSVG37Egg2wApgZKAjyzM9WZjMwKv6+8Zi+nIaJ7FYsO9EkoA=="
},
"node_modules/source-map": { "node_modules/source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",

View file

@ -12,8 +12,12 @@
"test": "jest" "test": "jest"
}, },
"dependencies": { "dependencies": {
"localforage": "^1.10.0",
"match-sorter": "^6.3.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0" "react-dom": "^18.2.0",
"react-router-dom": "^6.22.2",
"sort-by": "^0.0.2"
}, },
"devDependencies": { "devDependencies": {
"@swc/core": "^1.4.2", "@swc/core": "^1.4.2",

View file

@ -1,42 +0,0 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View file

@ -1,31 +0,0 @@
import reactLogo from "./assets/react.svg";
import viteLogo from "/vite.svg";
import "./App.css";
import { CountButton } from "./Components/CountButton";
function App(): JSX.Element {
return (
<>
<div>
<a href="https://vitejs.dev" target="_blank" rel="noreferrer">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank" rel="noreferrer">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<CountButton />
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
);
}
export default App;

View file

@ -0,0 +1,36 @@
/**
* The shape of a task
*/
interface Task {
id: number;
name: string;
}
/**
* The props for the TaskList component
*/
interface TaskProps {
tasks: Task[];
}
/**
* A simple list of tasks
* @param props - The tasks to display
* @returns {JSX.Element} The task list
* @example
* const tasks = [{ id: 1, name: "Do the thing" }];
* return <TaskList tasks={tasks} />;
*/
export function TaskList(props: TaskProps): JSX.Element {
return (
<div>
<h1>Task List</h1>
<p>Tasks go here</p>
<ul>
{props.tasks.map((task) => (
<li key={task.id}>{task.name}</li>
))}
</ul>
</div>
);
}

View file

@ -0,0 +1,24 @@
import { ReactNode } from "react";
/** The props for the container */
interface ContainerProps {
children: ReactNode;
}
/**
* Contains children
* @param props - The children to contain
* @returns {JSX.Element} The container
*/
export function Container(props: ContainerProps): JSX.Element {
return <div>{props.children}</div>;
}
/**
* Contains even more children
* @param props
* @returns {JSX.Element}
*/
export function Container2(props: ContainerProps): JSX.Element {
return <Container>{props.children}</Container>;
}

View file

@ -0,0 +1,36 @@
import reactLogo from "../assets/react.svg";
import viteLogo from "/vite.svg";
import "../index.css";
import { CountButton } from "../Components/CountButton";
import { Link } from "react-router-dom";
/**
* The home page of the application
* @returns {JSX.Element} The home page
*/
export default function HomePage(): JSX.Element {
return (
<>
<div>
<a href="https://vitejs.dev" target="_blank" rel="noreferrer">
<img src={viteLogo} className="logo h-32 p-5" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank" rel="noreferrer">
<img
src={reactLogo}
className="logo react h-32 p-5"
alt="React logo"
/>
</a>
</div>
<h1>Vite + React</h1>
<div className="card flex flex-col items-center space-y-4">
<CountButton />
<Link to="/settings">To Settings</Link>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
);
}

View file

@ -0,0 +1,17 @@
import "../index.css";
import { Link } from "react-router-dom";
/**
* The settings page of the application
* @returns {JSX.Element} The settings page
*/
export default function SettingsPage(): JSX.Element {
return (
<>
<h1>Very Fancy Settings Page</h1>
<div className="card">
<Link to="/">To Home</Link>
</div>
</>
);
}

View file

@ -2,6 +2,11 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/*
* We are using tailwind, so do not add any custom CSS here.
* Most of this is going to get cleaned up eventually.
*/
:root { :root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5; line-height: 1.5;
@ -70,3 +75,48 @@ button:focus-visible {
background-color: #f9f9f9; background-color: #f9f9f9;
} }
} }
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
a {
display: inline-block;
}
.logo {
transition: filter 0.25s;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View file

@ -1,12 +1,28 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import App from "./App.tsx"; import SettingsPage from "./Pages/Settings.tsx";
import HomePage from "./Pages/Home.tsx";
import "./index.css"; import "./index.css";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
// This is where the routes are mounted
const router = createBrowserRouter([
{
path: "/",
element: <HomePage />,
},
{
path: "/settings",
element: <SettingsPage />,
},
]);
// Semi-hacky way to get the root element
const root = document.getElementById("root") ?? document.createElement("div"); const root = document.getElementById("root") ?? document.createElement("div");
// Render the router at the root
ReactDOM.createRoot(root).render( ReactDOM.createRoot(root).render(
<React.StrictMode> <React.StrictMode>
<App /> <RouterProvider router={router} />
</React.StrictMode>, </React.StrictMode>,
); );