initial commit. Cloned timetracker repository

This commit is contained in:
Daniel Goc
2026-03-10 09:02:57 +01:00
commit f2952bcef0
189 changed files with 21334 additions and 0 deletions

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

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

View 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(),
})
})
}

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

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

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

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

View 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>
`