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

52
app/cache/redis.go vendored Normal file
View File

@@ -0,0 +1,52 @@
package cache
import (
"context"
"fmt"
"git.secnex.io/secnex/auth-api/config"
"github.com/valkey-io/valkey-go"
)
type RedisConfiguration struct {
Host string
Port string
Password string
}
type RedisCache struct {
Client valkey.Client
Context context.Context
}
var Cache RedisCache
func NewRedisConfiguration(host, port, password string) *RedisConfiguration {
return &RedisConfiguration{
Host: host,
Port: port,
Password: password,
}
}
func NewRedisConfigurationFromConfig(config *config.Config) *RedisConfiguration {
return &RedisConfiguration{
Host: config.RedisHost,
Port: config.RedisPort,
Password: config.RedisPassword,
}
}
func Connect(config *config.Config) error {
client, err := valkey.NewClient(valkey.ClientOption{InitAddress: []string{fmt.Sprintf("%s:%s", config.RedisHost, config.RedisPort)}})
if err != nil {
return err
}
ctx := context.Background()
cache := &RedisCache{
Client: client,
Context: ctx,
}
Cache = *cache
return nil
}

64
app/config/config.go Normal file
View File

@@ -0,0 +1,64 @@
package config
import (
"strings"
"git.secnex.io/secnex/auth-api/utils"
)
type Config struct {
Debug bool
FiberShowStartupMessage bool
CorsAllowOrigins string
CorsAllowHeaders string
CorsAllowMethods string
Address string
DatabaseHost string
DatabasePort string
DatabaseUser string
DatabasePassword string
DatabaseName string
RedisHost string
RedisPort string
RedisPassword string
JwtSecret string
ENV string
UNPROTECTED_ENDPOINTS []string
}
var CONFIG *Config
func generateSecret() string {
return utils.GenerateRandomString(32)
}
func NewConfig() *Config {
ENV := utils.GetEnv("ENV", "development")
UNPROTECTED_ENDPOINTS := strings.Split(utils.GetEnv("UNPROTECTED_ENDPOINTS", ""), ",")
if ENV == "development" {
UNPROTECTED_ENDPOINTS = append(UNPROTECTED_ENDPOINTS, "/api_keys")
}
c := &Config{
Debug: utils.GetEnvBool("DEBUG", false),
FiberShowStartupMessage: utils.GetEnvBool("FIBER_SHOW_STARTUP_MESSAGE", false),
CorsAllowOrigins: utils.GetEnv("CORS_ALLOW_ORIGINS", "*"),
CorsAllowHeaders: utils.GetEnv("CORS_ALLOW_HEADERS", "Origin, Content-Type, Accept"),
CorsAllowMethods: utils.GetEnv("CORS_ALLOW_METHODS", "GET, POST, PUT, DELETE"),
Address: utils.GetEnv("ADDRESS", ":3000"),
DatabaseHost: utils.GetEnv("DATABASE_HOST", "localhost"),
DatabasePort: utils.GetEnv("DATABASE_PORT", "5432"),
DatabaseUser: utils.GetEnv("DATABASE_USER", "postgres"),
DatabasePassword: utils.GetEnv("DATABASE_PASSWORD", "postgres"),
DatabaseName: utils.GetEnv("DATABASE_NAME", "secnex"),
JwtSecret: utils.GetEnv("JWT_SECRET", "your-256-bit-secret"),
RedisHost: utils.GetEnv("REDIS_HOST", "localhost"),
RedisPort: utils.GetEnv("REDIS_PORT", "6379"),
RedisPassword: utils.GetEnv("REDIS_PASSWORD", ""),
ENV: ENV,
UNPROTECTED_ENDPOINTS: UNPROTECTED_ENDPOINTS,
}
CONFIG = c
return c
}

View File

@@ -0,0 +1,11 @@
package controllers
import (
"git.secnex.io/secnex/auth-api/services"
"github.com/gofiber/fiber/v2"
)
func CreateApiKeyController(c *fiber.Ctx) error {
response := services.CreateApiKey()
return response.Send(c)
}

