feat(auth): Add login, register, session_info and api creation

This commit is contained in:
Björn Benouarets
2026-01-15 20:25:17 +01:00
commit 13d908420a
31 changed files with 1421 additions and 0 deletions

22
app/utils/env.go Normal file
View File

@@ -0,0 +1,22 @@
package utils
import (
"os"
"strings"
)
func GetEnv(key, defaultValue string) string {
value := os.Getenv(key)
if value == "" {
return defaultValue
}
return value
}
func GetEnvBool(key string, defaultValue bool) bool {
value := strings.ToLower(GetEnv(key, ""))
if value == "" {
return defaultValue
}
return value == "true" || value == "1"
}

81
app/utils/hash.go Normal file
View File

@@ -0,0 +1,81 @@
package utils
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"fmt"
"strings"
"golang.org/x/crypto/argon2"
)
const (
argon2Time = 3
argon2Memory = 64 * 1024
argon2Threads = 4
argon2KeyLen = 32
saltLength = 16
)
func Hash(password string) (string, error) {
salt := make([]byte, saltLength)
if _, err := rand.Read(salt); err != nil {
return "", err
}
hash := argon2.IDKey([]byte(password), salt, argon2Time, argon2Memory, argon2Threads, argon2KeyLen)
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
b64Hash := base64.RawStdEncoding.EncodeToString(hash)
encodedHash := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
argon2.Version, argon2Memory, argon2Time, argon2Threads, b64Salt, b64Hash)
return encodedHash, nil
}
func Verify(password, encodedHash string) (bool, error) {
parts := strings.Split(encodedHash, "$")
if len(parts) != 6 {
return false, fmt.Errorf("invalid hash format")
}
if parts[1] != "argon2id" {
return false, fmt.Errorf("unsupported hash algorithm")
}
var version int
_, err := fmt.Sscanf(parts[2], "v=%d", &version)
if err != nil {
return false, err
}
if version != argon2.Version {
return false, fmt.Errorf("incompatible version")
}
var memory, time uint32
var threads uint8
_, err = fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &time, &threads)
if err != nil {
return false, err
}
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
if err != nil {
return false, err
}
hash, err := base64.RawStdEncoding.DecodeString(parts[5])
if err != nil {
return false, err
}
otherHash := argon2.IDKey([]byte(password), salt, time, memory, threads, uint32(len(hash)))
if subtle.ConstantTimeCompare(hash, otherHash) == 1 {
return true, nil
}
return false, nil
}

11
app/utils/res/http.go Normal file
View File

@@ -0,0 +1,11 @@
package res
import "github.com/gofiber/fiber/v2"
type HTTPResponse struct {
Code int `json:"code"`
Body *fiber.Map `json:"body"`
ODataContext string `json:"@odata.context"`
ODataCount int `json:"@odata.count"`
ODataNextLink *string `json:"@odata.nextLink"`
}

77
app/utils/response.go Normal file
View File

@@ -0,0 +1,77 @@
package utils
import (
"github.com/gofiber/fiber/v2"
)
type Response interface {
JSON() map[string]interface{}
Send(c *fiber.Ctx) error
}
type HTTPResponse struct {
Code int `json:"code"`
Body *fiber.Map `json:"body"`
ODataContext string `json:"@odata.context"`
ODataCount *int `json:"@odata.count"`
ODataNextLink *string `json:"@odata.nextLink"`
}
type ErrorResponse struct {
Code int `json:"code"`
Body *fiber.Map `json:"body"`
}
func NewHTTPResponse(code int, body *fiber.Map, oDataContext string, oDataCount *int, oDataNextLink *string) *HTTPResponse {
return &HTTPResponse{
Code: code,
Body: body,
ODataContext: oDataContext,
ODataCount: oDataCount,
ODataNextLink: oDataNextLink,
}
}
func NewErrorResponse(code int, body *fiber.Map) *ErrorResponse {
return &ErrorResponse{
Code: code,
Body: body,
}
}
func (res *HTTPResponse) JSON() map[string]interface{} {
result := map[string]interface{}{
"code": res.Code,
}
if res.Body != nil {
result["body"] = res.Body
}
if res.ODataContext != "" {
result["@odata.context"] = res.ODataContext
}
if res.ODataCount != nil {
result["@odata.count"] = *res.ODataCount
}
if res.ODataNextLink != nil {
result["@odata.nextLink"] = res.ODataNextLink
}
return result
}
func (res *ErrorResponse) JSON() map[string]interface{} {
result := map[string]interface{}{
"code": res.Code,
}
if res.Body != nil {
result["body"] = res.Body
}
return result
}
func (res *ErrorResponse) Send(c *fiber.Ctx) error {
return c.Status(res.Code).JSON(res.JSON())
}
func (res *HTTPResponse) Send(c *fiber.Ctx) error {
return c.Status(res.Code).JSON(res.JSON())
}

25
app/utils/sql.go Normal file
View File

@@ -0,0 +1,25 @@
package utils
import (
"errors"
"strings"
"gorm.io/gorm"
)
// IsDuplicateKeyError checks if an error is a duplicate key constraint violation
func IsDuplicateKeyError(err error) bool {
if err == nil {
return false
}
// Check for GORM duplicate key error
if errors.Is(err, gorm.ErrDuplicatedKey) {
return true
}
// Check for PostgreSQL duplicate key error (SQLSTATE 23505)
errMsg := strings.ToLower(err.Error())
return strings.Contains(errMsg, "duplicate key value violates unique constraint") ||
strings.Contains(errMsg, "sqlstate 23505")
}

18
app/utils/token.go Normal file
View File

@@ -0,0 +1,18 @@
package utils
import (
"crypto/rand"
)
func GenerateRandomString(length int) string {
charset := "abcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, length)
_, err := rand.Read(b)
if err != nil {
return ""
}
for i := range b {
b[i] = charset[b[i]%byte(len(charset))]
}
return string(b)
}