From 224edb3ce567d7454b186f1cd9f594f6f0ca298e Mon Sep 17 00:00:00 2001 From: lauralani Date: Thu, 27 Apr 2023 10:37:42 +0200 Subject: [PATCH] add login method and api endpoint --- internal/api/users.go | 62 +++++++++++++++++++++++++++++++++ internal/app/fiber.go | 5 +++ internal/db/initialize.go | 15 ++++++++ internal/web/login.go | 67 ++++++++++++++++++++++++++++++++++++ models/apikey.go | 13 +++++++ models/{links.go => link.go} | 0 models/login.go | 19 ++++++++++ models/user.go | 20 +++++++++++ 8 files changed, 201 insertions(+) create mode 100644 internal/api/users.go create mode 100644 internal/web/login.go create mode 100644 models/apikey.go rename models/{links.go => link.go} (100%) create mode 100644 models/login.go create mode 100644 models/user.go diff --git a/internal/api/users.go b/internal/api/users.go new file mode 100644 index 0000000..a02484d --- /dev/null +++ b/internal/api/users.go @@ -0,0 +1,62 @@ +package api + +import ( + "codeberg.org/lauralani/go-urlsh/internal/misc" + "codeberg.org/lauralani/go-urlsh/models" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "github.com/gofiber/fiber/v2" + "log" + "time" +) + +func HandleUserPost(c *fiber.Ctx) error { + var newuser models.LoginRequest + + err := json.Unmarshal(c.Body(), &newuser) + if err != nil { + log.Println(err.Error()) + return fiber.NewError(fiber.StatusBadRequest, "400 Bad Request") + } + + usercount, err := models.DB.NewSelect().Model((*models.User)(nil)).Count(context.Background()) + if err != nil { + log.Printf("[POST /api/v1/users] Error querying database for users: %v\n", err.Error()) + return fiber.NewError(fiber.StatusInternalServerError, "500 Internal Server Error") + } + if usercount != 0 { + log.Printf("[POST /api/v1/users] someone trying to create user but user already exists\n") + return fiber.NewError(fiber.StatusUnauthorized, "401 Unauthorized") + } else { + salt := misc.RandomString(15) + created := time.Now() + hashbytes := sha256.Sum256([]byte(salt + newuser.Password)) + fmt.Printf("%x\n", hashbytes) + + hash := hex.EncodeToString(hashbytes[:]) + + user := new(models.User) + user.UserName = newuser.Username + user.PasswordSalt = salt + user.PasswordHash = hash + user.Created = created + + _, err = models.DB.NewInsert().Model(user).Exec(context.Background()) + if err != nil { + log.Printf("[POST /api/v1/users] Error adding user %v to database : %v\n", newuser.Username, err.Error()) + return fiber.NewError(fiber.StatusInternalServerError, "500 Internal Server Error") + } + + userresponse := models.UserResponse{UserName: newuser.Username, Created: created} + + c.Status(fiber.StatusCreated) + err = c.JSON(userresponse) + if err != nil { + log.Println(err) + } + return nil + } +} diff --git a/internal/app/fiber.go b/internal/app/fiber.go index 3c05f29..4f272b0 100644 --- a/internal/app/fiber.go +++ b/internal/app/fiber.go @@ -2,6 +2,7 @@ package app import ( "codeberg.org/lauralani/go-urlsh/internal/api" + "codeberg.org/lauralani/go-urlsh/internal/web" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/compress" "github.com/gofiber/fiber/v2/middleware/cors" @@ -47,6 +48,8 @@ func SetupFiber() error { fiberapp.Static("/admin/", "./web") + fiberapp.Post("/admin/login", web.HandleLogin) + v1 := fiberapp.Group("/api/v1") v1.Use(cors.New(cors.Config{AllowOrigins: "*"})) @@ -60,6 +63,8 @@ func SetupFiber() error { v1.Post("/apikeys", api.HandleApiKeysPost) v1.Delete("/apikeys/:id", api.HandleApiKeysPost) + v1.Post("/users", api.HandleUserPost) + listenerr := fiberapp.Listen(ip + ":" + port) if listenerr != nil { return listenerr diff --git a/internal/db/initialize.go b/internal/db/initialize.go index 3a97b73..6a8c66f 100644 --- a/internal/db/initialize.go +++ b/internal/db/initialize.go @@ -19,5 +19,20 @@ func InitializeDB() error { if err != nil { return fmt.Errorf("couldn't create database: [%w]", err) } + + _, err = models.DB.NewCreateTable().IfNotExists().Model((*models.User)(nil)).Exec(context.Background()) + if err != nil { + return fmt.Errorf("couldn't create database: [%w]", err) + } + + _, err = models.DB.NewCreateTable().IfNotExists().Model((*models.Login)(nil)).Exec(context.Background()) + if err != nil { + return fmt.Errorf("couldn't create database: [%w]", err) + } + + _, err = models.DB.NewCreateTable().IfNotExists().Model((*models.ApiKey)(nil)).Exec(context.Background()) + if err != nil { + return fmt.Errorf("couldn't create database: [%w]", err) + } return nil } diff --git a/internal/web/login.go b/internal/web/login.go new file mode 100644 index 0000000..986d37d --- /dev/null +++ b/internal/web/login.go @@ -0,0 +1,67 @@ +package web + +import ( + "codeberg.org/lauralani/go-urlsh/models" + "context" + "crypto/sha256" + "encoding/hex" + "github.com/gofiber/fiber/v2" + "github.com/google/uuid" + "log" + "time" +) + +func HandleLogin(c *fiber.Ctx) error { + var login models.LoginRequest + var user models.User + + if err := c.BodyParser(&login); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "400 Bad Request") + } + + err := models.DB.NewSelect().Model(&user).Where("username = ?", login.Username).Scan(context.Background()) + if err != nil { + log.Printf("Error authenticating User %v: User doesn't exist by IP %v\n", login.Username, c.IP()) + return fiber.NewError(fiber.StatusUnauthorized, "401 Unauthorized") + } + + passwordsumbytes := sha256.Sum256([]byte(user.PasswordSalt + login.Password)) + passwordsum := hex.EncodeToString(passwordsumbytes[:]) + + if passwordsum == user.PasswordHash { + // Passwords match + + expires := time.Now().Add(30 * 24 * time.Hour) + key := uuid.New().String() + login := new(models.Login) + + cookie := new(fiber.Cookie) + cookie.Name = "gourlsh_auth" + cookie.Value = key + cookie.Expires = expires + + login.Expires = expires + login.Cookie = key + login.UserName = user.UserName + + _, err = models.DB.NewInsert().Model(login).Exec(context.Background()) + if err != nil { + log.Printf("DB Error inserting login cookie information for user %v: %v\n", login.UserName, err.Error()) + return fiber.NewError(fiber.StatusInternalServerError, "500 Internal Server Error") + } + + user.LastLogin = time.Now() + _, err = models.DB.NewUpdate().Model(&user).WherePK().Exec(context.Background()) + if err != nil { + log.Printf("DB Error updating last login information for user %v: %v\n", login.UserName, err.Error()) + return fiber.NewError(fiber.StatusInternalServerError, "500 Internal Server Error") + } + c.Cookie(cookie) + c.Status(fiber.StatusSeeOther) + c.Location("/admin/") + return nil + } else { + log.Printf("Error authenticating User %v: password mismatch by IP %v\n", login.Username, c.IP()) + return fiber.NewError(fiber.StatusUnauthorized, "401 Unauthorized") + } +} diff --git a/models/apikey.go b/models/apikey.go new file mode 100644 index 0000000..3328682 --- /dev/null +++ b/models/apikey.go @@ -0,0 +1,13 @@ +package models + +import ( + "github.com/uptrace/bun" + "time" +) + +type ApiKey struct { + bun.BaseModel `bun:"table:apikeys"` + Key string `bun:"key,pk,type:uuid,default:gen_random_uuid()" json:"key,omitempty"` + UserName string `bun:"username,notnull" json:"username"` + Created time.Time `bun:"created,default:now()" json:"created"` +} diff --git a/models/links.go b/models/link.go similarity index 100% rename from models/links.go rename to models/link.go diff --git a/models/login.go b/models/login.go new file mode 100644 index 0000000..0c14bae --- /dev/null +++ b/models/login.go @@ -0,0 +1,19 @@ +package models + +import ( + "github.com/uptrace/bun" + "time" +) + +type Login struct { + bun.BaseModel `bun:"table:logins"` + ID int `bun:"id,pk,autoincrement"` + UserName string `bun:"username,notnull"` + Cookie string `bun:"cookie,notnull"` + Expires time.Time `bun:"expires,notnull"` +} + +type LoginRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} diff --git a/models/user.go b/models/user.go new file mode 100644 index 0000000..8e5dd32 --- /dev/null +++ b/models/user.go @@ -0,0 +1,20 @@ +package models + +import ( + "github.com/uptrace/bun" + "time" +) + +type User struct { + bun.BaseModel `bun:"table:users"` + UserName string `bun:"username,pk" json:"username"` + Created time.Time `bun:"created,notnull,default:now()" json:"created"` + LastLogin time.Time `bun:"lastlogin" json:"last_login"` + PasswordSalt string `bun:"salt" json:"password_salt"` + PasswordHash string `bun:"password" json:"password_hash"` +} + +type UserResponse struct { + UserName string `json:"username"` + Created time.Time `json:"created"` +}