initial commit. Cloned timetracker repository
This commit is contained in:
11
app/delivery/handler/auth.go
Normal file
11
app/delivery/handler/auth.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_marek/timetracker/app/delivery/web/public"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// AuthHandlerRoutes registers all auth routes
|
||||
func AuthHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
return public.AuthHandlerRoutes(r)
|
||||
}
|
||||
11
app/delivery/handler/repo.go
Normal file
11
app/delivery/handler/repo.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_marek/timetracker/app/delivery/web/public"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// AuthHandlerRoutes registers all auth routes
|
||||
func RepoHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
return public.RepoHandlerRoutes(r)
|
||||
}
|
||||
118
app/delivery/middleware/auth.go
Normal file
118
app/delivery/middleware/auth.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.ma-al.com/goc_marek/timetracker/app/config"
|
||||
"git.ma-al.com/goc_marek/timetracker/app/model"
|
||||
"git.ma-al.com/goc_marek/timetracker/app/service/authService"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// AuthMiddleware creates authentication middleware
|
||||
func AuthMiddleware() fiber.Handler {
|
||||
authService := authService.NewAuthService()
|
||||
|
||||
return func(c fiber.Ctx) error {
|
||||
// Get token from Authorization header
|
||||
authHeader := c.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
// Try to get from cookie
|
||||
authHeader = c.Cookies("access_token")
|
||||
if authHeader == "" {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "authorization token required",
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Extract token from "Bearer <token>"
|
||||
parts := strings.Split(authHeader, " ")
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "invalid authorization header format",
|
||||
})
|
||||
}
|
||||
authHeader = parts[1]
|
||||
}
|
||||
|
||||
// Validate token
|
||||
claims, err := authService.ValidateToken(authHeader)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "invalid or expired token",
|
||||
})
|
||||
}
|
||||
|
||||
// Get user from database
|
||||
user, err := authService.GetUserByID(claims.UserID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "user not found",
|
||||
})
|
||||
}
|
||||
|
||||
// Check if user is active
|
||||
if !user.IsActive {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
||||
"error": "user account is inactive",
|
||||
})
|
||||
}
|
||||
|
||||
// Set user in context
|
||||
c.Locals("user", user.ToSession())
|
||||
c.Locals("userID", user.ID)
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireAdmin creates admin-only middleware
|
||||
func RequireAdmin() fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
user := c.Locals("user")
|
||||
if user == nil {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "not authenticated",
|
||||
})
|
||||
}
|
||||
|
||||
userSession, ok := user.(*model.UserSession)
|
||||
if !ok {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": "invalid user session",
|
||||
})
|
||||
}
|
||||
|
||||
if userSession.Role != model.RoleAdmin {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
||||
"error": "admin access required",
|
||||
})
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserID extracts user ID from context
|
||||
func GetUserID(c fiber.Ctx) uint {
|
||||
userID, ok := c.Locals("userID").(uint)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return userID
|
||||
}
|
||||
|
||||
// GetUser extracts user from context
|
||||
func GetUser(c fiber.Ctx) *model.UserSession {
|
||||
user, ok := c.Locals("user").(*model.UserSession)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
// GetConfig returns the app config
|
||||
func GetConfig() *config.Config {
|
||||
return config.Get()
|
||||
}
|
||||
18
app/delivery/middleware/cors.go
Normal file
18
app/delivery/middleware/cors.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package middleware
|
||||
|
||||
import "github.com/gofiber/fiber/v3"
|
||||
|
||||
// CORSMiddleware creates CORS middleware
|
||||
func CORSMiddleware() fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
c.Set("Access-Control-Allow-Origin", "*")
|
||||
c.Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
c.Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
|
||||
if c.Method() == "OPTIONS" {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
114
app/delivery/middleware/language.go
Normal file
114
app/delivery/middleware/language.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.ma-al.com/goc_marek/timetracker/app/service/langs"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// LanguageMiddleware discovers client's language and stores it in context
|
||||
// Priority: Query param > Cookie > Accept-Language header > Default language
|
||||
func LanguageMiddleware() fiber.Handler {
|
||||
langService := langs.LangSrv
|
||||
|
||||
return func(c fiber.Ctx) error {
|
||||
var langID uint
|
||||
|
||||
// 1. Check query parameter
|
||||
langIDStr := c.Query("lang_id", "")
|
||||
if langIDStr != "" {
|
||||
if id, err := strconv.ParseUint(langIDStr, 10, 32); err == nil {
|
||||
langID = uint(id)
|
||||
if langID > 0 {
|
||||
lang, err := langService.GetLanguageById(langID)
|
||||
if err == nil {
|
||||
c.Locals("langID", langID)
|
||||
c.Locals("lang", lang)
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check cookie
|
||||
cookieLang := c.Cookies("lang_id", "")
|
||||
if cookieLang != "" {
|
||||
if id, err := strconv.ParseUint(cookieLang, 10, 32); err == nil {
|
||||
langID = uint(id)
|
||||
if langID > 0 {
|
||||
lang, err := langService.GetLanguageById(langID)
|
||||
if err == nil {
|
||||
c.Locals("langID", langID)
|
||||
c.Locals("lang", lang)
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Check Accept-Language header
|
||||
acceptLang := c.Get("Accept-Language", "")
|
||||
if acceptLang != "" {
|
||||
// Parse the Accept-Language header (e.g., "en-US,en;q=0.9,pl;q=0.8")
|
||||
isoCode := parseAcceptLanguage(acceptLang)
|
||||
if isoCode != "" {
|
||||
lang, err := langService.GetLanguageByISOCode(isoCode)
|
||||
if err == nil && lang != nil {
|
||||
langID = uint(lang.ID)
|
||||
c.Locals("langID", langID)
|
||||
c.Locals("lang", lang)
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Fall back to default language
|
||||
defaultLang, err := langService.GetDefaultLanguage()
|
||||
if err == nil && defaultLang != nil {
|
||||
langID = uint(defaultLang.ID)
|
||||
c.Locals("langID", langID)
|
||||
c.Locals("lang", defaultLang)
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// parseAcceptLanguage extracts the primary language ISO code from Accept-Language header
|
||||
func parseAcceptLanguage(header string) string {
|
||||
// Split by comma
|
||||
parts := strings.Split(header, ",")
|
||||
if len(parts) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Get the first part (highest priority)
|
||||
first := strings.TrimSpace(parts[0])
|
||||
if first == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Remove any quality value (e.g., ";q=0.9")
|
||||
if idx := strings.Index(first, ";"); idx != -1 {
|
||||
first = strings.TrimSpace(first[:idx])
|
||||
}
|
||||
|
||||
// Handle cases like "en-US" or "en"
|
||||
// Return the primary language code (first part before dash)
|
||||
if idx := strings.Index(first, "-"); idx != -1 {
|
||||
return strings.ToLower(first[:idx])
|
||||
}
|
||||
|
||||
return strings.ToLower(first)
|
||||
}
|
||||
|
||||
// GetLanguageID extracts language ID from context
|
||||
func GetLanguageID(c fiber.Ctx) uint {
|
||||
langID, ok := c.Locals("langID").(uint)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return langID
|
||||
}
|
||||
177
app/delivery/web/init.go
Normal file
177
app/delivery/web/init.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.ma-al.com/goc_marek/timetracker/app/config"
|
||||
"git.ma-al.com/goc_marek/timetracker/app/delivery/handler"
|
||||
"git.ma-al.com/goc_marek/timetracker/app/delivery/middleware"
|
||||
"git.ma-al.com/goc_marek/timetracker/app/delivery/web/public"
|
||||
|
||||
// "github.com/gofiber/fiber/v2/middleware/filesystem"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
// "github.com/gofiber/fiber/v3/middleware/filesystem"
|
||||
"github.com/gofiber/fiber/v3/middleware/logger"
|
||||
"github.com/gofiber/fiber/v3/middleware/recover"
|
||||
)
|
||||
|
||||
// Server represents the web server
|
||||
type Server struct {
|
||||
app *fiber.App
|
||||
cfg *config.Config
|
||||
api fiber.Router
|
||||
}
|
||||
|
||||
// App returns the fiber app
|
||||
func (s *Server) App() *fiber.App {
|
||||
return s.app
|
||||
}
|
||||
|
||||
// Cfg returns the config
|
||||
func (s *Server) Cfg() *config.Config {
|
||||
return s.cfg
|
||||
}
|
||||
|
||||
// New creates a new server instance
|
||||
func New() *Server {
|
||||
return &Server{
|
||||
app: fiber.New(fiber.Config{
|
||||
ErrorHandler: customErrorHandler,
|
||||
}),
|
||||
cfg: config.Get(),
|
||||
}
|
||||
}
|
||||
|
||||
// Setup configures the server with routes and middleware
|
||||
func (s *Server) Setup() error {
|
||||
// Global middleware
|
||||
s.app.Use(recover.New())
|
||||
s.app.Use(logger.New())
|
||||
|
||||
// CORS middleware
|
||||
s.app.Use(middleware.CORSMiddleware())
|
||||
|
||||
// Language middleware - discovers client's language and stores in context
|
||||
s.app.Use(middleware.LanguageMiddleware())
|
||||
|
||||
// initialize healthcheck
|
||||
public.InitHealth(s.App(), s.Cfg())
|
||||
|
||||
// serve favicon
|
||||
public.Favicon(s.app, s.cfg)
|
||||
|
||||
// API routes
|
||||
s.api = s.app.Group("/api/v1")
|
||||
|
||||
// initialize swagger endpoints
|
||||
public.InitSwagger(s.App())
|
||||
|
||||
// Auth routes (public)
|
||||
auth := s.api.Group("/auth")
|
||||
handler.AuthHandlerRoutes(auth)
|
||||
|
||||
// Repo routes (public)
|
||||
repo := s.api.Group("/repo")
|
||||
repo.Use(middleware.AuthMiddleware())
|
||||
handler.RepoHandlerRoutes(repo)
|
||||
|
||||
// Protected routes example
|
||||
protected := s.api.Group("/restricted")
|
||||
protected.Use(middleware.AuthMiddleware())
|
||||
protected.Get("/dashboard", func(c fiber.Ctx) error {
|
||||
user := middleware.GetUser(c)
|
||||
return c.JSON(fiber.Map{
|
||||
"message": "Welcome to the protected area",
|
||||
"user": user,
|
||||
})
|
||||
})
|
||||
|
||||
// Admin routes example
|
||||
admin := s.api.Group("/admin")
|
||||
admin.Use(middleware.AuthMiddleware())
|
||||
admin.Use(middleware.RequireAdmin())
|
||||
admin.Get("/users", func(c fiber.Ctx) error {
|
||||
return c.JSON(fiber.Map{
|
||||
"message": "Admin area - user management",
|
||||
})
|
||||
})
|
||||
|
||||
public.NewLangHandler().InitLanguage(s.api, s.cfg)
|
||||
|
||||
// Settings endpoint
|
||||
public.NewSettingsHandler().InitSettings(s.api, s.cfg)
|
||||
|
||||
// keep this at the end because its wilderange
|
||||
public.InitBo(s.App())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run starts the server
|
||||
func (s *Server) Run() error {
|
||||
// Run database migrations
|
||||
// if err := db.RunMigrations(); err != nil {
|
||||
// log.Printf("⚠️ Database migrations failed: %v", err)
|
||||
// } else {
|
||||
// log.Println("✓ Database migrations completed")
|
||||
// }
|
||||
|
||||
// // Seed admin user
|
||||
// if err := db.SeedAdminUser("admin@example.com", "admin123"); err != nil {
|
||||
// log.Printf("⚠️ Admin user seeding failed: %v", err)
|
||||
// }
|
||||
|
||||
addr := s.cfg.Server.Host + ":" + strconv.Itoa(s.cfg.Server.Port)
|
||||
log.Printf("Starting server on %s", addr)
|
||||
log.Printf("Swagger UI available at http://%s/swagger/index.html", addr)
|
||||
log.Printf("OpenAPI JSON available at http://%s/openapi.json", addr)
|
||||
|
||||
go func() {
|
||||
if err := s.app.Listen(":3000"); err != nil {
|
||||
log.Println("Server stopped:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for shutdown signal
|
||||
quit := make(chan os.Signal, 1)
|
||||
|
||||
signal.Notify(
|
||||
quit,
|
||||
syscall.SIGINT, // Ctrl+C
|
||||
syscall.SIGTERM, // docker stop
|
||||
)
|
||||
|
||||
<-quit
|
||||
|
||||
log.Println("Shutting down server...")
|
||||
|
||||
// graceful shutdown with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := s.app.ShutdownWithContext(ctx); err != nil {
|
||||
log.Fatal("Shutdown error:", err)
|
||||
}
|
||||
|
||||
log.Println("Server exited cleanly")
|
||||
return nil
|
||||
}
|
||||
|
||||
// customErrorHandler handles errors
|
||||
func customErrorHandler(c fiber.Ctx, err error) error {
|
||||
code := fiber.StatusInternalServerError
|
||||
|
||||
if e, ok := err.(*fiber.Error); ok {
|
||||
code = e.Code
|
||||
}
|
||||
|
||||
return c.Status(code).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
416
app/delivery/web/public/auth.go
Normal file
416
app/delivery/web/public/auth.go
Normal file
@@ -0,0 +1,416 @@
|
||||
package public
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"git.ma-al.com/goc_marek/timetracker/app/config"
|
||||
"git.ma-al.com/goc_marek/timetracker/app/delivery/middleware"
|
||||
"git.ma-al.com/goc_marek/timetracker/app/model"
|
||||
"git.ma-al.com/goc_marek/timetracker/app/service/authService"
|
||||
"git.ma-al.com/goc_marek/timetracker/app/utils/i18n"
|
||||
"git.ma-al.com/goc_marek/timetracker/app/view"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// AuthHandler handles authentication endpoints
|
||||
type AuthHandler struct {
|
||||
authService *authService.AuthService
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
// NewAuthHandler creates a new AuthHandler instance
|
||||
func NewAuthHandler() *AuthHandler {
|
||||
authService := authService.NewAuthService()
|
||||
return &AuthHandler{
|
||||
authService: authService,
|
||||
config: config.Get(),
|
||||
}
|
||||
}
|
||||
|
||||
// AuthHandlerRoutes registers all auth routes
|
||||
func AuthHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
handler := NewAuthHandler()
|
||||
|
||||
r.Post("/login", handler.Login)
|
||||
r.Post("/register", handler.Register)
|
||||
r.Post("/complete-registration", handler.CompleteRegistration)
|
||||
r.Post("/forgot-password", handler.ForgotPassword)
|
||||
r.Post("/reset-password", handler.ResetPassword)
|
||||
r.Post("/logout", handler.Logout)
|
||||
r.Post("/refresh", handler.RefreshToken)
|
||||
|
||||
// Google OAuth2
|
||||
r.Get("/google", handler.GoogleLogin)
|
||||
r.Get("/google/callback", handler.GoogleCallback)
|
||||
|
||||
authProtected := r.Group("", middleware.AuthMiddleware())
|
||||
authProtected.Get("/me", handler.Me)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Login(c fiber.Ctx) error {
|
||||
var req model.LoginRequest
|
||||
|
||||
if err := c.Bind().Body(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrInvalidBody),
|
||||
})
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if req.Email == "" || req.Password == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrEmailPasswordRequired),
|
||||
})
|
||||
}
|
||||
|
||||
// Attempt login
|
||||
response, rawRefreshToken, err := h.authService.Login(&req)
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, err),
|
||||
})
|
||||
}
|
||||
|
||||
// Set cookies for web-based authentication
|
||||
h.setAuthCookies(c, response.AccessToken, rawRefreshToken)
|
||||
|
||||
return c.JSON(response)
|
||||
}
|
||||
|
||||
// setAuthCookies sets the access token (HTTPOnly) and refresh token (HTTPOnly) cookies,
|
||||
// plus a non-HTTPOnly is_authenticated flag cookie for frontend state detection.
|
||||
func (h *AuthHandler) setAuthCookies(c fiber.Ctx, accessToken, rawRefreshToken string) {
|
||||
isProduction := h.config.App.Environment == "production"
|
||||
|
||||
// HTTPOnly access token cookie — not readable by JS, protects against XSS
|
||||
c.Cookie(&fiber.Cookie{
|
||||
Name: "access_token",
|
||||
Value: accessToken,
|
||||
Expires: time.Now().Add(time.Duration(h.config.Auth.JWTExpiration) * time.Second),
|
||||
HTTPOnly: true,
|
||||
Secure: isProduction,
|
||||
SameSite: "Lax",
|
||||
})
|
||||
|
||||
// HTTPOnly refresh token cookie — opaque, stored as hash in DB
|
||||
if rawRefreshToken != "" {
|
||||
c.Cookie(&fiber.Cookie{
|
||||
Name: "refresh_token",
|
||||
Value: rawRefreshToken,
|
||||
Expires: time.Now().Add(time.Duration(h.config.Auth.RefreshExpiration) * time.Second),
|
||||
HTTPOnly: true,
|
||||
Secure: isProduction,
|
||||
SameSite: "Lax",
|
||||
})
|
||||
}
|
||||
|
||||
// Non-HTTPOnly flag cookie — readable by JS to detect auth state.
|
||||
// Contains no sensitive data; actual auth is enforced by the HTTPOnly access_token cookie.
|
||||
c.Cookie(&fiber.Cookie{
|
||||
Name: "is_authenticated",
|
||||
Value: "1",
|
||||
Expires: time.Now().Add(time.Duration(h.config.Auth.JWTExpiration) * time.Second),
|
||||
HTTPOnly: false,
|
||||
Secure: isProduction,
|
||||
SameSite: "Lax",
|
||||
})
|
||||
}
|
||||
|
||||
// clearAuthCookies expires all auth-related cookies
|
||||
func (h *AuthHandler) clearAuthCookies(c fiber.Ctx) {
|
||||
isProduction := h.config.App.Environment == "production"
|
||||
past := time.Now().Add(-time.Hour)
|
||||
|
||||
c.Cookie(&fiber.Cookie{
|
||||
Name: "access_token",
|
||||
Value: "",
|
||||
Expires: past,
|
||||
HTTPOnly: true,
|
||||
Secure: isProduction,
|
||||
SameSite: "Lax",
|
||||
})
|
||||
c.Cookie(&fiber.Cookie{
|
||||
Name: "refresh_token",
|
||||
Value: "",
|
||||
Expires: past,
|
||||
HTTPOnly: true,
|
||||
Secure: isProduction,
|
||||
SameSite: "Lax",
|
||||
})
|
||||
c.Cookie(&fiber.Cookie{
|
||||
Name: "is_authenticated",
|
||||
Value: "",
|
||||
Expires: past,
|
||||
HTTPOnly: false,
|
||||
Secure: isProduction,
|
||||
SameSite: "Lax",
|
||||
})
|
||||
}
|
||||
|
||||
// ForgotPassword handles password reset request
|
||||
func (h *AuthHandler) ForgotPassword(c fiber.Ctx) error {
|
||||
var req struct {
|
||||
Email string `json:"email" form:"email"`
|
||||
}
|
||||
|
||||
if err := c.Bind().Body(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrInvalidBody),
|
||||
})
|
||||
}
|
||||
|
||||
// Validate email
|
||||
if req.Email == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrEmailRequired),
|
||||
})
|
||||
}
|
||||
|
||||
// Request password reset - always return success to prevent email enumeration
|
||||
err := h.authService.RequestPasswordReset(req.Email)
|
||||
if err != nil {
|
||||
log.Printf("Password reset request error: %v", err)
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"message": i18n.T_(c, "auth.auth_if_account_exists"),
|
||||
})
|
||||
}
|
||||
|
||||
// ResetPassword handles password reset completion
|
||||
func (h *AuthHandler) ResetPassword(c fiber.Ctx) error {
|
||||
var req model.ResetPasswordRequest
|
||||
|
||||
if err := c.Bind().Body(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrInvalidBody),
|
||||
})
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if req.Token == "" || req.Password == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrTokenPasswordRequired),
|
||||
})
|
||||
}
|
||||
|
||||
// Reset password (also revokes all refresh tokens for the user)
|
||||
err := h.authService.ResetPassword(req.Token, req.Password)
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, err),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"message": i18n.T_(c, "auth.auth_password_reset_successfully"),
|
||||
})
|
||||
}
|
||||
|
||||
// Logout handles user logout — revokes the refresh token from DB and clears all cookies
|
||||
func (h *AuthHandler) Logout(c fiber.Ctx) error {
|
||||
// Revoke the refresh token from the database
|
||||
rawRefreshToken := c.Cookies("refresh_token")
|
||||
if rawRefreshToken != "" {
|
||||
h.authService.RevokeRefreshToken(rawRefreshToken)
|
||||
}
|
||||
|
||||
// Clear all auth cookies
|
||||
h.clearAuthCookies(c)
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"message": i18n.T_(c, "auth.auth_logged_out_successfully"),
|
||||
})
|
||||
}
|
||||
|
||||
// RefreshToken handles token refresh — validates opaque refresh token, rotates it, issues new access token
|
||||
func (h *AuthHandler) RefreshToken(c fiber.Ctx) error {
|
||||
// Get refresh token from HTTPOnly cookie (preferred) or request body (fallback for API clients)
|
||||
rawRefreshToken := c.Cookies("refresh_token")
|
||||
if rawRefreshToken == "" {
|
||||
var body struct {
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
if err := c.Bind().Body(&body); err == nil {
|
||||
rawRefreshToken = body.RefreshToken
|
||||
}
|
||||
}
|
||||
|
||||
if rawRefreshToken == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrRefreshTokenRequired),
|
||||
})
|
||||
}
|
||||
|
||||
response, newRawRefreshToken, err := h.authService.RefreshToken(rawRefreshToken)
|
||||
if err != nil {
|
||||
// If refresh token is invalid/expired, clear cookies
|
||||
h.clearAuthCookies(c)
|
||||
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, err),
|
||||
})
|
||||
}
|
||||
|
||||
// Set new cookies (rotated refresh token + new access token)
|
||||
h.setAuthCookies(c, response.AccessToken, newRawRefreshToken)
|
||||
|
||||
return c.JSON(response)
|
||||
}
|
||||
|
||||
// Me returns the current user info
|
||||
func (h *AuthHandler) Me(c fiber.Ctx) error {
|
||||
user := c.Locals("user")
|
||||
if user == nil {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrNotAuthenticated),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"user": user,
|
||||
})
|
||||
}
|
||||
|
||||
// Register handles user registration
|
||||
func (h *AuthHandler) Register(c fiber.Ctx) error {
|
||||
var req model.RegisterRequest
|
||||
|
||||
if err := c.Bind().Body(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrInvalidBody),
|
||||
})
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if req.FirstName == "" || req.LastName == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrFirstLastNameRequired),
|
||||
})
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if req.Email == "" || req.Password == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrEmailPasswordRequired),
|
||||
})
|
||||
}
|
||||
|
||||
// Attempt registration
|
||||
err := h.authService.Register(&req)
|
||||
if err != nil {
|
||||
log.Printf("Register error: %v", err)
|
||||
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, err),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
|
||||
"message": i18n.T_(c, "auth.auth_registration_successful"),
|
||||
})
|
||||
}
|
||||
|
||||
// CompleteRegistration handles completion of registration with password
|
||||
func (h *AuthHandler) CompleteRegistration(c fiber.Ctx) error {
|
||||
var req model.CompleteRegistrationRequest
|
||||
|
||||
if err := c.Bind().Body(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrInvalidBody),
|
||||
})
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if req.Token == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrTokenRequired),
|
||||
})
|
||||
}
|
||||
|
||||
// Attempt to complete registration
|
||||
response, rawRefreshToken, err := h.authService.CompleteRegistration(&req)
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, err),
|
||||
})
|
||||
}
|
||||
|
||||
// Set cookies for web-based authentication
|
||||
h.setAuthCookies(c, response.AccessToken, rawRefreshToken)
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(response)
|
||||
}
|
||||
|
||||
// GoogleLogin redirects the user to Google's OAuth2 consent page
|
||||
func (h *AuthHandler) GoogleLogin(c fiber.Ctx) error {
|
||||
// Generate a random state token and store it in a short-lived cookie
|
||||
state, err := h.authService.GenerateOAuthState()
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": i18n.T_(c, "error.err_internal_server_error"),
|
||||
})
|
||||
}
|
||||
|
||||
c.Cookie(&fiber.Cookie{
|
||||
Name: "oauth_state",
|
||||
Value: state,
|
||||
Expires: time.Now().Add(10 * time.Minute),
|
||||
HTTPOnly: true,
|
||||
Secure: h.config.App.Environment == "production",
|
||||
SameSite: "Lax",
|
||||
})
|
||||
|
||||
url := h.authService.GetGoogleAuthURL(state)
|
||||
return c.Redirect().To(url)
|
||||
}
|
||||
|
||||
// GoogleCallback handles the OAuth2 callback from Google
|
||||
func (h *AuthHandler) GoogleCallback(c fiber.Ctx) error {
|
||||
// Validate state to prevent CSRF
|
||||
cookieState := c.Cookies("oauth_state")
|
||||
queryState := c.Query("state")
|
||||
if cookieState == "" || cookieState != queryState {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": i18n.T_(c, "error.err_invalid_token"),
|
||||
})
|
||||
}
|
||||
|
||||
// Clear the state cookie
|
||||
c.Cookie(&fiber.Cookie{
|
||||
Name: "oauth_state",
|
||||
Value: "",
|
||||
Expires: time.Now().Add(-time.Hour),
|
||||
HTTPOnly: true,
|
||||
Secure: h.config.App.Environment == "production",
|
||||
SameSite: "Lax",
|
||||
})
|
||||
|
||||
code := c.Query("code")
|
||||
if code == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": i18n.T_(c, "error.err_invalid_body"),
|
||||
})
|
||||
}
|
||||
|
||||
response, rawRefreshToken, err := h.authService.HandleGoogleCallback(code)
|
||||
if err != nil {
|
||||
log.Printf("Google OAuth callback error: %v", err)
|
||||
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, err),
|
||||
})
|
||||
}
|
||||
|
||||
// Set cookies for web-based authentication (including is_authenticated flag)
|
||||
h.setAuthCookies(c, response.AccessToken, rawRefreshToken)
|
||||
|
||||
// Redirect to the locale-prefixed charts page after successful Google login.
|
||||
// The user's preferred language is stored in the auth response; fall back to "en".
|
||||
lang := response.User.Lang
|
||||
if lang == "" {
|
||||
lang = "en"
|
||||
}
|
||||
return c.Redirect().To(h.config.App.BaseURL + "/" + lang + "/chart")
|
||||
}
|
||||
26
app/delivery/web/public/bo.go
Normal file
26
app/delivery/web/public/bo.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package public
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_marek/timetracker/assets"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/gofiber/fiber/v3/middleware/static"
|
||||
)
|
||||
|
||||
func InitBo(app *fiber.App) {
|
||||
// static files
|
||||
app.Get("/*", static.New("", static.Config{
|
||||
FS: assets.FS(),
|
||||
// Browse: true,
|
||||
MaxAge: 60 * 60 * 24 * 30, // 30 days
|
||||
}))
|
||||
|
||||
app.Get("/*", static.New("", static.Config{
|
||||
FS: assets.FSDist(),
|
||||
// Browse: true,
|
||||
MaxAge: 60 * 60 * 24 * 30, // 30 days
|
||||
}))
|
||||
|
||||
app.Get("/*", func(c fiber.Ctx) error {
|
||||
return c.SendFile("./assets/public/dist/index.html")
|
||||
})
|
||||
}
|
||||
17
app/delivery/web/public/favicon.go
Normal file
17
app/delivery/web/public/favicon.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package public
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_marek/timetracker/app/config"
|
||||
"git.ma-al.com/goc_marek/timetracker/assets"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func Favicon(app *fiber.App, cfg *config.Config) {
|
||||
// Favicon check endpoint
|
||||
app.Get("/favicon.ico", func(c fiber.Ctx) error {
|
||||
return c.SendFile("img/favicon.ico", fiber.SendFile{
|
||||
FS: assets.FS(),
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
20
app/delivery/web/public/health.go
Normal file
20
app/delivery/web/public/health.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package public
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_marek/timetracker/app/config"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func InitHealth(app *fiber.App, cfg *config.Config) {
|
||||
// Health check endpoint
|
||||
app.Get("/health", func(c fiber.Ctx) error {
|
||||
// emailService.NewEmailService().SendVerificationEmail("goc_daniel@ma-al.com", "jakis_token", c.BaseURL(), "en")
|
||||
// emailService.NewEmailService().SendPasswordResetEmail("goc_daniel@ma-al.com", "jakis_token", c.BaseURL(), "en")
|
||||
// emailService.NewEmailService().SendNewUserAdminNotification("goc_daniel@ma-al.com", "admin", c.BaseURL())
|
||||
return c.JSON(fiber.Map{
|
||||
"status": "ok",
|
||||
"app": cfg.App.Name,
|
||||
"version": cfg.App.Version,
|
||||
})
|
||||
})
|
||||
}
|
||||
48
app/delivery/web/public/languages.go
Normal file
48
app/delivery/web/public/languages.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package public
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"git.ma-al.com/goc_marek/timetracker/app/config"
|
||||
"git.ma-al.com/goc_marek/timetracker/app/service/langs"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
type LangHandler struct {
|
||||
service langs.LangService
|
||||
}
|
||||
|
||||
func NewLangHandler() *LangHandler {
|
||||
return &LangHandler{
|
||||
service: *langs.LangSrv,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *LangHandler) InitLanguage(api fiber.Router, cfg *config.Config) {
|
||||
|
||||
api.Get("langs", h.GetLanguages)
|
||||
api.Get("translations", h.GetTranslations)
|
||||
api.Get("translations/reload", h.ReloadTranslations)
|
||||
}
|
||||
|
||||
func (h *LangHandler) GetLanguages(c fiber.Ctx) error {
|
||||
return c.JSON(h.service.GetActive(c))
|
||||
}
|
||||
|
||||
func (h *LangHandler) GetTranslations(c fiber.Ctx) error {
|
||||
langIDStr := c.Query("lang_id", "0")
|
||||
langID, _ := strconv.Atoi(langIDStr)
|
||||
scope := c.Query("scope", "")
|
||||
componentsStr := c.Query("components", "")
|
||||
|
||||
var components []string
|
||||
if componentsStr != "" {
|
||||
components = []string{componentsStr}
|
||||
}
|
||||
|
||||
return c.JSON(h.service.GetTranslations(c, uint(langID), scope, components))
|
||||
}
|
||||
|
||||
func (h *LangHandler) ReloadTranslations(c fiber.Ctx) error {
|
||||
return c.JSON(h.service.ReloadTranslationsResponse(c))
|
||||
}
|
||||
179
app/delivery/web/public/repo.go
Normal file
179
app/delivery/web/public/repo.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package public
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"git.ma-al.com/goc_marek/timetracker/app/config"
|
||||
"git.ma-al.com/goc_marek/timetracker/app/service/repoService"
|
||||
"git.ma-al.com/goc_marek/timetracker/app/utils/pagination"
|
||||
"git.ma-al.com/goc_marek/timetracker/app/view"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// RepoHandler handles endpoints asking for repository data (to create charts)
|
||||
type RepoHandler struct {
|
||||
repoService *repoService.RepoService
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
// NewRepoHandler creates a new RepoHandler instance
|
||||
func NewRepoHandler() *RepoHandler {
|
||||
repoService := repoService.New()
|
||||
return &RepoHandler{
|
||||
repoService: repoService,
|
||||
config: config.Get(),
|
||||
}
|
||||
}
|
||||
|
||||
// RepoHandlerRoutes registers all repo routes
|
||||
func RepoHandlerRoutes(r fiber.Router) fiber.Router {
|
||||
handler := NewRepoHandler()
|
||||
|
||||
r.Get("/get-repos", handler.GetRepoIDs)
|
||||
r.Get("/get-years", handler.GetYears)
|
||||
r.Get("/get-quarters", handler.GetQuarters)
|
||||
r.Get("/get-issues", handler.GetIssues)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (h *RepoHandler) GetRepoIDs(c fiber.Ctx) error {
|
||||
userID, ok := c.Locals("userID").(uint)
|
||||
if !ok {
|
||||
return c.Status(view.GetErrorStatus(view.ErrInvalidBody)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrInvalidBody),
|
||||
})
|
||||
}
|
||||
|
||||
response, err := h.repoService.GetRepositoriesForUser(userID)
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, err),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(response)
|
||||
}
|
||||
|
||||
func (h *RepoHandler) GetYears(c fiber.Ctx) error {
|
||||
userID, ok := c.Locals("userID").(uint)
|
||||
if !ok {
|
||||
return c.Status(view.GetErrorStatus(view.ErrInvalidBody)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrInvalidBody),
|
||||
})
|
||||
}
|
||||
|
||||
repoID_attribute := c.Query("repoID")
|
||||
repoID, err := strconv.Atoi(repoID_attribute)
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(view.ErrBadRepoIDAttribute)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrBadRepoIDAttribute),
|
||||
})
|
||||
}
|
||||
|
||||
response, err := h.repoService.GetYearsForUser(userID, uint(repoID))
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, err),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(response)
|
||||
}
|
||||
|
||||
func (h *RepoHandler) GetQuarters(c fiber.Ctx) error {
|
||||
userID, ok := c.Locals("userID").(uint)
|
||||
if !ok {
|
||||
return c.Status(view.GetErrorStatus(view.ErrInvalidBody)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrInvalidBody),
|
||||
})
|
||||
}
|
||||
|
||||
repoID_attribute := c.Query("repoID")
|
||||
repoID, err := strconv.Atoi(repoID_attribute)
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(view.ErrBadRepoIDAttribute)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrBadRepoIDAttribute),
|
||||
})
|
||||
}
|
||||
|
||||
year_attribute := c.Query("year")
|
||||
year, err := strconv.Atoi(year_attribute)
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(view.ErrBadYearAttribute)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrBadYearAttribute),
|
||||
})
|
||||
}
|
||||
|
||||
response, err := h.repoService.GetQuartersForUser(userID, uint(repoID), uint(year))
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, err),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(response)
|
||||
}
|
||||
|
||||
func (h *RepoHandler) GetIssues(c fiber.Ctx) error {
|
||||
userID, ok := c.Locals("userID").(uint)
|
||||
if !ok {
|
||||
return c.Status(view.GetErrorStatus(view.ErrInvalidBody)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrInvalidBody),
|
||||
})
|
||||
}
|
||||
|
||||
repoID_attribute := c.Query("repoID")
|
||||
repoID, err := strconv.Atoi(repoID_attribute)
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(view.ErrBadRepoIDAttribute)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrBadRepoIDAttribute),
|
||||
})
|
||||
}
|
||||
|
||||
year_attribute := c.Query("year")
|
||||
year, err := strconv.Atoi(year_attribute)
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(view.ErrBadYearAttribute)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrBadYearAttribute),
|
||||
})
|
||||
}
|
||||
|
||||
quarter_attribute := c.Query("quarter")
|
||||
quarter, err := strconv.Atoi(quarter_attribute)
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(view.ErrBadQuarterAttribute)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrBadQuarterAttribute),
|
||||
})
|
||||
}
|
||||
|
||||
page_number_attribute := c.Query("page_number")
|
||||
page_number, err := strconv.Atoi(page_number_attribute)
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(view.ErrBadPaging)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrBadPaging),
|
||||
})
|
||||
}
|
||||
|
||||
elements_per_page_attribute := c.Query("quarter")
|
||||
elements_per_page, err := strconv.Atoi(elements_per_page_attribute)
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(view.ErrBadPaging)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, view.ErrBadPaging),
|
||||
})
|
||||
}
|
||||
|
||||
var paging pagination.Paging
|
||||
paging.Page = uint(page_number)
|
||||
paging.Elements = uint(elements_per_page)
|
||||
|
||||
response, err := h.repoService.GetIssuesForUser(userID, uint(repoID), uint(year), uint(quarter), paging)
|
||||
if err != nil {
|
||||
return c.Status(view.GetErrorStatus(err)).JSON(fiber.Map{
|
||||
"error": view.GetErrorCode(c, err),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(response)
|
||||
}
|
||||
91
app/delivery/web/public/settings.go
Normal file
91
app/delivery/web/public/settings.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package public
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_marek/timetracker/app/config"
|
||||
constdata "git.ma-al.com/goc_marek/timetracker/app/utils/const_data"
|
||||
"git.ma-al.com/goc_marek/timetracker/app/utils/i18n"
|
||||
"git.ma-al.com/goc_marek/timetracker/app/utils/nullable"
|
||||
"git.ma-al.com/goc_marek/timetracker/app/utils/response"
|
||||
"git.ma-al.com/goc_marek/timetracker/app/utils/version"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// SettingsResponse represents the settings endpoint response
|
||||
type SettingsResponse struct {
|
||||
App AppSettings `json:"app"`
|
||||
Server ServerSettings `json:"server"`
|
||||
Auth AuthSettings `json:"auth"`
|
||||
Features FeatureFlags `json:"features"`
|
||||
Version version.Info `json:"version"`
|
||||
}
|
||||
|
||||
// AppSettings represents app configuration
|
||||
type AppSettings struct {
|
||||
Name string `json:"name"`
|
||||
Environment string `json:"environment"`
|
||||
BaseURL string `json:"base_url"`
|
||||
PasswordRegex string `json:"password_regex"`
|
||||
// Config config.Config `json:"config"`
|
||||
}
|
||||
|
||||
// ServerSettings represents server configuration (non-sensitive)
|
||||
type ServerSettings struct {
|
||||
Port int `json:"port"`
|
||||
Host string `json:"host"`
|
||||
}
|
||||
|
||||
// AuthSettings represents auth configuration (non-sensitive)
|
||||
type AuthSettings struct {
|
||||
JWTExpiration int `json:"jwt_expiration"`
|
||||
RefreshExpiration int `json:"refresh_expiration"`
|
||||
}
|
||||
|
||||
// FeatureFlags represents feature flags
|
||||
type FeatureFlags struct {
|
||||
EmailEnabled bool `json:"email_enabled"`
|
||||
OAuthGoogle bool `json:"oauth_google"`
|
||||
}
|
||||
|
||||
// SettingsHandler handles settings/config endpoints
|
||||
type SettingsHandler struct{}
|
||||
|
||||
// NewSettingsHandler creates a new settings handler
|
||||
func NewSettingsHandler() *SettingsHandler {
|
||||
return &SettingsHandler{}
|
||||
}
|
||||
|
||||
// InitSettings initializes the settings routes
|
||||
func (h *SettingsHandler) InitSettings(api fiber.Router, cfg *config.Config) {
|
||||
settings := api.Group("/settings")
|
||||
settings.Get("", h.GetSettings(cfg))
|
||||
}
|
||||
|
||||
// GetSettings returns all settings/config
|
||||
func (h *SettingsHandler) GetSettings(cfg *config.Config) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
settings := SettingsResponse{
|
||||
App: AppSettings{
|
||||
Name: cfg.App.Name,
|
||||
Environment: cfg.App.Environment,
|
||||
BaseURL: cfg.App.BaseURL,
|
||||
PasswordRegex: constdata.PASSWORD_VALIDATION_REGEX,
|
||||
// Config: *config.Get(),
|
||||
},
|
||||
Server: ServerSettings{
|
||||
Port: cfg.Server.Port,
|
||||
Host: cfg.Server.Host,
|
||||
},
|
||||
Auth: AuthSettings{
|
||||
JWTExpiration: cfg.Auth.JWTExpiration,
|
||||
RefreshExpiration: cfg.Auth.RefreshExpiration,
|
||||
},
|
||||
Features: FeatureFlags{
|
||||
EmailEnabled: cfg.Email.Enabled,
|
||||
OAuthGoogle: cfg.OAuth.Google.ClientID != "",
|
||||
},
|
||||
Version: version.GetInfo(),
|
||||
}
|
||||
|
||||
return c.JSON(response.Make(c, fiber.StatusOK, nullable.GetNil(settings), nullable.GetNil(0), i18n.T_(c, response.Message_OK)))
|
||||
}
|
||||
}
|
||||
60
app/delivery/web/public/swagger.go
Normal file
60
app/delivery/web/public/swagger.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package public
|
||||
|
||||
import (
|
||||
"git.ma-al.com/goc_marek/timetracker/app/api"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/gofiber/fiber/v3/middleware/static"
|
||||
)
|
||||
|
||||
func InitSwagger(app *fiber.App) {
|
||||
// Swagger - serve OpenAPI JSON
|
||||
app.Get("/openapi.json", func(c fiber.Ctx) error {
|
||||
c.Set("Content-Type", "application/json")
|
||||
return c.SendString(api.ApenapiJson)
|
||||
})
|
||||
|
||||
// Swagger UI HTML
|
||||
app.Get("/swagger", func(c fiber.Ctx) error {
|
||||
return c.Redirect().Status(fiber.StatusFound).To("/swagger/index.html")
|
||||
})
|
||||
|
||||
app.Get("/swagger/index.html", func(c fiber.Ctx) error {
|
||||
c.Set("Content-Type", "text/html")
|
||||
return c.SendString(swaggerHTML)
|
||||
})
|
||||
|
||||
// Serve Swagger assets
|
||||
app.Get("/swagger/assets", static.New("app/api/swagger/assets"))
|
||||
}
|
||||
|
||||
// Embedded Swagger UI HTML (minimal version)
|
||||
var swaggerHTML = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>API Documentation</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<script src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-bundle.js" charset="UTF-8"></script>
|
||||
<script src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-standalone-preset.js" charset="UTF-8"></script>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
window.ui = SwaggerUIBundle({
|
||||
url: "/openapi.json",
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
layout: "StandaloneLayout"
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
Reference in New Issue
Block a user