35
app/controllers/login.go Normal file
View File

@@ -0,0 +1,35 @@
package controllers
import (
"git.secnex.io/secnex/auth-api/services"
"git.secnex.io/secnex/auth-api/utils"
"git.secnex.io/secnex/masterlog"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
)
type LoginRequest struct {
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
}
func LoginController(c *fiber.Ctx) error {
var request LoginRequest
if err := c.BodyParser(&request); err != nil {
return utils.NewErrorResponse(fiber.StatusBadRequest, &fiber.Map{
"message": "Invalid request body",
}).Send(c)
}
masterlog.Debug("Processing login request", map[string]interface{}{"username": request.Username})
validate := validator.New()
if err := validate.Struct(request); err != nil {
return utils.NewErrorResponse(fiber.StatusBadRequest, &fiber.Map{
"message": "Invalid request body",
}).Send(c)
}
response := services.Login(request.Username, request.Password)
return response.Send(c)
}

View File

@@ -0,0 +1,35 @@
package controllers
import (
"git.secnex.io/secnex/auth-api/services"
"git.secnex.io/secnex/auth-api/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
)
type RegisterRequest struct {
FirstName string `json:"first_name" validate:"required"`
LastName string `json:"last_name" validate:"required"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
Email string `json:"email" validate:"required"`
}
func RegisterController(c *fiber.Ctx) error {
var request RegisterRequest
if err := c.BodyParser(&request); err != nil {
return utils.NewErrorResponse(fiber.StatusBadRequest, &fiber.Map{
"message": "Invalid request body",
}).Send(c)
}
validate := validator.New()
if err := validate.Struct(request); err != nil {
return utils.NewErrorResponse(fiber.StatusBadRequest, &fiber.Map{
"message": "Invalid request body",
}).Send(c)
}
response := services.Register(request.FirstName, request.LastName, request.Username, request.Password, request.Email)
return response.Send(c)
}

View File

@@ -0,0 +1,40 @@
package controllers
import (
"git.secnex.io/secnex/auth-api/services"
"git.secnex.io/secnex/auth-api/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
)
type SessionInfoRequest struct {
SessionToken string `json:"token" validate:"required"`
}
func SessionInfoController(c *fiber.Ctx) error {
var request SessionInfoRequest
if err := c.BodyParser(&request); err != nil {
return utils.NewErrorResponse(fiber.StatusBadRequest, &fiber.Map{
"message": "Invalid request body",
}).Send(c)
}
validate := validator.New()
if err := validate.Struct(request); err != nil {
return utils.NewErrorResponse(fiber.StatusBadRequest, &fiber.Map{
"message": "Invalid request body",
}).Send(c)
}
sessionDetails, err := services.SessionInfo(request.SessionToken)
if err != nil {
return utils.NewErrorResponse(fiber.StatusUnauthorized, &fiber.Map{
"message": "Invalid token",
}).Send(c)
}
return utils.NewHTTPResponse(fiber.StatusOK, &fiber.Map{
"message": "OK",
"session": sessionDetails,
}, "", nil, nil).Send(c)
}

102
app/database/conn.go Normal file
View File

@@ -0,0 +1,102 @@
package database
import (
"fmt"
"strings"
"git.secnex.io/secnex/auth-api/config"
"git.secnex.io/secnex/auth-api/utils"
"git.secnex.io/secnex/masterlog"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
type DatabaseConfiguration struct {
Host string
Port string
User string
Password string
Database string
}
var DB *gorm.DB
func NewDatabaseConfiguration(host, port, user, password, database string) *DatabaseConfiguration {
return &DatabaseConfiguration{
Host: host,
Port: port,
User: user,
Password: password,
Database: database,
}
}
func NewDatabaseConfigurationFromConfig(config *config.Config) *DatabaseConfiguration {
return &DatabaseConfiguration{
Host: config.DatabaseHost,
Port: config.DatabasePort,
User: config.DatabaseUser,
Password: config.DatabasePassword,
Database: config.DatabaseName,
}
}
func NewDatabaseConfigurationFromEnv() *DatabaseConfiguration {
return &DatabaseConfiguration{
Host: utils.GetEnv("DB_HOST", "localhost"),
Port: utils.GetEnv("DB_PORT", "5432"),
User: utils.GetEnv("DB_USER", "postgres"),
Password: utils.GetEnv("DB_PASSWORD", "postgres"),
Database: utils.GetEnv("DB_DATABASE", "secnex"),
}
}
func (c *DatabaseConfiguration) String() string {
return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable client_encoding=UTF8", c.Host, c.Port, c.User, c.Password, c.Database)
}
func (c *DatabaseConfiguration) Connect(config *config.Config, models ...interface{}) error {
logLevel := logger.Silent
if config.Debug {
logLevel = logger.Info
}
db, err := gorm.Open(postgres.Open(c.String()), &gorm.Config{
Logger: logger.Default.LogMode(logLevel),
})
if err != nil {
return err
}
if err := AutoMigrate(db, models...); err != nil {
return err
}
DB = db
return nil
}
type SchemaProvider interface {
Schema() string
}
func AutoMigrate(conn *gorm.DB, models ...interface{}) error {
masterlog.Debug("Starting database auto-migration", map[string]interface{}{"model_count": len(models)})
err := conn.AutoMigrate(models...)
if err != nil {
// Check if error is about constraint that doesn't exist
// This can happen when GORM tries to drop old constraints during migration
errMsg := strings.ToLower(err.Error())
if strings.Contains(errMsg, "does not exist") && strings.Contains(errMsg, "constraint") {
masterlog.Debug("Ignoring constraint drop error", map[string]interface{}{"error": err.Error()})
// Return nil to continue despite the constraint error
return nil
}
masterlog.Debug("Auto-migration failed", map[string]interface{}{"error": err.Error()})
return err
}
masterlog.Debug("Database auto-migration completed successfully", map[string]interface{}{"model_count": len(models)})
return nil
}

13
app/database/exec.go Normal file
View File

@@ -0,0 +1,13 @@
package database
import "git.secnex.io/secnex/masterlog"
func Execute(query string) error {
masterlog.Debug("Executing database query", map[string]interface{}{"query": query})
if err := DB.Exec(query).Error; err != nil {
masterlog.Debug("Database query execution failed", map[string]interface{}{"error": err.Error(), "query": query})
return err
}
masterlog.Debug("Database query executed successfully", map[string]interface{}{"query": query})
return nil
}

40
app/go.mod Normal file
View File

@@ -0,0 +1,40 @@
module git.secnex.io/secnex/auth-api
go 1.25.3
require (
git.secnex.io/secnex/masterlog v0.1.0
github.com/go-playground/validator/v10 v10.30.1
github.com/gofiber/fiber/v2 v2.52.10
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0
golang.org/x/crypto v0.46.0
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.1
)
require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/leodido/go-urn v1.4.0 // 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.16 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/valkey-io/valkey-go v1.0.70 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
)

81
app/go.sum Normal file
View File

@@ -0,0 +1,81 @@
git.secnex.io/secnex/masterlog v0.1.0 h1:74j9CATpfeK0lxpWIQC9ag9083akwG8khi5BwLedD8E=
git.secnex.io/secnex/masterlog v0.1.0/go.mod h1:OnDlwEzdkKMnqY+G5O9kHdhoJ6fH1llbVdXpgSc5SdM=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/gofiber/fiber/v2 v2.52.10 h1:jRHROi2BuNti6NYXmZ6gbNSfT3zj/8c0xy94GOU5elY=
github.com/gofiber/fiber/v2 v2.52.10/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
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/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/valkey-io/valkey-go v1.0.70 h1:mjYNT8qiazxDAJ0QNQ8twWT/YFOkOoRd40ERV2mB49Y=
github.com/valkey-io/valkey-go v1.0.70/go.mod h1:VGhZ6fs68Qrn2+OhH+6waZH27bjpgQOiLyUQyXuYK5k=
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/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
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.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=

78
app/main.go Normal file
View File

@@ -0,0 +1,78 @@
package main
import (
"git.secnex.io/secnex/auth-api/cache"
"git.secnex.io/secnex/auth-api/config"
"git.secnex.io/secnex/auth-api/controllers"
"git.secnex.io/secnex/auth-api/database"
"git.secnex.io/secnex/auth-api/middlewares"
"git.secnex.io/secnex/auth-api/models"
"git.secnex.io/secnex/masterlog"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
)
func main() {
config := config.NewConfig()
pseudonymizer := masterlog.NewPseudonymizerFromString("1234567890")
masterlog.SetPseudonymizer(pseudonymizer)
masterlog.AddSensitiveFields("password", "token", "email")
if config.Debug {
masterlog.SetLevel(masterlog.LevelDebug)
} else {
masterlog.SetLevel(masterlog.LevelInfo)
}
// resetAdminApiKey := utils.GetEnvBool("RESET_ADMIN_API_KEY", false)
masterlog.AddEncoder(&masterlog.JSONEncoder{})
allModels := []interface{}{
&models.User{},
&models.Tenant{},
&models.Session{},
&models.ApiKey{},
}
dbConfig := database.NewDatabaseConfigurationFromConfig(config)
masterlog.Info("Connecting to database", map[string]interface{}{"host": config.DatabaseHost, "port": config.DatabasePort, "database": config.DatabaseName})
if err := dbConfig.Connect(config, allModels...); err != nil {
masterlog.Error("failed to connect to database", map[string]interface{}{"error": err.Error()})
return
}
masterlog.Info("Connected to database!")
masterlog.Info("Connecting to Redis", map[string]interface{}{"host": config.RedisHost, "port": config.RedisPort})
if err := cache.Connect(config); err != nil {
masterlog.Error("failed to connect to Redis", map[string]interface{}{"error": err.Error()})
return
}
masterlog.Info("Connected to Redis!")
app := fiber.New(fiber.Config{
DisableStartupMessage: !config.FiberShowStartupMessage,
})
app.Use(middlewares.RequestLogger())
app.Use(middlewares.AuthMiddleware())
app.Use(cors.New(cors.Config{
AllowOrigins: config.CorsAllowOrigins,
AllowHeaders: config.CorsAllowHeaders,
AllowMethods: config.CorsAllowMethods,
}))
// Controllers
app.Post("/login", controllers.LoginController)
app.Post("/register", controllers.RegisterController)
app.Get("/api_keys", controllers.CreateApiKeyController)
app.Post("/session/info", controllers.SessionInfoController)
masterlog.Info("Starting server", map[string]interface{}{"address": config.Address})
if err := app.Listen(config.Address); err != nil {
masterlog.Error("failed to start server", map[string]interface{}{"error": err.Error()})
return
}
}

66
app/middlewares/auth.go Normal file
View File

@@ -0,0 +1,66 @@
package middlewares
import (
"encoding/base64"
"slices"
"strings"
"git.secnex.io/secnex/auth-api/config"
"git.secnex.io/secnex/auth-api/repositories"
"git.secnex.io/secnex/auth-api/utils"
"github.com/gofiber/fiber/v2"
)
func AuthMiddleware() fiber.Handler {
return func(c *fiber.Ctx) error {
// check if the endpoint is in the unprotected endpoints
if slices.Contains(config.CONFIG.UNPROTECTED_ENDPOINTS, c.Path()) {
return c.Next()
}
token := c.Get("Authorization")
if token == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"message": "Unauthorized"})
}
tokenParts := strings.Split(token, " ")
if len(tokenParts) != 2 {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"message": "Unauthorized"})
}
tokenPartType, tokenPartValue := tokenParts[0], tokenParts[1]
if tokenPartType != "Bearer" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"message": "Unauthorized"})
}
// Decode the token from base64 to string
tokenValue, err := base64.StdEncoding.DecodeString(tokenPartValue)
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"message": "Unauthorized"})
}
tokenValueString := string(tokenValue)
tokenValueParts := strings.Split(tokenValueString, ":")
if len(tokenValueParts) != 2 {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"message": "Unauthorized"})
}
keyId, keyValue := tokenValueParts[0], tokenValueParts[1]
apiKey, err := repositories.GetApiKey(keyId)
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"message": "Unauthorized"})
}
if apiKey == nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"message": "Unauthorized"})
}
valid, err := utils.Verify(keyValue, apiKey.Key)
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"message": "Unauthorized"})
}
if !valid {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"message": "Unauthorized"})
}
c.Locals("key", keyId)
return c.Next()
}
}

49
app/middlewares/log.go Normal file
View File

@@ -0,0 +1,49 @@
package middlewares
import (
"time"
"git.secnex.io/secnex/masterlog"
"github.com/gofiber/fiber/v2"
)
// RequestLogger logs incoming HTTP requests via masterlog.
func RequestLogger() fiber.Handler {
return func(c *fiber.Ctx) error {
start := time.Now()
err := c.Next()
duration := time.Since(start)
entry := map[string]interface{}{
"method": c.Method(),
"path": c.OriginalURL(),
"ip": c.IP(),
"duration": duration.String(),
"status": c.Response().StatusCode(),
"user_agent": c.Get("User-Agent"),
}
if err != nil {
entry["error"] = err.Error()
masterlog.Error(
"HTTP request failed",
entry,
)
return err
}
if status := c.Response().StatusCode(); status >= fiber.StatusInternalServerError {
masterlog.Error(
"HTTP request failed",
entry,
)
} else {
masterlog.Info(
"HTTP request successful",
entry,
)
}
return nil
}
}

31
app/models/key.go Normal file
View File

@@ -0,0 +1,31 @@
package models
import (
"time"
"git.secnex.io/secnex/auth-api/utils"
"github.com/google/uuid"
"gorm.io/gorm"
)
type ApiKey struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
Key string `gorm:"not null" json:"key"`
Enabled bool `gorm:"not null;default:true" json:"enabled"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"`
}
func (ApiKey) TableName() string {
return "api_keys"
}
func (apiKey *ApiKey) BeforeCreate(tx *gorm.DB) (err error) {
apiKeyHash, err := utils.Hash(apiKey.Key)
if err != nil {
return err
}
apiKey.Key = apiKeyHash
return nil
}

22
app/models/session.go Normal file
View File

@@ -0,0 +1,22 @@
package models
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type Session struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
UserID uuid.UUID `gorm:"type:uuid;not null" json:"user_id"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"`
User *User `gorm:"foreignKey:UserID" json:"user"`
}
func (Session) TableName() string {
return "sessions"
}

25
app/models/tenant.go Normal file
View File

@@ -0,0 +1,25 @@
package models
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type Tenant struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
Name string `gorm:"not null" json:"name"`
Enabled bool `gorm:"not null;default:true" json:"enabled"`
AllowSelfRegistration bool `gorm:"not null;default:false" json:"allow_self_registration"`
AllowSelfRegistrationDomains []string `gorm:"type:jsonb;not null" json:"allow_self_registration_domains"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"`
Users []User `gorm:"foreignKey:TenantID" json:"users"`
}
func (Tenant) TableName() string {
return "tenants"
}

38
app/models/users.go Normal file
View File

@@ -0,0 +1,38 @@
package models
import (
"time"
"git.secnex.io/secnex/auth-api/utils"
"github.com/google/uuid"
"gorm.io/gorm"
)
type User struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
FirstName string `gorm:"not null" json:"first_name"`
LastName string `gorm:"not null" json:"last_name"`
Username string `gorm:"not null;unique" json:"username"`
Password string `gorm:"not null" json:"password"`
Email string `gorm:"not null;unique" json:"email"`
Verified bool `gorm:"not null;default:false" json:"verified"`
TenantID *uuid.UUID `gorm:"type:uuid" json:"tenant_id"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"`
Tenant *Tenant `gorm:"foreignKey:TenantID" json:"tenant"`
}
func (User) TableName() string {
return "users"
}
func (user *User) BeforeCreate(tx *gorm.DB) (err error) {
passwordHash, err := utils.Hash(user.Password)
if err != nil {
return err
}
user.Password = passwordHash
return nil
}

14
app/repositories/key.go Normal file
View File

@@ -0,0 +1,14 @@
package repositories
import (
"git.secnex.io/secnex/auth-api/database"
"git.secnex.io/secnex/auth-api/models"
)
func GetApiKey(id string) (*models.ApiKey, error) {
var apiKey *models.ApiKey
if err := database.DB.Where("id = ? AND deleted_at IS NULL AND enabled = true", id).First(&apiKey).Error; err != nil {
return nil, err
}
return apiKey, nil
}

View File

@@ -0,0 +1,99 @@
package repositories
import (
"encoding/json"
"time"
"git.secnex.io/secnex/auth-api/cache"
"git.secnex.io/secnex/auth-api/database"
"git.secnex.io/secnex/auth-api/models"
"git.secnex.io/secnex/masterlog"
"github.com/google/uuid"
)
type SessionDetails struct {
UserID uuid.UUID `json:"user_id"`
Username string `json:"username"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}
func CreateSession(user *models.User) *models.Session {
session := &models.Session{
ID: uuid.New(),
UserID: user.ID,
}
if err := database.DB.Create(session).Error; err != nil {
masterlog.Debug("Failed to create session in database", map[string]interface{}{"error": err.Error(), "user_id": user.ID})
return nil
}
sessionDetails := SessionDetails{
UserID: user.ID,
Username: user.Username,
Email: user.Email,
FirstName: user.FirstName,
LastName: user.LastName,
}
jsonData, err := json.Marshal(sessionDetails)
if err != nil {
masterlog.Debug("Failed to marshal session details", map[string]interface{}{"error": err.Error(), "session_id": session.ID})
return nil
}
ttl := time.Hour * 24
if cache.Cache.Client == nil {
masterlog.Debug("Redis client not initialized, skipping cache storage", map[string]interface{}{"session_id": session.ID})
return session
}
if err := cache.Cache.Client.Do(cache.Cache.Context, cache.Cache.Client.B().Set().Key(session.ID.String()).Value(string(jsonData)).Ex(ttl).Build()).Error(); err != nil {
masterlog.Debug("Failed to store session in cache", map[string]interface{}{"error": err.Error(), "session_id": session.ID})
return session
}
masterlog.Debug("Session stored in cache", map[string]interface{}{"session_id": session.ID})
return session
}
func GetSessionCache(sessionID string) *SessionDetails {
masterlog.Debug("Retrieving session from cache", map[string]interface{}{"session_id": sessionID})
if cache.Cache.Client == nil {
masterlog.Debug("Redis client not initialized", map[string]interface{}{"session_id": sessionID})
return nil
}
res := cache.Cache.Client.Do(cache.Cache.Context, cache.Cache.Client.B().Get().Key(sessionID).Build())
if res.Error() != nil {
masterlog.Debug("Failed to get session from cache", map[string]interface{}{"error": res.Error(), "session_id": sessionID})
return nil
}
rawStr := res.String()
if rawStr == "" {
masterlog.Debug("Session not found in cache", map[string]interface{}{"session_id": sessionID})
return nil
}
// Parse the valkey response structure to extract the actual JSON string
var valkeyResponse struct {
Message struct {
Value string `json:"Value"`
Type string `json:"Type"`
} `json:"Message"`
}
if err := json.Unmarshal([]byte(rawStr), &valkeyResponse); err != nil {
// If it's not the wrapped format, use it directly
masterlog.Debug("Cache response not in wrapped format, using directly", map[string]interface{}{"session_id": sessionID})
} else {
// Extract the actual JSON string from Message.Value
rawStr = valkeyResponse.Message.Value
masterlog.Debug("Extracted JSON from cache response", map[string]interface{}{"session_id": sessionID})
}
var sessionDetails SessionDetails
if err := json.Unmarshal([]byte(rawStr), &sessionDetails); err != nil {
masterlog.Debug("Failed to unmarshal session details", map[string]interface{}{"error": err.Error(), "session_id": sessionID})
return nil
}
masterlog.Debug("Session retrieved from cache", map[string]interface{}{"session_id": sessionID, "user_id": sessionDetails.UserID})
return &sessionDetails
}

40
app/repositories/users.go Normal file
View File

@@ -0,0 +1,40 @@
package repositories
import (
"git.secnex.io/secnex/auth-api/database"
"git.secnex.io/secnex/auth-api/models"
"git.secnex.io/secnex/masterlog"
)
func GetUserByUsername(username string) (*models.User, error) {
var user *models.User
if err := database.DB.Where("username = ?", username).First(&user).Error; err != nil {
return nil, err
}
return user, nil
}
func GetUserByUniqueFields(username, email string) (*models.User, error) {
var user *models.User
if err := database.DB.Where("username = ? OR email = ?", username, email).First(&user).Error; err != nil {
return nil, nil
}
return user, nil
}
func CreateUser(firstName, lastName, username, password, email string) error {
user := &models.User{
FirstName: firstName,
LastName: lastName,
Username: username,
Password: password,
Email: email,
}
if err := database.DB.Create(user).Error; err != nil {
masterlog.Debug("Failed to create user in database", map[string]interface{}{"error": err.Error(), "username": username, "email": email})
return err
}
masterlog.Debug("User created successfully", map[string]interface{}{"username": username, "email": email})
return nil
}

34
app/services/api_key.go Normal file
View File

@@ -0,0 +1,34 @@
package services
import (
"encoding/base64"
"fmt"
"git.secnex.io/secnex/auth-api/database"
"git.secnex.io/secnex/auth-api/models"
"git.secnex.io/secnex/auth-api/utils"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)
func CreateApiKey() *utils.HTTPResponse {
keyID := uuid.New()
key := utils.GenerateRandomString(32)
createApiKey := &models.ApiKey{
ID: keyID,
Key: key,
}
if err := database.DB.Create(createApiKey).Error; err != nil {
return utils.NewHTTPResponse(fiber.StatusInternalServerError, &fiber.Map{
"message": "Error creating API key",
}, "", nil, nil)
}
apiKeyPlain := fmt.Sprintf("%s:%s", keyID.String(), key)
apiKey := base64.StdEncoding.EncodeToString([]byte(apiKeyPlain))
return utils.NewHTTPResponse(fiber.StatusOK, &fiber.Map{
"message": "API key created successfully",
"key": apiKey,
}, "", nil, nil)
}

67
app/services/login.go Normal file
View File

@@ -0,0 +1,67 @@
package services
import (
"time"
"git.secnex.io/secnex/auth-api/config"
"git.secnex.io/secnex/auth-api/repositories"
"git.secnex.io/secnex/auth-api/utils"
"git.secnex.io/secnex/masterlog"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
)
func Login(username, password string) *utils.HTTPResponse {
// Get user by username
user, err := repositories.GetUserByUsername(username)
if err != nil {
return utils.NewHTTPResponse(fiber.StatusNotFound, &fiber.Map{
"message": "User not found",
}, "", nil, nil)
}
if user == nil {
return utils.NewHTTPResponse(fiber.StatusNotFound, &fiber.Map{
"message": "User not found",
}, "", nil, nil)
}
hashedPassword := user.Password
valid, err := utils.Verify(password, hashedPassword)
if err != nil {
return utils.NewHTTPResponse(fiber.StatusInternalServerError, &fiber.Map{
"message": "Error verifying password",
}, "", nil, nil)
}
if !valid {
return utils.NewHTTPResponse(fiber.StatusUnauthorized, &fiber.Map{
"message": "Invalid password",
}, "", nil, nil)
}
session := repositories.CreateSession(user)
if session == nil {
return utils.NewHTTPResponse(fiber.StatusInternalServerError, &fiber.Map{
"message": "Error creating session",
}, "", nil, nil)
}
masterlog.Debug("Session created successfully", map[string]interface{}{"session_id": session.ID, "user_id": session.UserID})
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": session.ID.String(),
"exp": time.Now().Add(time.Hour * 24).Unix(),
})
secret := config.CONFIG.JwtSecret
tokenString, err := token.SignedString([]byte(secret))
if err != nil {
return utils.NewHTTPResponse(fiber.StatusInternalServerError, &fiber.Map{
"message": "Error generating token",
}, "", nil, nil)
}
return utils.NewHTTPResponse(fiber.StatusOK, &fiber.Map{
"message": "Login successful",
"token": tokenString,
}, "", nil, nil)
}

37
app/services/register.go Normal file
View File

@@ -0,0 +1,37 @@
package services
import (
"git.secnex.io/secnex/auth-api/repositories"
"git.secnex.io/secnex/auth-api/utils"
"github.com/gofiber/fiber/v2"
)
func Register(firstName, lastName, username, password, email string) *utils.HTTPResponse {
user, err := repositories.GetUserByUniqueFields(username, email)
if err != nil {
return utils.NewHTTPResponse(fiber.StatusInternalServerError, &fiber.Map{
"message": "Error getting user",
}, "", nil, nil)
}
if user != nil {
return utils.NewHTTPResponse(fiber.StatusBadRequest, &fiber.Map{
"message": "User already exists",
}, "", nil, nil)
}
err = repositories.CreateUser(firstName, lastName, username, password, email)
if err != nil {
if utils.IsDuplicateKeyError(err) {
return utils.NewHTTPResponse(fiber.StatusBadRequest, &fiber.Map{
"message": "User already exists",
}, "", nil, nil)
}
return utils.NewHTTPResponse(fiber.StatusInternalServerError, &fiber.Map{
"message": "Error creating user",
}, "", nil, nil)
}
return utils.NewHTTPResponse(fiber.StatusOK, &fiber.Map{
"message": "Your account has been created successfully. Please check your email for verification. If you don't see it, please check your spam folder.",
}, "", nil, nil)
}

View File

@@ -0,0 +1,49 @@
package services
import (
"errors"
"fmt"
"git.secnex.io/secnex/auth-api/config"
"git.secnex.io/secnex/auth-api/repositories"
"git.secnex.io/secnex/masterlog"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
)
func SessionInfo(token string) (*repositories.SessionDetails, error) {
claims, err := jwt.ParseWithClaims(token, &jwt.MapClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(config.CONFIG.JwtSecret), nil
})
if err != nil {
return nil, err
}
if !claims.Valid {
return nil, errors.New("invalid token")
}
mapClaims := claims.Claims.(*jwt.MapClaims)
subValue, ok := (*mapClaims)["sub"]
if !ok {
return nil, errors.New("sub claim not found")
}
var sessionID string
switch v := subValue.(type) {
case string:
sessionID = v
case uuid.UUID:
sessionID = v.String()
default:
sessionID = fmt.Sprintf("%v", v)
}
masterlog.Debug("Session ID extracted from token", map[string]interface{}{"session_id": sessionID, "sub_type": fmt.Sprintf("%T", subValue)})
sessionDetails := repositories.GetSessionCache(sessionID)
if sessionDetails == nil {
masterlog.Debug("Session not found in cache", map[string]interface{}{"session_id": sessionID})
return nil, errors.New("session not found")
}
masterlog.Debug("Session details retrieved successfully", map[string]interface{}{"session_id": sessionID, "user_id": sessionDetails.UserID})
return sessionDetails, nil
}

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)
}