Refactor configuration and database initialization: replace LoadConfig with GetConfig, introduce DbConfig struct, and streamline token service retrieval. Remove unused request types and enhance user refresh token handling.
This commit is contained in:
parent
3731e6e409
commit
5b9fe6c9b7
|
|
@ -36,20 +36,12 @@ func main() {
|
||||||
seedCount := flag.Int("seed", 0, "seed N fake users at startup (0 to skip)")
|
seedCount := flag.Int("seed", 0, "seed N fake users at startup (0 to skip)")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
cfg, err := config.LoadConfig()
|
cfg, err := config.GetConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("load config: %v", err)
|
log.Fatalf("config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if secret := os.Getenv("AUTH_SECRET"); secret != "" {
|
dbConn, err := db.GetDB()
|
||||||
cfg.Auth.Secret = secret
|
|
||||||
}
|
|
||||||
|
|
||||||
dbCfg := db.Config{
|
|
||||||
Driver: envOrDefault("DB_driver", "sqlite"),
|
|
||||||
DSN: envOrDefault("DB_dsn", "file:./data/data.db?_foreign_keys=on"),
|
|
||||||
}
|
|
||||||
dbConn, err := db.Init(dbCfg)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("init db: %v", err)
|
log.Fatalf("init db: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -58,12 +50,10 @@ func main() {
|
||||||
log.Fatalf("migrate user: %v", err)
|
log.Fatalf("migrate user: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tockenService, _ := tokens.NewTockenService(tokens.Config{
|
tokenService, err := tokens.GetTockenService()
|
||||||
Secret: "your-secret-key",
|
if err != nil {
|
||||||
Issuer: "your-issuer",
|
log.Fatalf("init tokens: %v", err)
|
||||||
AccessTokenExpiry: time.Hour,
|
}
|
||||||
RefreshTokenExpiry: 24 * time.Hour,
|
|
||||||
})
|
|
||||||
|
|
||||||
app := fiber.New(fiber.Config{
|
app := fiber.New(fiber.Config{
|
||||||
AppName: cfg.AppName,
|
AppName: cfg.AppName,
|
||||||
|
|
@ -107,7 +97,7 @@ func main() {
|
||||||
return c.Next()
|
return c.Next()
|
||||||
})
|
})
|
||||||
|
|
||||||
app.Use(roles.RequireEndpointPermission(dbConn, tockenService))
|
app.Use(roles.RequireEndpointPermission(dbConn, tokenService))
|
||||||
|
|
||||||
routes.Register(app)
|
routes.Register(app)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
package auth
|
|
||||||
|
|
||||||
// Typescript: interface
|
|
||||||
type LoginRequest struct {
|
|
||||||
Username string `json:"username" validate:"required,email"`
|
|
||||||
Password string `json:"password" validate:"required,min=8,max=128"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Typescript: interface
|
|
||||||
type RefreshRequest struct {
|
|
||||||
RefreshToken string `json:"refresh_token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Typescript: interface
|
|
||||||
type ForgotPasswordRequest struct {
|
|
||||||
Email string `json:"email" validate:"required,email"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Typescript: interface
|
|
||||||
type ResetPasswordRequest struct {
|
|
||||||
Token string `json:"token" validate:"required,min=20,max=255"`
|
|
||||||
Password string `json:"password" validate:"required,min=8,max=128"`
|
|
||||||
}
|
|
||||||
|
|
@ -14,6 +14,7 @@ type ServerConfig struct {
|
||||||
DisableStartupMessage bool `json:"disable_startup_message"`
|
DisableStartupMessage bool `json:"disable_startup_message"`
|
||||||
Auth AuthConfig `json:"auth"`
|
Auth AuthConfig `json:"auth"`
|
||||||
Mail MailConfig `json:"mail"`
|
Mail MailConfig `json:"mail"`
|
||||||
|
Db DbConfig `json:"db_config"`
|
||||||
RolesConfigPath string `json:"roles_config_path"`
|
RolesConfigPath string `json:"roles_config_path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,6 +43,25 @@ type SMTPMailConfig struct {
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DbConfig struct {
|
||||||
|
Driver string
|
||||||
|
DSN string
|
||||||
|
}
|
||||||
|
|
||||||
|
var Config *ServerConfig = nil
|
||||||
|
|
||||||
|
func GetConfig() (*ServerConfig, error) {
|
||||||
|
if Config == nil {
|
||||||
|
var err error
|
||||||
|
Config, err = loadConfig()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to load config: %v\n", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Config, nil
|
||||||
|
}
|
||||||
|
|
||||||
func envOrDefault(key, defaultValue string) string {
|
func envOrDefault(key, defaultValue string) string {
|
||||||
if value, exists := os.LookupEnv(key); exists {
|
if value, exists := os.LookupEnv(key); exists {
|
||||||
return value
|
return value
|
||||||
|
|
@ -49,26 +69,28 @@ func envOrDefault(key, defaultValue string) string {
|
||||||
return defaultValue
|
return defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig() (ServerConfig, error) {
|
func loadConfig() (*ServerConfig, error) {
|
||||||
|
|
||||||
path := envOrDefault("CONFIG_PATH", "configs/config.json")
|
path := envOrDefault("CONFIG_PATH", "configs/config.json")
|
||||||
|
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ServerConfig{}, fmt.Errorf("read config: %w", err)
|
return nil, fmt.Errorf("read config: %w", err)
|
||||||
}
|
}
|
||||||
var cfg ServerConfig
|
var cfg ServerConfig
|
||||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||||
return ServerConfig{}, fmt.Errorf("parse config: %w", err)
|
return nil, fmt.Errorf("parse config: %w", err)
|
||||||
|
}
|
||||||
|
if secret := os.Getenv("AUTH_SECRET"); secret != "" {
|
||||||
|
cfg.Auth.Secret = secret
|
||||||
}
|
}
|
||||||
if cfg.Auth.Secret == "" {
|
if cfg.Auth.Secret == "" {
|
||||||
return ServerConfig{}, fmt.Errorf("auth.secret must be set")
|
return nil, fmt.Errorf("auth.secret must be set")
|
||||||
}
|
}
|
||||||
if cfg.Auth.AccessTokenExpiryMinutes <= 0 {
|
if cfg.Auth.AccessTokenExpiryMinutes <= 0 {
|
||||||
return ServerConfig{}, fmt.Errorf("auth.access_token_expiry_minutes must be greater than zero")
|
return nil, fmt.Errorf("auth.access_token_expiry_minutes must be greater than zero")
|
||||||
}
|
}
|
||||||
if cfg.Auth.RefreshTokenExpiryMinutes <= 0 {
|
if cfg.Auth.RefreshTokenExpiryMinutes <= 0 {
|
||||||
return ServerConfig{}, fmt.Errorf("auth.refresh_token_expiry_minutes must be greater than zero")
|
return nil, fmt.Errorf("auth.refresh_token_expiry_minutes must be greater than zero")
|
||||||
}
|
}
|
||||||
if cfg.Mail.Mode == "" {
|
if cfg.Mail.Mode == "" {
|
||||||
cfg.Mail.Mode = "file"
|
cfg.Mail.Mode = "file"
|
||||||
|
|
@ -80,21 +102,26 @@ func LoadConfig() (ServerConfig, error) {
|
||||||
cfg.Mail.ResetPasswordPath = "/#reset-password"
|
cfg.Mail.ResetPasswordPath = "/#reset-password"
|
||||||
}
|
}
|
||||||
if cfg.Mail.Mode != "smtp" && cfg.Mail.Mode != "file" {
|
if cfg.Mail.Mode != "smtp" && cfg.Mail.Mode != "file" {
|
||||||
return ServerConfig{}, fmt.Errorf("mail.mode must be either smtp or file")
|
return nil, fmt.Errorf("mail.mode must be either smtp or file")
|
||||||
}
|
}
|
||||||
if cfg.Mail.From == "" {
|
if cfg.Mail.From == "" {
|
||||||
return ServerConfig{}, fmt.Errorf("mail.from must be set")
|
return nil, fmt.Errorf("mail.from must be set")
|
||||||
}
|
}
|
||||||
if cfg.Mail.Mode == "smtp" {
|
if cfg.Mail.Mode == "smtp" {
|
||||||
if cfg.Mail.SMTP.Host == "" {
|
if cfg.Mail.SMTP.Host == "" {
|
||||||
return ServerConfig{}, fmt.Errorf("mail.smtp.host must be set when mail.mode=smtp")
|
return nil, fmt.Errorf("mail.smtp.host must be set when mail.mode=smtp")
|
||||||
}
|
}
|
||||||
if cfg.Mail.SMTP.Port <= 0 {
|
if cfg.Mail.SMTP.Port <= 0 {
|
||||||
return ServerConfig{}, fmt.Errorf("mail.smtp.port must be greater than zero when mail.mode=smtp")
|
return nil, fmt.Errorf("mail.smtp.port must be greater than zero when mail.mode=smtp")
|
||||||
}
|
}
|
||||||
} else if cfg.Mail.DebugDir == "" {
|
} else if cfg.Mail.DebugDir == "" {
|
||||||
cfg.Mail.DebugDir = "data/mail-debug"
|
cfg.Mail.DebugDir = "data/mail-debug"
|
||||||
}
|
}
|
||||||
|
|
||||||
return cfg, nil
|
cfg.Db = DbConfig{
|
||||||
|
Driver: envOrDefault("DB_driver", "sqlite"),
|
||||||
|
DSN: envOrDefault("DB_dsn", "file:./data/data.db?_foreign_keys=on"),
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cfg, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"server/internal/config"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
|
|
@ -12,33 +13,42 @@ import (
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Driver string
|
|
||||||
DSN string
|
|
||||||
}
|
|
||||||
|
|
||||||
var DB *gorm.DB
|
var DB *gorm.DB
|
||||||
|
|
||||||
|
// GetDB returns the global *gorm.DB instance. It panics if the database is not initialized.
|
||||||
|
func GetDB() (*gorm.DB, error) {
|
||||||
|
if DB == nil {
|
||||||
|
cfg, err := config.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
DB, err = InitDB(cfg.Db)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to initialize database: %v\n", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DB, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Init opens the configured database connection and runs schema migrations.
|
// Init opens the configured database connection and runs schema migrations.
|
||||||
func Init(cfg Config) (*gorm.DB, error) {
|
func InitDB(cfg config.DbConfig) (*gorm.DB, error) {
|
||||||
switch cfg.Driver {
|
switch cfg.Driver {
|
||||||
case "sqlite":
|
case "sqlite":
|
||||||
if err := ensureSQLiteDir(cfg.DSN); err != nil {
|
if err := ensureSQLiteDir(cfg.DSN); err != nil {
|
||||||
return nil, fmt.Errorf("prepare sqlite path: %w", err)
|
return nil, fmt.Errorf("prepare sqlite path: %w", err)
|
||||||
}
|
}
|
||||||
db, err := gorm.Open(sqlite.Open(cfg.DSN), &gorm.Config{})
|
DB, err := gorm.Open(sqlite.Open(cfg.DSN), &gorm.Config{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("open sqlite: %w", err)
|
return nil, fmt.Errorf("open sqlite: %w", err)
|
||||||
}
|
}
|
||||||
DB = db
|
return DB, nil
|
||||||
return db, nil
|
|
||||||
case "postgres":
|
case "postgres":
|
||||||
db, err := gorm.Open(postgres.Open(cfg.DSN), &gorm.Config{})
|
DB, err := gorm.Open(postgres.Open(cfg.DSN), &gorm.Config{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("open postgres: %w", err)
|
return nil, fmt.Errorf("open postgres: %w", err)
|
||||||
}
|
}
|
||||||
DB = db
|
return DB, nil
|
||||||
return db, nil
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported driver %q", cfg.Driver)
|
return nil, fmt.Errorf("unsupported driver %q", cfg.Driver)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
|
||||||
"net"
|
"net"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -42,7 +41,7 @@ type Message struct {
|
||||||
TemplateData any
|
TemplateData any
|
||||||
}
|
}
|
||||||
|
|
||||||
type Service struct {
|
type MailService struct {
|
||||||
cfg Config
|
cfg Config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,11 +53,12 @@ type TemplateData struct {
|
||||||
ResetToken string
|
ResetToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() (*Service, error) {
|
// if service fail send admin allert instead a response to user or a simple response server error.
|
||||||
|
func New() (*MailService, error) {
|
||||||
|
|
||||||
serverCfg, err := config.LoadConfig()
|
serverCfg, err := config.GetConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("load config: %v", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := Config{
|
cfg := Config{
|
||||||
|
|
@ -102,10 +102,10 @@ func New() (*Service, error) {
|
||||||
return nil, fmt.Errorf("smtp host and port are required")
|
return nil, fmt.Errorf("smtp host and port are required")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &Service{cfg: cfg}, nil
|
return &MailService{cfg: cfg}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Send(ctx context.Context, msg Message) error {
|
func (s *MailService) Send(ctx context.Context, msg Message) error {
|
||||||
htmlBody, textBody, err := s.renderBodies(msg.Template, msg.TemplateData)
|
htmlBody, textBody, err := s.renderBodies(msg.Template, msg.TemplateData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -122,7 +122,7 @@ func (s *Service) Send(ctx context.Context, msg Message) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ResetLink(token string) string {
|
func (s *MailService) ResetLink(token string) string {
|
||||||
base := strings.TrimRight(s.cfg.FrontendBaseURL, "/")
|
base := strings.TrimRight(s.cfg.FrontendBaseURL, "/")
|
||||||
path := s.cfg.ResetPasswordPath
|
path := s.cfg.ResetPasswordPath
|
||||||
if path == "" {
|
if path == "" {
|
||||||
|
|
@ -137,11 +137,11 @@ func (s *Service) ResetLink(token string) string {
|
||||||
return base + path + "?token=" + token
|
return base + path + "?token=" + token
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) AppName() string {
|
func (s *MailService) AppName() string {
|
||||||
return s.cfg.AppName
|
return s.cfg.AppName
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) renderBodies(templateName string, data any) (string, string, error) {
|
func (s *MailService) renderBodies(templateName string, data any) (string, string, error) {
|
||||||
htmlPath := filepath.Join(s.cfg.TemplatesDir, templateName+".html.tmpl")
|
htmlPath := filepath.Join(s.cfg.TemplatesDir, templateName+".html.tmpl")
|
||||||
textPath := filepath.Join(s.cfg.TemplatesDir, templateName+".txt.tmpl")
|
textPath := filepath.Join(s.cfg.TemplatesDir, templateName+".txt.tmpl")
|
||||||
|
|
||||||
|
|
@ -195,7 +195,7 @@ func buildMessage(from, to, subject, textBody, htmlBody string) []byte {
|
||||||
return []byte(strings.Join(append(headers, body...), "\r\n"))
|
return []byte(strings.Join(append(headers, body...), "\r\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) sendSMTP(ctx context.Context, to string, raw []byte) error {
|
func (s *MailService) sendSMTP(ctx context.Context, to string, raw []byte) error {
|
||||||
addr := fmt.Sprintf("%s:%d", s.cfg.SMTP.Host, s.cfg.SMTP.Port)
|
addr := fmt.Sprintf("%s:%d", s.cfg.SMTP.Host, s.cfg.SMTP.Port)
|
||||||
dialer := &net.Dialer{}
|
dialer := &net.Dialer{}
|
||||||
conn, err := dialer.DialContext(ctx, "tcp", addr)
|
conn, err := dialer.DialContext(ctx, "tcp", addr)
|
||||||
|
|
@ -245,7 +245,7 @@ func (s *Service) sendSMTP(ctx context.Context, to string, raw []byte) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) writeDebugMail(to, subject string, raw []byte) error {
|
func (s *MailService) writeDebugMail(to, subject string, raw []byte) error {
|
||||||
safeRecipient := strings.NewReplacer("@", "_at_", "/", "_", "\\", "_", ":", "_", " ", "_").Replace(to)
|
safeRecipient := strings.NewReplacer("@", "_at_", "/", "_", "\\", "_", ":", "_", " ", "_").Replace(to)
|
||||||
filename := fmt.Sprintf("%d_%s.eml", time.Now().UnixNano(), safeRecipient)
|
filename := fmt.Sprintf("%d_%s.eml", time.Now().UnixNano(), safeRecipient)
|
||||||
path := filepath.Join(s.cfg.DebugDir, filename)
|
path := filepath.Join(s.cfg.DebugDir, filename)
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,9 @@ type SimpleResponse struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// success wraps a payload in the standard API envelope.
|
// success wraps a payload in the standard API envelope.
|
||||||
func success(data any) fiber.Map {
|
func Success(data any) fiber.Map {
|
||||||
return fiber.Map{
|
return fiber.Map{
|
||||||
"data": data,
|
"data": data,
|
||||||
"error": nil,
|
"error": nil,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Success(data any) fiber.Map {
|
|
||||||
return success(data)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,6 @@ import (
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Typescript: interface
|
|
||||||
type FormRequest struct {
|
|
||||||
Req string `json:"req"`
|
|
||||||
Count int `json:"count"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Typescript: interface
|
|
||||||
type FormResponse struct {
|
|
||||||
Test string `json:"test"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func Register(app *fiber.App) {
|
func Register(app *fiber.App) {
|
||||||
systemUtils.RegisterSystemRoutes(app)
|
systemUtils.RegisterSystemRoutes(app)
|
||||||
users.RegisterUserRoutes(app)
|
users.RegisterUserRoutes(app)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
|
"server/internal/config"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
|
|
@ -13,19 +14,12 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type TockenService struct {
|
type TockenService struct {
|
||||||
cfg Config
|
cfg config.AuthConfig
|
||||||
secret []byte
|
secret []byte
|
||||||
accessExpiry time.Duration
|
accessExpiry time.Duration
|
||||||
refreshExpiry time.Duration
|
refreshExpiry time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
Secret string
|
|
||||||
Issuer string
|
|
||||||
AccessTokenExpiry time.Duration
|
|
||||||
RefreshTokenExpiry time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
type Claims struct {
|
type Claims struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
|
|
@ -44,22 +38,38 @@ const (
|
||||||
TokenTypeRefresh = "refresh"
|
TokenTypeRefresh = "refresh"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewTockenService(cfg Config) (*TockenService, error) {
|
var Tockens *TockenService
|
||||||
|
|
||||||
|
func GetTockenService() (*TockenService, error) {
|
||||||
|
if Tockens == nil {
|
||||||
|
cfg, err := config.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
Tockens, err = NewTockenService(cfg.Auth)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Tockens, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTockenService(cfg config.AuthConfig) (*TockenService, error) {
|
||||||
if cfg.Secret == "" {
|
if cfg.Secret == "" {
|
||||||
return nil, errors.New("jwt secret is required")
|
return nil, errors.New("jwt secret is required")
|
||||||
}
|
}
|
||||||
if cfg.AccessTokenExpiry <= 0 {
|
if cfg.AccessTokenExpiryMinutes <= 0 {
|
||||||
return nil, errors.New("access token expiry must be positive")
|
return nil, errors.New("access token expiry must be positive")
|
||||||
}
|
}
|
||||||
if cfg.RefreshTokenExpiry <= 0 {
|
if cfg.RefreshTokenExpiryMinutes <= 0 {
|
||||||
return nil, errors.New("refresh token expiry must be positive")
|
return nil, errors.New("refresh token expiry must be positive")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &TockenService{
|
return &TockenService{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
secret: []byte(cfg.Secret),
|
secret: []byte(cfg.Secret),
|
||||||
accessExpiry: cfg.AccessTokenExpiry,
|
accessExpiry: time.Duration(cfg.AccessTokenExpiryMinutes) * time.Minute,
|
||||||
refreshExpiry: cfg.RefreshTokenExpiry,
|
refreshExpiry: time.Duration(cfg.RefreshTokenExpiryMinutes) * time.Minute,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
"server/internal/config"
|
|
||||||
tsrpc "server/pkg/ts-rpc"
|
tsrpc "server/pkg/ts-rpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -32,14 +30,6 @@ func TsGenerate() (string, error) {
|
||||||
return "", fmt.Errorf("write local generated typescript: %w", err)
|
return "", fmt.Errorf("write local generated typescript: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
configPath := os.Getenv("CONFIG_PATH")
|
|
||||||
if configPath == "" {
|
|
||||||
configPath = "configs/config.json"
|
|
||||||
}
|
|
||||||
if _, err := config.LoadConfig(); err != nil {
|
|
||||||
return "", fmt.Errorf("load config from %s: %w", configPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
frontendAPIPath := os.Getenv("FRONTEND_API_PATH")
|
frontendAPIPath := os.Getenv("FRONTEND_API_PATH")
|
||||||
if frontendAPIPath == "" {
|
if frontendAPIPath == "" {
|
||||||
return "", errors.New("FRONTEND_API_PATH must be set")
|
return "", errors.New("FRONTEND_API_PATH must be set")
|
||||||
|
|
|
||||||
|
|
@ -670,13 +670,25 @@ func (uc *UserController) Me(c fiber.Ctx) error {
|
||||||
return c.JSON(responses.Success(&user))
|
return c.JSON(responses.Success(&user))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (us *UserController) Refresh(refreshToken string) (tokens.TokenPair, error) {
|
func (us *UserController) Refresh(c fiber.Ctx) error {
|
||||||
claims, err := us.TockenService.ParseToken(refreshToken)
|
var req RefreshRequest
|
||||||
|
if err := c.Bind().Body(&req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "invalid payload")
|
||||||
|
}
|
||||||
|
if req.RefreshToken == "" {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "refresh_token is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, err := us.TockenService.ParseToken(req.RefreshToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return tokens.TokenPair{}, err
|
return fiber.NewError(fiber.StatusUnauthorized, err.Error())
|
||||||
}
|
}
|
||||||
if claims.TokenType != tokens.TokenTypeRefresh {
|
if claims.TokenType != tokens.TokenTypeRefresh {
|
||||||
return tokens.TokenPair{}, errors.New("refresh token required")
|
return fiber.NewError(fiber.StatusUnauthorized, "refresh token required")
|
||||||
}
|
}
|
||||||
return us.TockenService.GenerateTokenPair(claims.Username)
|
tokens, err := us.TockenService.GenerateTokenPair(claims.Username)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||||
|
}
|
||||||
|
return c.JSON(responses.Success(tokens))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package users
|
package users
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"server/internal/roles"
|
"server/internal/roles"
|
||||||
"server/internal/tokens"
|
"server/internal/tokens"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -10,12 +11,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func RegisterUserRoutes(app *fiber.App) {
|
func RegisterUserRoutes(app *fiber.App) {
|
||||||
tockenService, _ := tokens.NewTockenService(tokens.Config{
|
tockenService, err := tokens.GetTockenService()
|
||||||
Secret: "your-secret-key",
|
if err != nil {
|
||||||
Issuer: "your-issuer",
|
panic(fmt.Sprintf("token service: %v", err))
|
||||||
AccessTokenExpiry: time.Hour,
|
}
|
||||||
RefreshTokenExpiry: 24 * time.Hour,
|
|
||||||
})
|
|
||||||
|
|
||||||
authRateLimiter := limiter.New(limiter.Config{
|
authRateLimiter := limiter.New(limiter.Config{
|
||||||
Max: 10,
|
Max: 10,
|
||||||
|
|
@ -25,37 +24,37 @@ func RegisterUserRoutes(app *fiber.App) {
|
||||||
|
|
||||||
userController := NewUserController(tockenService)
|
userController := NewUserController(tockenService)
|
||||||
|
|
||||||
// Typescript: TSEndpoint= path=/users/:uuid; name=getUser; method=GET; response=models.UserProfile
|
// Typescript: TSEndpoint= path=/users/:uuid; name=getUser; method=GET; response=users.UserProfile
|
||||||
app.Get("/users/:uuid", tockenService.Middleware(), userController.GetUser)
|
app.Get("/users/:uuid", tockenService.Middleware(), userController.GetUser)
|
||||||
|
|
||||||
// Typescript: TSEndpoint= path=/users; name=createUser; method=POST; request=models.UserCreateInput; response=models.UserProfile
|
// Typescript: TSEndpoint= path=/users; name=createUser; method=POST; request=users.UserCreateInput; response=users.UserProfile
|
||||||
app.Post("/users", tockenService.Middleware(), userController.CreateUser)
|
app.Post("/users", tockenService.Middleware(), userController.CreateUser)
|
||||||
|
|
||||||
// Typescript: TSEndpoint= path=/users/:uuid; name=updateUser; method=PUT; request=controllers.UpdateUserRequest; response=models.UserProfile
|
// Typescript: TSEndpoint= path=/users/:uuid; name=updateUser; method=PUT; request=users.UpdateUserRequest; response=users.UserProfile
|
||||||
app.Put("/users/:uuid", tockenService.Middleware(), userController.UpdateUser)
|
app.Put("/users/:uuid", tockenService.Middleware(), userController.UpdateUser)
|
||||||
|
|
||||||
// Typescript: TSEndpoint= path=/users/:uuid; name=deleteUser; method=DELETE; response=controllers.SimpleResponse
|
// Typescript: TSEndpoint= path=/users/:uuid; name=deleteUser; method=DELETE; response=responses.SimpleResponse
|
||||||
app.Delete("/users/:uuid", tockenService.Middleware(), userController.DeleteUser)
|
app.Delete("/users/:uuid", tockenService.Middleware(), userController.DeleteUser)
|
||||||
|
|
||||||
// Typescript: TSEndpoint= path=/auth/me; name=me; method=GET; response=models.UserShort
|
// Typescript: TSEndpoint= path=/auth/me; name=me; method=GET; response=users.User
|
||||||
app.Get("/auth/me", tockenService.Middleware(), userController.Me)
|
app.Get("/auth/me", tockenService.Middleware(), userController.Me)
|
||||||
roles.RegisterEndpoint("GET/auth/me", int(roles.UserPermission))
|
roles.RegisterEndpoint("GET/auth/me", int(roles.UserPermission))
|
||||||
|
|
||||||
// Typescript: TSEndpoint= path=/auth/login; name=login; method=POST; request=model.LoginRequest; response=model.TokenPair
|
// Typescript: TSEndpoint= path=/auth/login; name=login; method=POST; request=users.LoginRequest; response=tokens.TokenPair
|
||||||
app.Post("/auth/login", authRateLimiter, userController.Login)
|
app.Post("/auth/login", authRateLimiter, userController.Login)
|
||||||
|
|
||||||
// Typescript: TSEndpoint= path=/auth/refresh; name=refresh; method=POST; request=model.RefreshRequest; response=model.TokenPair
|
// Typescript: TSEndpoint= path=/auth/refresh; name=refresh; method=POST; request=tokens.RefreshRequest; response=tokens.TokenPair
|
||||||
app.Post("/auth/refresh", authRateLimiter, userController.Refresh)
|
app.Post("/auth/refresh", authRateLimiter, userController.Refresh)
|
||||||
|
|
||||||
// Typescript: TSEndpoint= path=/auth/register; name=register; method=POST; request=models.UserCreateInput; response=models.UserShort
|
// Typescript: TSEndpoint= path=/auth/register; name=register; method=POST; request=users.UserCreateInput; response=users.User
|
||||||
app.Post("/auth/register", authRateLimiter, userController.Register)
|
app.Post("/auth/register", authRateLimiter, userController.Register)
|
||||||
|
|
||||||
// Typescript: TSEndpoint= path=/auth/password/forgot; name=forgotPassword; method=POST; request=model.ForgotPasswordRequest; response=controllers.SimpleResponse
|
// Typescript: TSEndpoint= path=/auth/password/forgot; name=forgotPassword; method=POST; request=users.ForgotPasswordRequest; response=responses.SimpleResponse
|
||||||
app.Post("/auth/password/forgot", authRateLimiter, userController.ForgotPassword)
|
app.Post("/auth/password/forgot", authRateLimiter, userController.ForgotPassword)
|
||||||
|
|
||||||
// Typescript: TSEndpoint= path=/auth/password/reset; name=resetPassword; method=POST; request=model.ResetPasswordRequest; response=controllers.SimpleResponse
|
// Typescript: TSEndpoint= path=/auth/password/reset; name=resetPassword; method=POST; request=users.ResetPasswordRequest; response=responses.SimpleResponse
|
||||||
app.Post("/auth/password/reset", authRateLimiter, userController.ResetPassword)
|
app.Post("/auth/password/reset", authRateLimiter, userController.ResetPassword)
|
||||||
|
|
||||||
// Typescript: TSEndpoint= path=/auth/password/valid; name=validToken; method=POST; request=string; response=controllers.SimpleResponse
|
// Typescript: TSEndpoint= path=/auth/password/valid; name=validToken; method=POST; request=string; response=responses.SimpleResponse
|
||||||
app.Post("/auth/password/valid", authRateLimiter, userController.ValidToken)
|
app.Post("/auth/password/valid", authRateLimiter, userController.ValidToken)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue