update
This commit is contained in:
parent
ae9ee91bc4
commit
7ed986e4eb
21 changed files with 547 additions and 93 deletions
|
@ -69,3 +69,7 @@ lint:
|
|||
|
||||
# Default target
|
||||
default: build
|
||||
|
||||
install-just:
|
||||
@echo "Installing just"
|
||||
@curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin
|
||||
|
|
|
@ -9,6 +9,8 @@ import (
|
|||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
jwtware "github.com/gofiber/contrib/jwt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -33,9 +35,23 @@ func main() {
|
|||
// This will likely be replaced by an embedded filesystem in the future
|
||||
server.Static("/", "./static")
|
||||
|
||||
// Register our handlers
|
||||
// Register our unprotected routes
|
||||
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
|
||||
err = server.Listen(fmt.Sprintf(":%d", conf.Port))
|
||||
if err != nil {
|
||||
|
|
|
@ -8,18 +8,24 @@ require (
|
|||
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
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/gofiber/fiber/v2 v2.52.1 // indirect
|
||||
github.com/google/uuid v1.5.0 // indirect
|
||||
github.com/klauspost/compress v1.17.0 // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/gofiber/fiber/v2 v2.52.2
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.17.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // 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/fasthttp v1.51.0 // indirect
|
||||
github.com/valyala/fasthttp v1.52.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
|
||||
)
|
||||
|
|
|
@ -1,17 +1,31 @@
|
|||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||
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/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/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/gofiber/contrib/jwt v1.0.8 h1:/GeOsm/Mr1OGr0GTy+RIVSz5VgNNyP3ZgK4wdqxF/WY=
|
||||
github.com/gofiber/contrib/jwt v1.0.8/go.mod h1:gWWBtBiLmKXRN7xy6a96QO0KGvPEyxdh8x496Ujtg84=
|
||||
github.com/gofiber/fiber/v2 v2.52.1 h1:1RoU2NS+b98o1L77sdl5mboGPiW+0Ypsi5oLmcYlgHI=
|
||||
github.com/gofiber/fiber/v2 v2.52.1/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
||||
github.com/gofiber/fiber/v2 v2.52.2 h1:b0rYH6b06Df+4NyrbdptQL8ifuxw/Tf2DgfkZkDaxEo=
|
||||
github.com/gofiber/fiber/v2 v2.52.2/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
||||
github.com/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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
||||
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
||||
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
||||
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
|
||||
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||
github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0=
|
||||
github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
|
|
|
@ -14,9 +14,13 @@ import (
|
|||
type Database interface {
|
||||
AddUser(username string, password string) error
|
||||
RemoveUser(username string) error
|
||||
PromoteToAdmin(username string) error
|
||||
GetUserId(username string) (int, error)
|
||||
AddProject(name string, description string, username string) error
|
||||
Migrate(dirname string) error
|
||||
// AddTimeReport(projectname string, start time.Time, end time.Time) error
|
||||
// AddUserToProject(username string, projectname string) error
|
||||
// ChangeUserRole(username string, projectname string, role string) error
|
||||
}
|
||||
|
||||
// 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 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
|
||||
func DbConnect(dbpath string) Database {
|
||||
|
@ -48,6 +57,18 @@ func DbConnect(dbpath string) Database {
|
|||
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
|
||||
func (d *Db) AddUser(username string, password string) error {
|
||||
_, err := d.Exec(userInsert, username, password)
|
||||
|
@ -60,6 +81,11 @@ func (d *Db) RemoveUser(username string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func (d *Db) PromoteToAdmin(username string) error {
|
||||
_, err := d.Exec(promoteToAdmin, username)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Db) GetUserId(username string) (int, error) {
|
||||
var id int
|
||||
err := d.Get(&id, "SELECT id FROM users WHERE username = ?", username)
|
||||
|
|
|
@ -74,3 +74,32 @@ func TestDbRemoveUser(t *testing.T) {
|
|||
t.Error("RemoveUser failed:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromoteToAdmin(t *testing.T) {
|
||||
db, err := setupState()
|
||||
if err != nil {
|
||||
t.Error("setupState failed:", err)
|
||||
}
|
||||
|
||||
err = db.AddUser("test", "password")
|
||||
if err != nil {
|
||||
t.Error("AddUser failed:", err)
|
||||
}
|
||||
|
||||
err = db.PromoteToAdmin("test")
|
||||
if err != nil {
|
||||
t.Error("PromoteToAdmin failed:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// func TestAddTimeReport(t *testing.T) {
|
||||
|
||||
// }
|
||||
|
||||
// func TestAddUserToProject(t *testing.T) {
|
||||
|
||||
// }
|
||||
|
||||
// func TestChangeUserRole(t *testing.T) {
|
||||
|
||||
// }
|
||||
|
|
4
backend/internal/database/migrations/0060_site_admin.sql
Normal file
4
backend/internal/database/migrations/0060_site_admin.sql
Normal 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
|
||||
)
|
|
@ -1,25 +1,49 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"time"
|
||||
"ttime/internal/database"
|
||||
"ttime/internal/types"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// The actual interface that we will use
|
||||
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"
|
||||
func NewGlobalState(db database.Database) GlobalState {
|
||||
return &GState{Db: db}
|
||||
return &GState{Db: db, ButtonCount: 0}
|
||||
}
|
||||
|
||||
// The global state, which implements all the handlers
|
||||
type GState struct {
|
||||
Db database.Database
|
||||
Db database.Database
|
||||
ButtonCount int
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// 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})
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue