mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
initial commit
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AuthCfg struct {
|
||||
Issuer string
|
||||
JWTSecret string
|
||||
AccessTTL time.Duration
|
||||
RefreshTTL time.Duration
|
||||
RefreshCookieName string
|
||||
RefreshCookiePath string
|
||||
}
|
||||
|
||||
func LoadAuth() AuthCfg {
|
||||
return AuthCfg{
|
||||
JWTSecret: getenv("JWT_SECRET", "dev-secret-change-me"),
|
||||
AccessTTL: getenvDuration("ACCESS_TTL", 10*time.Minute),
|
||||
RefreshTTL: getenvDuration("REFRESH_TTL", 30*24*time.Hour),
|
||||
RefreshCookieName: getenv("REFRESH_COOKIE_NAME", "rt"),
|
||||
RefreshCookiePath: getenv("REFRESH_COOKIE_PATH", "/api/auth"),
|
||||
Issuer: getenv("ISSUER", "http://localhost:8080"),
|
||||
}
|
||||
}
|
||||
|
||||
func getenv(k, def string) string {
|
||||
if v := os.Getenv(k); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
func getenvDuration(k string, def time.Duration) time.Duration {
|
||||
if v := os.Getenv(k); v != "" {
|
||||
d, err := time.ParseDuration(v)
|
||||
if err == nil {
|
||||
return d
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/utils"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
IsProd bool
|
||||
AppHost string
|
||||
Version string
|
||||
LogLevel string
|
||||
AppPort int
|
||||
DBHost string
|
||||
DBUser string
|
||||
DBPassword string
|
||||
DBName string
|
||||
DBPort int
|
||||
JWTSecret string
|
||||
JWTAccessExp int
|
||||
JWTRefreshExp int
|
||||
JWTResetPasswordExp int
|
||||
JWTVerifyEmailExp int
|
||||
PostgresDSN string
|
||||
RedisURL string
|
||||
Issuer string
|
||||
SMTPHost string
|
||||
SMTPPort int
|
||||
SMTPUsername string
|
||||
SMTPPassword string
|
||||
EmailFrom string
|
||||
GoogleClientID string
|
||||
GoogleClientSecret string
|
||||
RedirectURL string
|
||||
)
|
||||
|
||||
func init() {
|
||||
loadConfig()
|
||||
|
||||
// server configuration
|
||||
IsProd = viper.GetString("APP_ENV") == "prod"
|
||||
// AppHost = viper.GetString("APP_HOST")
|
||||
// AppPort = viper.GetInt("APP_PORT")
|
||||
AppHost = viper.GetString("APP_HOST")
|
||||
if AppHost == "" {
|
||||
AppHost = "0.0.0.0"
|
||||
}
|
||||
AppPort = viper.GetInt("APP_PORT")
|
||||
if AppPort == 0 {
|
||||
AppPort = 8080
|
||||
}
|
||||
Version = viper.GetString("VERSION")
|
||||
LogLevel = viper.GetString("LOG_LEVEL")
|
||||
|
||||
// database configuration
|
||||
DBHost = viper.GetString("DB_HOST")
|
||||
DBUser = viper.GetString("DB_USER")
|
||||
DBPassword = viper.GetString("DB_PASSWORD")
|
||||
DBName = viper.GetString("DB_NAME")
|
||||
DBPort = viper.GetInt("DB_PORT")
|
||||
PostgresDSN = viper.GetString("POSTGRES_DSN")
|
||||
if PostgresDSN == "" {
|
||||
PostgresDSN = fmt.Sprintf(
|
||||
"postgres://%s:%s@%s:%d/%s?sslmode=disable",
|
||||
DBUser, DBPassword, DBHost, DBPort, DBName,
|
||||
)
|
||||
}
|
||||
|
||||
// jwt configuration
|
||||
JWTSecret = viper.GetString("JWT_SECRET")
|
||||
JWTAccessExp = viper.GetInt("JWT_ACCESS_EXP_MINUTES")
|
||||
JWTRefreshExp = viper.GetInt("JWT_REFRESH_EXP_DAYS")
|
||||
JWTResetPasswordExp = viper.GetInt("JWT_RESET_PASSWORD_EXP_MINUTES")
|
||||
JWTVerifyEmailExp = viper.GetInt("JWT_VERIFY_EMAIL_EXP_MINUTES")
|
||||
|
||||
// Redis / OIDC
|
||||
RedisURL = viper.GetString("REDIS_URL")
|
||||
if RedisURL == "" {
|
||||
RedisURL = "redis://redis:6379/0"
|
||||
}
|
||||
Issuer = viper.GetString("ISSUER")
|
||||
if Issuer == "" {
|
||||
// fallback ke SSO_ISSUER jika kamu sudah pakai itu sebelumnya
|
||||
Issuer = viper.GetString("SSO_ISSUER")
|
||||
}
|
||||
// SMTP configuration
|
||||
SMTPHost = viper.GetString("SMTP_HOST")
|
||||
SMTPPort = viper.GetInt("SMTP_PORT")
|
||||
SMTPUsername = viper.GetString("SMTP_USERNAME")
|
||||
SMTPPassword = viper.GetString("SMTP_PASSWORD")
|
||||
EmailFrom = viper.GetString("EMAIL_FROM")
|
||||
|
||||
// oauth2 configuration
|
||||
GoogleClientID = viper.GetString("GOOGLE_CLIENT_ID")
|
||||
GoogleClientSecret = viper.GetString("GOOGLE_CLIENT_SECRET")
|
||||
RedirectURL = viper.GetString("REDIRECT_URL")
|
||||
}
|
||||
|
||||
func loadConfig() {
|
||||
viper.AutomaticEnv()
|
||||
|
||||
viper.SetConfigFile(".env")
|
||||
if err := viper.ReadInConfig(); err == nil {
|
||||
utils.Log.Info("Config file loaded from .env")
|
||||
} else {
|
||||
utils.Log.Warn("No .env file found, using environment variables only")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/utils"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func FiberConfig() fiber.Config {
|
||||
return fiber.Config{
|
||||
Prefork: IsProd,
|
||||
CaseSensitive: true,
|
||||
ServerHeader: "Fiber",
|
||||
AppName: "Fiber API",
|
||||
ErrorHandler: utils.ErrorHandler,
|
||||
JSONEncoder: sonic.Marshal,
|
||||
JSONDecoder: sonic.Unmarshal,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
GoogleLoginConfig oauth2.Config
|
||||
}
|
||||
|
||||
var AppConfig Config
|
||||
|
||||
func GoogleConfig() oauth2.Config {
|
||||
AppConfig.GoogleLoginConfig = oauth2.Config{
|
||||
RedirectURL: RedirectURL,
|
||||
ClientID: GoogleClientID,
|
||||
ClientSecret: GoogleClientSecret,
|
||||
Scopes: []string{
|
||||
"https://www.googleapis.com/auth/userinfo.email",
|
||||
"https://www.googleapis.com/auth/userinfo.profile",
|
||||
},
|
||||
Endpoint: google.Endpoint,
|
||||
}
|
||||
|
||||
return AppConfig.GoogleLoginConfig
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package config
|
||||
|
||||
var allRoles = map[string][]string{
|
||||
"user": {},
|
||||
"admin": {"getUsers", "manageUsers"},
|
||||
}
|
||||
|
||||
var Roles = getKeys(allRoles)
|
||||
var RoleRights = allRoles
|
||||
|
||||
func getKeys(m map[string][]string) []string {
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package config
|
||||
|
||||
const (
|
||||
TokenTypeAccess = "access"
|
||||
TokenTypeRefresh = "refresh"
|
||||
TokenTypeResetPassword = "resetPassword"
|
||||
TokenTypeVerifyEmail = "verifyEmail"
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/config"
|
||||
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/utils"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func Connect(dbHost, dbName string) *gorm.DB {
|
||||
dsn := fmt.Sprintf(
|
||||
"host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Shanghai",
|
||||
dbHost, config.DBUser, config.DBPassword, dbName, config.DBPort,
|
||||
)
|
||||
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Info),
|
||||
SkipDefaultTransaction: true,
|
||||
PrepareStmt: true,
|
||||
TranslateError: true,
|
||||
})
|
||||
if err != nil {
|
||||
utils.Log.Errorf("Failed to connect to database: %+v", err)
|
||||
}
|
||||
|
||||
sqlDB, errDB := db.DB()
|
||||
if errDB != nil {
|
||||
utils.Log.Errorf("Failed to connect to database: %+v", errDB)
|
||||
}
|
||||
|
||||
// Config connection pooling
|
||||
sqlDB.SetMaxIdleConns(10)
|
||||
sqlDB.SetMaxOpenConns(100)
|
||||
sqlDB.SetConnMaxLifetime(60 * time.Minute)
|
||||
|
||||
return db
|
||||
}
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
CREATE DATABASE IF NOT EXISTS db_lti_erp;
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS users;
|
||||
@@ -0,0 +1,8 @@
|
||||
-- Users
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
@@ -0,0 +1,30 @@
|
||||
package seed
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
mUser "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func Run(db *gorm.DB) error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
// pw, err := secure.Hash("asdasdasd", nil)
|
||||
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// ===== Users (user) =====
|
||||
user := mUser.User{
|
||||
Name: "Super Admin",
|
||||
}
|
||||
if err := tx.Where("email = ?", user.Id).FirstOrCreate(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("✅ Seeder successfully")
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/config"
|
||||
service "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/services"
|
||||
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/utils"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func Auth(userService service.UserService, requiredRights ...string) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
authHeader := c.Get("Authorization")
|
||||
token := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
|
||||
|
||||
if token == "" {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||
}
|
||||
|
||||
userID, err := utils.VerifyToken(token, config.JWTSecret, config.TokenTypeAccess)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||
}
|
||||
|
||||
user, err := userService.GetOne(c, userID)
|
||||
if err != nil || user == nil {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||
}
|
||||
|
||||
c.Locals("user", user)
|
||||
|
||||
// if len(requiredRights) > 0 {
|
||||
// userRights, hasRights := config.RoleRights[user.Role]
|
||||
// if (!hasRights || !hasAllRights(userRights, requiredRights)) && c.Params("userId") != userID {
|
||||
// return fiber.NewError(fiber.StatusForbidden, "You don't have permission to access this resource")
|
||||
// }
|
||||
// }
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// func hasAllRights(userRights, requiredRights []string) bool {
|
||||
// rightSet := make(map[string]struct{}, len(userRights))
|
||||
// for _, right := range userRights {
|
||||
// rightSet[right] = struct{}{}
|
||||
// }
|
||||
|
||||
// for _, right := range requiredRights {
|
||||
// if _, exists := rightSet[right]; !exists {
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
// return true
|
||||
// }
|
||||
@@ -0,0 +1,12 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
jwtware "github.com/gofiber/contrib/jwt"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func JwtConfig() fiber.Handler {
|
||||
return jwtware.New(jwtware.Config{
|
||||
SigningKey: jwtware.SigningKey{Key: []byte("secret")},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/response"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/limiter"
|
||||
)
|
||||
|
||||
func LimiterConfig() fiber.Handler {
|
||||
return limiter.New(limiter.Config{
|
||||
Max: 20,
|
||||
Expiration: 15 * time.Minute,
|
||||
LimitReached: func(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusTooManyRequests).
|
||||
JSON(response.Common{
|
||||
Code: fiber.StatusTooManyRequests,
|
||||
Status: "error",
|
||||
Message: "Too many requests, please try again later",
|
||||
})
|
||||
},
|
||||
SkipSuccessfulRequests: true,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
)
|
||||
|
||||
func LoggerConfig() fiber.Handler {
|
||||
return logger.New(logger.Config{
|
||||
Format: "${time} ${method} ${status} ${path} in ${latency}\n",
|
||||
TimeFormat: "15:04:05.00",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||
)
|
||||
|
||||
func RecoverConfig() fiber.Handler {
|
||||
return recover.New(recover.Config{
|
||||
EnableStackTrace: true,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package modules
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Module interface {
|
||||
RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/dto"
|
||||
service "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/services"
|
||||
validation "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/validations"
|
||||
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/response"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type UserController struct {
|
||||
UserService service.UserService
|
||||
}
|
||||
|
||||
func NewUserController(userService service.UserService) *UserController {
|
||||
return &UserController{
|
||||
UserService: userService,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UserController) GetAll(c *fiber.Ctx) error {
|
||||
query := &validation.Query{
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Search: c.Query("search", ""),
|
||||
}
|
||||
|
||||
result, totalResults, err := u.UserService.GetAll(c, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.SuccessWithPaginate[dto.UserListDTO]{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get all users successfully",
|
||||
Meta: response.Meta{
|
||||
Page: query.Page,
|
||||
Limit: query.Limit,
|
||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||
TotalResults: totalResults,
|
||||
},
|
||||
Data: dto.ToUserListDTOs(result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *UserController) GetOne(c *fiber.Ctx) error {
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
result, err := u.UserService.GetOne(c, uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get user successfully",
|
||||
Data: dto.ToUserListDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *UserController) CreateOne(c *fiber.Ctx) error {
|
||||
req := new(validation.Create)
|
||||
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
result, err := u.UserService.CreateOne(c, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusCreated,
|
||||
Status: "success",
|
||||
Message: "Create user successfully",
|
||||
Data: dto.ToUserListDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *UserController) UpdateOne(c *fiber.Ctx) error {
|
||||
req := new(validation.Update)
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
result, err := u.UserService.UpdateOne(c, req, uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Update user successfully",
|
||||
Data: dto.ToUserListDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *UserController) DeleteOne(c *fiber.Ctx) error {
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
if err := u.UserService.DeleteOne(c, uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Common{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Delete user successfully",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
model "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/models"
|
||||
)
|
||||
|
||||
// === DTO Structs ===
|
||||
|
||||
type UserListDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type UserDetailDTO struct {
|
||||
UserListDTO
|
||||
}
|
||||
|
||||
// === Mapper Functions ===
|
||||
|
||||
func ToUserListDTO(m model.User) UserListDTO {
|
||||
return UserListDTO{
|
||||
Id: m.Id,
|
||||
Name: m.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func ToUserListDTOs(m []model.User) []UserListDTO {
|
||||
result := make([]UserListDTO, len(m))
|
||||
for i, r := range m {
|
||||
result[i] = ToUserListDTO(r)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"not null"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
rUser "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/repositories"
|
||||
sUser "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/services"
|
||||
)
|
||||
|
||||
type UserModule struct{}
|
||||
|
||||
func (UserModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
UserRoutes(router, userService)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
model "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/models"
|
||||
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/repository"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserRepository interface {
|
||||
repository.BaseRepository[model.User]
|
||||
}
|
||||
|
||||
type UserRepositoryImpl struct {
|
||||
*repository.BaseRepositoryImpl[model.User]
|
||||
}
|
||||
|
||||
func NewUserRepository(db *gorm.DB) UserRepository {
|
||||
return &UserRepositoryImpl{
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[model.User](db),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
controller "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/controllers"
|
||||
user "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func UserRoutes(v1 fiber.Router, s user.UserService) {
|
||||
ctrl := controller.NewUserController(s)
|
||||
|
||||
route := v1.Group("/users")
|
||||
|
||||
route.Get("/", ctrl.GetAll)
|
||||
route.Post("/", ctrl.CreateOne)
|
||||
route.Get("/:id", ctrl.GetOne)
|
||||
route.Patch("/:id", ctrl.UpdateOne)
|
||||
route.Delete("/:id", ctrl.DeleteOne)
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
model "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/models"
|
||||
repository "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/repositories"
|
||||
validation "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users/validations"
|
||||
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/utils"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserService interface {
|
||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]model.User, int64, error)
|
||||
GetOne(ctx *fiber.Ctx, id uint) (*model.User, error)
|
||||
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*model.User, error)
|
||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*model.User, error)
|
||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||
}
|
||||
|
||||
type userService struct {
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
Repository repository.UserRepository
|
||||
}
|
||||
|
||||
func NewUserService(repo repository.UserRepository, validate *validator.Validate) UserService {
|
||||
return &userService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
Repository: repo,
|
||||
}
|
||||
}
|
||||
func (s userService) GetAll(c *fiber.Ctx, params *validation.Query) ([]model.User, int64, error) {
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
|
||||
users, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||
if params.Search != "" {
|
||||
return db.Where("name LIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get users: %+v", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
return users, total, nil
|
||||
}
|
||||
|
||||
func (s userService) GetOne(c *fiber.Ctx, id uint) (*model.User, error) {
|
||||
user, err := s.Repository.GetByID(c.Context(), id, nil)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "User not found")
|
||||
}
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed get user by id: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *userService) CreateOne(c *fiber.Ctx, req *validation.Create) (*model.User, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
createBody := &model.User{
|
||||
Name: req.Name,
|
||||
}
|
||||
|
||||
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create user: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return createBody, nil
|
||||
}
|
||||
|
||||
func (s userService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*model.User, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updateBody := make(map[string]any)
|
||||
|
||||
if req.Name != nil {
|
||||
updateBody["name"] = *req.Name
|
||||
}
|
||||
|
||||
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "User not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to update user: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.GetOne(c, id)
|
||||
}
|
||||
|
||||
func (s userService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "User not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to delete user: %+v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package validation
|
||||
|
||||
type Create struct {
|
||||
Name string `json:"name" validate:"required_strict,min=3"`
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
Name *string `json:"name,omitempty" validate:"omitempty,max=50"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type BaseRepository[T any] interface {
|
||||
GetAll(ctx context.Context, offset, limit int, modifier func(*gorm.DB) *gorm.DB) ([]T, int64, error)
|
||||
GetByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*T, error)
|
||||
GetByIDs(ctx context.Context, ids []uint, modifier func(*gorm.DB) *gorm.DB) ([]T, error)
|
||||
|
||||
CreateOne(ctx context.Context, entity *T, modifier func(*gorm.DB) *gorm.DB) error
|
||||
CreateMany(ctx context.Context, entities []*T, modifier func(*gorm.DB) *gorm.DB) error
|
||||
|
||||
UpdateOne(ctx context.Context, id uint, entity *T, modifier func(*gorm.DB) *gorm.DB) error
|
||||
UpdateMany(ctx context.Context, entities []*T, modifier func(*gorm.DB) *gorm.DB) error
|
||||
PatchOne(ctx context.Context, id uint, updates map[string]any, modifier func(*gorm.DB) *gorm.DB) error
|
||||
|
||||
DeleteOne(ctx context.Context, id uint) error
|
||||
DeleteMany(ctx context.Context, modifier func(*gorm.DB) *gorm.DB) error
|
||||
|
||||
Upsert(ctx context.Context, entity *T, conflictColumns []clause.Column, modifier func(*gorm.DB) *gorm.DB) error
|
||||
|
||||
WithTx(tx *gorm.DB) BaseRepository[T]
|
||||
DB() *gorm.DB
|
||||
}
|
||||
|
||||
type BaseRepositoryImpl[T any] struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewBaseRepository[T any](db *gorm.DB) *BaseRepositoryImpl[T] {
|
||||
return &BaseRepositoryImpl[T]{db: db}
|
||||
}
|
||||
|
||||
func (r *BaseRepositoryImpl[T]) GetAll(
|
||||
ctx context.Context,
|
||||
offset, limit int,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) ([]T, int64, error) {
|
||||
var entities []T
|
||||
var total int64
|
||||
|
||||
q := r.db.WithContext(ctx).Model(new(T))
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err := q.Offset(offset).Limit(limit).Find(&entities).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return entities, total, nil
|
||||
}
|
||||
|
||||
func (r *BaseRepositoryImpl[T]) GetByID(
|
||||
ctx context.Context,
|
||||
id uint,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) (*T, error) {
|
||||
entity := new(T)
|
||||
q := r.db.WithContext(ctx)
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
if err := q.First(entity, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return entity, nil
|
||||
}
|
||||
|
||||
func (r *BaseRepositoryImpl[T]) GetByIDs(
|
||||
ctx context.Context,
|
||||
ids []uint,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) ([]T, error) {
|
||||
var entities []T
|
||||
q := r.db.WithContext(ctx).Model(new(T))
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
if err := q.Where("id IN ?", ids).Find(&entities).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(entities) == 0 {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
return entities, nil
|
||||
}
|
||||
|
||||
// ---- CREATE ----
|
||||
func (r *BaseRepositoryImpl[T]) CreateOne(
|
||||
ctx context.Context,
|
||||
entity *T,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) error {
|
||||
q := r.db.WithContext(ctx)
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
return q.Create(entity).Error
|
||||
}
|
||||
|
||||
func (r *BaseRepositoryImpl[T]) CreateMany(
|
||||
ctx context.Context,
|
||||
entities []*T,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) error {
|
||||
q := r.db.WithContext(ctx)
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
return q.Create(&entities).Error
|
||||
}
|
||||
|
||||
// ---- UPDATE ----
|
||||
func (r *BaseRepositoryImpl[T]) UpdateOne(
|
||||
ctx context.Context,
|
||||
id uint,
|
||||
entity *T,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) error {
|
||||
q := r.db.WithContext(ctx).Model(new(T)).Where("id = ?", id)
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
|
||||
result := q.Updates(entity)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *BaseRepositoryImpl[T]) UpdateMany(
|
||||
ctx context.Context,
|
||||
entities []*T,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) error {
|
||||
q := r.db.WithContext(ctx)
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
|
||||
result := q.Save(&entities)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *BaseRepositoryImpl[T]) PatchOne(
|
||||
ctx context.Context,
|
||||
id uint,
|
||||
updates map[string]any,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) error {
|
||||
q := r.db.WithContext(ctx).Model(new(T)).Where("id = ?", id)
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
|
||||
result := q.Updates(updates)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---- DELETE ----
|
||||
func (r *BaseRepositoryImpl[T]) DeleteOne(ctx context.Context, id uint) error {
|
||||
result := r.db.WithContext(ctx).Delete(new(T), id)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *BaseRepositoryImpl[T]) DeleteMany(ctx context.Context, modifier func(*gorm.DB) *gorm.DB) error {
|
||||
q := r.db.WithContext(ctx).Model(new(T))
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
|
||||
result := q.Delete(new(T))
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---- UPSERT ----
|
||||
func (r *BaseRepositoryImpl[T]) Upsert(
|
||||
ctx context.Context,
|
||||
entity *T,
|
||||
conflictColumns []clause.Column,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) error {
|
||||
q := r.db.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: conflictColumns,
|
||||
UpdateAll: true,
|
||||
})
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
return q.Create(entity).Error
|
||||
}
|
||||
|
||||
func (r *BaseRepositoryImpl[T]) WithTx(tx *gorm.DB) BaseRepository[T] {
|
||||
return &BaseRepositoryImpl[T]{db: tx}
|
||||
}
|
||||
|
||||
func (r *BaseRepositoryImpl[T]) DB() *gorm.DB {
|
||||
return r.db
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func Error(c *fiber.Ctx, statusCode int, message string, details interface{}) error {
|
||||
var errRes error
|
||||
if details != nil {
|
||||
errRes = c.Status(statusCode).JSON(ErrorDetails{
|
||||
Code: statusCode,
|
||||
Status: "error",
|
||||
Message: message,
|
||||
Errors: details,
|
||||
})
|
||||
} else {
|
||||
errRes = c.Status(statusCode).JSON(Common{
|
||||
Code: statusCode,
|
||||
Status: "error",
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
if errRes != nil {
|
||||
logrus.Errorf("Failed to send error response : %+v", errRes)
|
||||
}
|
||||
|
||||
return errRes
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package response
|
||||
|
||||
type Common struct {
|
||||
Code int `json:"code"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type Success struct {
|
||||
Code int `json:"code"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type Meta struct {
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
TotalPages int64 `json:"total_pages"`
|
||||
TotalResults int64 `json:"total_results"`
|
||||
}
|
||||
|
||||
type SuccessWithPaginate[T any] struct {
|
||||
Code int `json:"code"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Meta Meta `json:"meta"`
|
||||
Data []T `json:"data"`
|
||||
}
|
||||
|
||||
type ErrorDetails struct {
|
||||
Code int `json:"code"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Errors interface{} `json:"errors"`
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules"
|
||||
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/validation"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
users "github.com/hafizhproject45/Golang-Boilerplate.git/internal/modules/users"
|
||||
// MODULE IMPORTS
|
||||
)
|
||||
|
||||
func Routes(app *fiber.App, db *gorm.DB) {
|
||||
validate := validation.Validator()
|
||||
api := app.Group("/api")
|
||||
|
||||
// masterRoute.Routes(api, db)
|
||||
|
||||
// root modules di sini
|
||||
allModules := []modules.Module{
|
||||
users.UserModule{},
|
||||
// MODULE REGISTRY
|
||||
}
|
||||
|
||||
// daftarkan root modules
|
||||
for _, m := range allModules {
|
||||
m.RegisterRoutes(api, db, validate)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package utils
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// FlagType
|
||||
// -------------------------------------------------------------------
|
||||
type FlagType string
|
||||
|
||||
const (
|
||||
FlagIsActive FlagType = "IS_ACTIVE"
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Validators
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
func IsValidFlagType(v string) bool {
|
||||
switch FlagType(v) {
|
||||
case FlagIsActive:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// example use
|
||||
|
||||
/**
|
||||
if !utils.IsValidFlagType(req.FlagName) {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "invalid flag type")
|
||||
}
|
||||
*/
|
||||
@@ -0,0 +1,27 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/response"
|
||||
"github.com/hafizhproject45/Golang-Boilerplate.git/internal/validation"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func ErrorHandler(c *fiber.Ctx, err error) error {
|
||||
if errorsMap := validation.CustomErrorMessages(err); len(errorsMap) > 0 {
|
||||
return response.Error(c, fiber.StatusBadRequest, "Bad Request", errorsMap)
|
||||
}
|
||||
|
||||
var fiberErr *fiber.Error
|
||||
if errors.As(err, &fiberErr) {
|
||||
return response.Error(c, fiberErr.Code, fiberErr.Message, nil)
|
||||
}
|
||||
|
||||
return response.Error(c, fiber.StatusInternalServerError, "Internal Server Error", nil)
|
||||
}
|
||||
|
||||
func NotFoundHandler(c *fiber.Ctx) error {
|
||||
return response.Error(c, fiber.StatusNotFound, "Endpoint Not Found", nil)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type CustomFormatter struct {
|
||||
logrus.TextFormatter
|
||||
}
|
||||
|
||||
var Log *logrus.Logger
|
||||
|
||||
func init() {
|
||||
Log = logrus.New()
|
||||
|
||||
// Set logger to use the custom text formatter
|
||||
Log.SetFormatter(&CustomFormatter{
|
||||
TextFormatter: logrus.TextFormatter{
|
||||
TimestampFormat: "15:04:05.000",
|
||||
FullTimestamp: true,
|
||||
ForceColors: true,
|
||||
},
|
||||
})
|
||||
|
||||
Log.SetOutput(os.Stdout)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package utils
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type NullString struct {
|
||||
Set bool
|
||||
Value *string
|
||||
}
|
||||
|
||||
func (ns *NullString) UnmarshalJSON(b []byte) error {
|
||||
ns.Set = true
|
||||
if string(b) == "null" {
|
||||
ns.Value = nil
|
||||
return nil
|
||||
}
|
||||
var s string
|
||||
if err := json.Unmarshal(b, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
ns.Value = &s
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package secure
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/argon2"
|
||||
)
|
||||
|
||||
type Params struct {
|
||||
Memory uint32
|
||||
Time uint32
|
||||
Threads uint8
|
||||
SaltLen uint32
|
||||
KeyLen uint32
|
||||
}
|
||||
|
||||
var Default = &Params{
|
||||
Memory: 64 * 1024,
|
||||
Time: 3,
|
||||
Threads: 2,
|
||||
SaltLen: 16,
|
||||
KeyLen: 32,
|
||||
}
|
||||
|
||||
func Hash(plain string, p *Params) (string, error) {
|
||||
if strings.TrimSpace(plain) == "" {
|
||||
return "", errors.New("empty password")
|
||||
}
|
||||
if p == nil { p = Default }
|
||||
|
||||
salt := make([]byte, p.SaltLen)
|
||||
if _, err := rand.Read(salt); err != nil { return "", err }
|
||||
|
||||
key := argon2.IDKey([]byte(plain), salt, p.Time, p.Memory, p.Threads, p.KeyLen)
|
||||
return fmt.Sprintf("$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s",
|
||||
p.Memory, p.Time, p.Threads,
|
||||
base64.RawStdEncoding.EncodeToString(salt),
|
||||
base64.RawStdEncoding.EncodeToString(key),
|
||||
), nil
|
||||
}
|
||||
|
||||
func Verify(encoded, plain string) bool {
|
||||
parts := strings.Split(encoded, "$")
|
||||
if len(parts) != 6 { return false }
|
||||
|
||||
var m uint32; var t uint32; var p uint8
|
||||
if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &m, &t, &p); err != nil { return false }
|
||||
|
||||
salt, err := base64.RawStdEncoding.DecodeString(parts[4]); if err != nil { return false }
|
||||
want, err := base64.RawStdEncoding.DecodeString(parts[5]); if err != nil { return false }
|
||||
|
||||
got := argon2.IDKey([]byte(plain), salt, t, m, p, uint32(len(want)))
|
||||
return subtle.ConstantTimeCompare(want, got) == 1
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package secure
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
)
|
||||
|
||||
func RandomToken(n int) (string, error) {
|
||||
// n = bytes, 32 → 256-bit
|
||||
b := make([]byte, n)
|
||||
if _, err := rand.Read(b); err != nil { return "", err }
|
||||
return base64.RawURLEncoding.EncodeToString(b), nil
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package secure
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
func SHA256Hex(s string) string {
|
||||
h := sha256.Sum256([]byte(s))
|
||||
return hex.EncodeToString(h[:])
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
func VerifyToken(tokenStr, secret, tokenType string) (uint, error) {
|
||||
token, err := jwt.Parse(tokenStr, func(_ *jwt.Token) (interface{}, error) {
|
||||
return []byte(secret), nil
|
||||
})
|
||||
if err != nil || !token.Valid {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return 0, errors.New("invalid token claims")
|
||||
}
|
||||
|
||||
jwtType, ok := claims["type"].(string)
|
||||
if !ok || jwtType != tokenType {
|
||||
return 0, errors.New("invalid token type")
|
||||
}
|
||||
|
||||
sub, ok := claims["sub"]
|
||||
if !ok {
|
||||
return 0, errors.New("invalid token sub")
|
||||
}
|
||||
|
||||
switch v := sub.(type) {
|
||||
case float64:
|
||||
return uint(v), nil
|
||||
case string:
|
||||
id, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return 0, errors.New("invalid sub format")
|
||||
}
|
||||
return uint(id), nil
|
||||
default:
|
||||
return 0, errors.New("unsupported sub type")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
var (
|
||||
reUpper = regexp.MustCompile(`[A-Z]`)
|
||||
reLower = regexp.MustCompile(`[a-z]`)
|
||||
reDigit = regexp.MustCompile(`[0-9]`)
|
||||
reSym = regexp.MustCompile(`[^A-Za-z0-9]`)
|
||||
)
|
||||
|
||||
func Password(fl validator.FieldLevel) bool {
|
||||
pw := fl.Field().String()
|
||||
pw = strings.TrimSpace(pw)
|
||||
|
||||
if len(pw) < 8 {
|
||||
return false
|
||||
}
|
||||
if !reUpper.MatchString(pw) {
|
||||
return false
|
||||
}
|
||||
if !reLower.MatchString(pw) {
|
||||
return false
|
||||
}
|
||||
if !reDigit.MatchString(pw) {
|
||||
return false
|
||||
}
|
||||
if !reSym.MatchString(pw) {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(pw, " ") {
|
||||
return false
|
||||
}
|
||||
|
||||
parent := fl.Parent()
|
||||
if parent.IsValid() && parent.Kind() == reflect.Struct {
|
||||
emailField := parent.FieldByName("Email")
|
||||
if emailField.IsValid() && emailField.Kind() == reflect.String {
|
||||
if email := emailField.String(); email != "" {
|
||||
if i := strings.IndexByte(email, '@'); i > 0 {
|
||||
local := strings.ToLower(email[:i])
|
||||
if local != "" && strings.Contains(strings.ToLower(pw), local) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func RequiredStrict(fl validator.FieldLevel) bool {
|
||||
field := fl.Field()
|
||||
|
||||
switch field.Kind() {
|
||||
case reflect.String:
|
||||
return field.String() != ""
|
||||
case reflect.Ptr:
|
||||
return !field.IsNil()
|
||||
}
|
||||
|
||||
return field.IsValid() && !field.IsZero()
|
||||
}
|
||||
|
||||
func OmitemptyStrict(fl validator.FieldLevel) bool {
|
||||
field := fl.Field()
|
||||
|
||||
if !field.IsValid() || field.IsZero() {
|
||||
return true
|
||||
}
|
||||
|
||||
if field.Kind() == reflect.String {
|
||||
return field.String() != ""
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
var customMessages = map[string]string{
|
||||
"required": "Field %s is required",
|
||||
"required_strict": "Field %s is required and cannot be null or empty",
|
||||
"omitempty_strict": "Field %s cannot be null or empty when provided",
|
||||
|
||||
"email": "Invalid email address for field %s",
|
||||
"min": "Field %s must have a minimum length of %s characters",
|
||||
"max": "Field %s must have a maximum length of %s characters",
|
||||
"len": "Field %s must be exactly %s characters long",
|
||||
"number": "Field %s must be a number",
|
||||
"positive": "Field %s must be a positive number",
|
||||
"alphanum": "Field %s must contain only alphanumeric characters",
|
||||
"oneof": "Invalid value for field %s",
|
||||
"password": "Field %s must be at least 8 characters, contain uppercase, lowercase, number, and special character",
|
||||
}
|
||||
|
||||
func CustomErrorMessages(err error) map[string]string {
|
||||
var validationErrors validator.ValidationErrors
|
||||
if errors.As(err, &validationErrors) {
|
||||
return generateErrorMessages(validationErrors)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateErrorMessages(validationErrors validator.ValidationErrors) map[string]string {
|
||||
errorsMap := make(map[string]string)
|
||||
for _, err := range validationErrors {
|
||||
fieldName := err.StructNamespace()
|
||||
tag := err.Tag()
|
||||
|
||||
customMessage := customMessages[tag]
|
||||
if customMessage != "" {
|
||||
errorsMap[fieldName] = formatErrorMessage(customMessage, err, tag)
|
||||
} else {
|
||||
errorsMap[fieldName] = defaultErrorMessage(err)
|
||||
}
|
||||
}
|
||||
return errorsMap
|
||||
}
|
||||
|
||||
func formatErrorMessage(customMessage string, err validator.FieldError, tag string) string {
|
||||
if tag == "min" || tag == "max" || tag == "len" {
|
||||
return fmt.Sprintf(customMessage, err.Field(), err.Param())
|
||||
}
|
||||
return fmt.Sprintf(customMessage, err.Field())
|
||||
}
|
||||
|
||||
func defaultErrorMessage(err validator.FieldError) string {
|
||||
return fmt.Sprintf("Field validation for '%s' failed on the '%s' tag", err.Field(), err.Tag())
|
||||
}
|
||||
|
||||
func Validator() *validator.Validate {
|
||||
validate := validator.New()
|
||||
|
||||
if err := validate.RegisterValidation("password", Password); err != nil {
|
||||
return nil
|
||||
}
|
||||
if err := validate.RegisterValidation("required_strict", RequiredStrict); err != nil {
|
||||
return nil
|
||||
}
|
||||
if err := validate.RegisterValidation("omitempty_strict", OmitemptyStrict); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return validate
|
||||
}
|
||||
Reference in New Issue
Block a user