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,4 @@
package constdata
// PASSWORD_VALIDATION_REGEX is used by the frontend (JavaScript supports lookaheads).
const PASSWORD_VALIDATION_REGEX = `^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{10,}$`

237
app/utils/i18n/i18n.go Normal file
View File

@@ -0,0 +1,237 @@
package i18n
import (
"errors"
"fmt"
"html/template"
"strings"
"sync"
"git.ma-al.com/goc_marek/timetracker/app/model"
"github.com/gofiber/fiber/v3"
)
type I18nTranslation string
type TranslationResponse map[uint]map[string]map[string]map[string]string
var TransStore = newTranslationsStore()
var (
ErrLangIsoEmpty = errors.New("lang_id_empty")
ErrScopeEmpty = errors.New("scope_empty")
ErrComponentEmpty = errors.New("component_empty")
ErrKeyEmpty = errors.New("key_empty")
ErrLangIsoNotFoundInCache = errors.New("lang_id_not_in_cache")
ErrScopeNotFoundInCache = errors.New("scope_not_in_cache")
ErrComponentNotFoundInCache = errors.New("component_not_in_cache")
ErrKeyNotFoundInCache = errors.New("key_invalid_in_cache")
)
type TranslationsStore struct {
mutex sync.RWMutex
cache TranslationResponse
}
func newTranslationsStore() *TranslationsStore {
service := &TranslationsStore{
cache: make(TranslationResponse),
}
return service
}
func (s *TranslationsStore) Get(langID uint, scope string, component string, key string) (string, error) {
s.mutex.RLock()
defer s.mutex.RUnlock()
if langID == 0 {
return "lang_id_empty", ErrLangIsoEmpty
}
if _, ok := s.cache[langID]; !ok {
return fmt.Sprintf("lang_id_not_in_cache: %d", langID), ErrLangIsoNotFoundInCache
}
if scope == "" {
return "scope_empty", ErrScopeEmpty
}
if _, ok := s.cache[langID][scope]; !ok {
return fmt.Sprintf("scope_not_in_cache: %s", scope), ErrScopeNotFoundInCache
}
if component == "" {
return "component_empty", ErrComponentEmpty
}
if _, ok := s.cache[langID][scope][component]; !ok {
return fmt.Sprintf("component_not_in_cache: %s", component), ErrComponentNotFoundInCache
}
if key == "" {
return "key_empty", ErrKeyEmpty
}
if _, ok := s.cache[langID][scope][component][key]; !ok {
return fmt.Sprintf("key_invalid_in_cache: %s", key), ErrKeyNotFoundInCache
}
return s.cache[langID][scope][component][key], nil
}
func (s *TranslationsStore) GetTranslations(langID uint, scope string, components []string) (*TranslationResponse, error) {
s.mutex.RLock()
defer s.mutex.RUnlock()
if langID == 0 {
resp := make(TranslationResponse)
for k, v := range s.cache {
resp[k] = v
}
return &resp, nil
}
if _, ok := s.cache[langID]; !ok {
return nil, fmt.Errorf("invalid translation arguments")
}
tr := make(TranslationResponse)
if scope == "" {
tr[langID] = s.cache[langID]
return &tr, nil
}
if _, ok := s.cache[langID][scope]; !ok {
return nil, fmt.Errorf("invalid translation arguments")
}
tr[langID] = make(map[string]map[string]map[string]string)
if len(components) <= 0 {
tr[langID][scope] = s.cache[langID][scope]
return &tr, nil
}
tr[langID][scope] = make(map[string]map[string]string)
var invalidComponents []string
for _, component := range components {
if _, ok := s.cache[langID][scope][component]; !ok {
invalidComponents = append(invalidComponents, component)
continue
}
tr[langID][scope][component] = s.cache[langID][scope][component]
}
if len(invalidComponents) > 0 {
return &tr, fmt.Errorf("invalid translation arguments")
}
return &tr, nil
}
// GetAllTranslations returns all translations from the cache
func (s *TranslationsStore) GetAllTranslations() *TranslationResponse {
s.mutex.RLock()
defer s.mutex.RUnlock()
resp := make(TranslationResponse)
for k, v := range s.cache {
resp[k] = v
}
return &resp
}
func (s *TranslationsStore) LoadTranslations(translations []model.Translation) error {
s.mutex.Lock()
defer s.mutex.Unlock()
s.cache = make(TranslationResponse)
for _, t := range translations {
lang := uint(t.LangID)
scp := t.Scope.Name
cmp := t.Component.Name
data := ""
if t.Data != nil {
data = *t.Data
}
if _, ok := s.cache[lang]; !ok {
s.cache[lang] = make(map[string]map[string]map[string]string)
}
if _, ok := s.cache[lang][scp]; !ok {
s.cache[lang][scp] = make(map[string]map[string]string)
}
if _, ok := s.cache[lang][scp][cmp]; !ok {
s.cache[lang][scp][cmp] = make(map[string]string)
}
s.cache[lang][scp][cmp][t.Key] = data
}
return nil
}
// ReloadTranslations reloads translations from the database
func (s *TranslationsStore) ReloadTranslations(translations []model.Translation) error {
return s.LoadTranslations(translations)
}
// T_ is meant to be used to translate error messages and other system communicates.
func T_[T ~string](c fiber.Ctx, key T, params ...interface{}) string {
if langID, ok := c.Locals("langID").(uint); ok {
parts := strings.Split(string(key), ".")
if len(parts) >= 2 {
if trans, err := TransStore.Get(langID, "Backend", parts[0], strings.Join(parts[1:], ".")); err == nil {
return Format(trans, params...)
}
} else {
if trans, err := TransStore.Get(langID, "Backend", "be", parts[0]); err == nil {
return Format(trans, params...)
}
}
}
return string(key)
}
// T___ works exactly the same as T_ but uses just language ID instead of the whole context
func T___[T ~string](langId uint, key T, params ...interface{}) string {
parts := strings.Split(string(key), ".")
if len(parts) >= 2 {
if trans, err := TransStore.Get(langId, "Backend", parts[0], strings.Join(parts[1:], ".")); err == nil {
return Format(trans, params...)
}
} else {
if trans, err := TransStore.Get(langId, "Backend", "be", parts[0]); err == nil {
return Format(trans, params...)
}
}
return string(key)
}
func Format(text string, params ...interface{}) string {
text = fmt.Sprintf(text, params...)
return text
}
// T__ wraps T_ adding a conversion from string to template.HTML
func T__(c fiber.Ctx, key string, params ...interface{}) template.HTML {
return template.HTML(T_(c, key, params...))
}
// MapKeyOnTranslationMap is a helper function to map keys on translation map
// this is used to map keys on translation map
//
// example:
// map := map[T]string{}
// MapKeyOnTranslationMap(ctx, map, key1, key2, key3)
func MapKeyOnTranslationMap[T ~string](c fiber.Ctx, m *map[T]string, key ...T) {
if *m == nil {
*m = make(map[T]string)
}
for _, k := range key {
(*m)[k] = T_(c, string(k))
}
}

View File

@@ -0,0 +1,74 @@
package mapper
// Package mapper provides utilities to map fields from one struct to another
// by matching field names (case-insensitive). Unmatched fields are left as
// their zero values.
import (
"fmt"
"reflect"
"strings"
)
// Map copies field values from src into dst by matching field names
// (case-insensitive). Fields in dst that have no counterpart in src
// are left at their zero value.
//
// Both dst and src must be pointers to structs.
// Returns an error if the types do not satisfy those constraints.
func Map(dst, src any) error {
dstVal := reflect.ValueOf(dst)
srcVal := reflect.ValueOf(src)
if dstVal.Kind() != reflect.Ptr || dstVal.Elem().Kind() != reflect.Struct {
return fmt.Errorf("mapper: dst must be a pointer to a struct, got %T", dst)
}
if srcVal.Kind() != reflect.Ptr || srcVal.Elem().Kind() != reflect.Struct {
return fmt.Errorf("mapper: src must be a pointer to a struct, got %T", src)
}
dstElem := dstVal.Elem()
srcElem := srcVal.Elem()
// Build a lookup map of src fields: lowercase name -> field value
srcFields := make(map[string]reflect.Value)
for i := 0; i < srcElem.NumField(); i++ {
f := srcElem.Type().Field(i)
if !f.IsExported() {
continue
}
srcFields[strings.ToLower(f.Name)] = srcElem.Field(i)
}
// Iterate over dst fields and copy matching src values
for i := 0; i < dstElem.NumField(); i++ {
dstField := dstElem.Field(i)
dstType := dstElem.Type().Field(i)
if !dstType.IsExported() || !dstField.CanSet() {
continue
}
srcField, ok := srcFields[strings.ToLower(dstType.Name)]
if !ok {
// No matching src field leave zero value in place
continue
}
if srcField.Type().AssignableTo(dstField.Type()) {
dstField.Set(srcField)
} else if srcField.Type().ConvertibleTo(dstField.Type()) {
dstField.Set(srcField.Convert(dstField.Type()))
}
// If neither assignable nor convertible, the dst field keeps its zero value
}
return nil
}
// MustMap is like Map but panics on error.
func MustMap(dst, src any) {
if err := Map(dst, src); err != nil {
panic(err)
}
}

View File

@@ -0,0 +1,78 @@
package mapper_test
import (
"testing"
"git.ma-al.com/goc_marek/timetracker/app/utils/mapper"
)
// --- example structs ---
type UserInput struct {
Name string
Email string
Age int
}
type UserRecord struct {
Name string
Email string
Age int
CreatedAt string // not in src → stays ""
Active bool // not in src → stays false
}
// --- tests ---
func TestMap_MatchingFields(t *testing.T) {
src := &UserInput{Name: "Alice", Email: "alice@example.com", Age: 30}
dst := &UserRecord{}
if err := mapper.Map(dst, src); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if dst.Name != "Alice" {
t.Errorf("Name: want Alice, got %s", dst.Name)
}
if dst.Email != "alice@example.com" {
t.Errorf("Email: want alice@example.com, got %s", dst.Email)
}
if dst.Age != 30 {
t.Errorf("Age: want 30, got %d", dst.Age)
}
}
func TestMap_UnmatchedFieldsAreZero(t *testing.T) {
src := &UserInput{Name: "Bob"}
dst := &UserRecord{}
mapper.MustMap(dst, src)
if dst.CreatedAt != "" {
t.Errorf("CreatedAt: expected empty string, got %q", dst.CreatedAt)
}
if dst.Active != false {
t.Errorf("Active: expected false, got %v", dst.Active)
}
}
func TestMap_TypeConversion(t *testing.T) {
type Src struct{ Score int32 }
type Dst struct{ Score int64 }
src := &Src{Score: 99}
dst := &Dst{}
mapper.MustMap(dst, src)
if dst.Score != 99 {
t.Errorf("Score: want 99, got %d", dst.Score)
}
}
func TestMap_InvalidInput(t *testing.T) {
if err := mapper.Map("not a struct", 42); err == nil {
t.Error("expected error for non-struct inputs")
}
}

View File

@@ -0,0 +1,6 @@
package nullable
//go:fix inline
func GetNil[T any](in T) *T {
return new(in)
}

View File

@@ -0,0 +1,63 @@
package pagination
import (
"strconv"
"github.com/gofiber/fiber/v3"
"gorm.io/gorm"
)
type Paging struct {
Page uint `json:"page_number" example:"5"`
Elements uint `json:"elements_per_page" example:"30"`
}
func (p Paging) Offset() int {
return int(p.Elements) * int(p.Page-1)
}
func (p Paging) Limit() int {
return int(p.Elements)
}
type Found[T any] struct {
Items []T `json:"items,omitempty"`
Count uint `json:"items_count" example:"56"`
}
func Paginate[T any](paging Paging, stmt *gorm.DB) (Found[T], error) {
var items []T
var count int64
base := stmt.Session(&gorm.Session{})
countDB := stmt.Session(&gorm.Session{
NewDB: true, // critical: do NOT reuse statement
})
if err := countDB.
Table("(?) as sub", base).
Count(&count).Error; err != nil {
return Found[T]{}, err
}
err := base.
Offset(paging.Offset()).
Limit(paging.Limit()).
Find(&items).
Error
if err != nil {
return Found[T]{}, err
}
return Found[T]{
Items: items,
Count: uint(count),
}, err
}
func ParsePagination(c *fiber.Ctx) Paging {
pageNum, _ := strconv.ParseInt((*c).Query("p", "1"), 10, 64)
pageSize, _ := strconv.ParseInt((*c).Query("elems", "10"), 10, 64)
return Paging{Page: uint(pageNum), Elements: uint(pageSize)}
}

View File

@@ -0,0 +1,10 @@
package response
import "git.ma-al.com/goc_marek/timetracker/app/utils/i18n"
type ResponseMessage i18n.I18nTranslation
const (
Message_OK ResponseMessage = "message_ok"
Message_NOK ResponseMessage = "message_nok"
)

View File

@@ -0,0 +1,18 @@
package response
import "github.com/gofiber/fiber/v3"
type Response[T any] struct {
Message string `json:"message,omitempty"`
Items *T `json:"items,omitempty"`
Count *int `json:"count,omitempty"`
}
func Make[T any](c fiber.Ctx, status int, items *T, count *int, message string) Response[T] {
c.Status(status)
return Response[T]{
Message: message,
Items: items,
Count: count,
}
}

View File

@@ -0,0 +1,136 @@
package version
import (
"fmt"
"os"
"strings"
"time"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
)
// Version info populated at build time
var (
Version string // Git tag or commit hash
Commit string // Short commit hash
BuildDate string // Build timestamp
)
// Info returns version information
type Info struct {
Version string `json:"version"`
Commit string `json:"commit"`
BuildDate string `json:"build_date"`
}
// GetInfo returns version information
func GetInfo() Info {
v := Info{
Version: Version,
Commit: Commit,
BuildDate: BuildDate,
}
// If not set during build, try to get from git
if v.Version == "" || v.Version == "unknown" || v.Version == "(devel)" {
if gitVersion, gitCommit := getGitInfo(); gitVersion != "" {
v.Version = gitVersion
v.Commit = gitCommit
}
}
// If build date not set, use current time
if v.BuildDate == "" {
v.BuildDate = time.Now().Format(time.RFC3339)
}
return v
}
// getGitInfo returns the latest tag or short commit hash and the commit hash
func getGitInfo() (string, string) {
// Get the current working directory
dir, err := os.Getwd()
if err != nil {
return "", ""
}
// Open the git repository
repo, err := git.PlainOpen(dir)
if err != nil {
return "", ""
}
// Get the HEAD reference
head, err := repo.Head()
if err != nil {
return "", ""
}
commitHash := head.Hash().String()[:7]
// Get all tags
tagIter, err := repo.Tags()
if err != nil {
return commitHash, commitHash
}
// Get the commit for HEAD
commit, err := repo.CommitObject(head.Hash())
if err != nil {
return commitHash, commitHash
}
// Build ancestry map
ancestry := make(map[string]bool)
c := commit
for c != nil {
ancestry[c.Hash.String()] = true
c, _ = c.Parent(0)
}
// Find the most recent tag that's an ancestor of HEAD
var latestTag string
err = tagIter.ForEach(func(ref *plumbing.Reference) error {
// Get the target commit
targetHash := ref.Hash()
// Get the target commit
targetCommit, err := repo.CommitObject(targetHash)
if err != nil {
return nil
}
// Check if this tag is an ancestor of HEAD
checkCommit := targetCommit
for checkCommit != nil {
if ancestry[checkCommit.Hash.String()] {
// Extract tag name (remove refs/tags/ prefix)
tagName := strings.TrimPrefix(ref.Name().String(), "refs/tags/")
if latestTag == "" || tagName > latestTag {
latestTag = tagName
}
return nil
}
checkCommit, _ = checkCommit.Parent(0)
}
return nil
})
if err != nil {
return commitHash, commitHash
}
if latestTag != "" {
return latestTag, commitHash
}
return commitHash, commitHash
}
// String returns a formatted version string
func String() string {
info := GetInfo()
return fmt.Sprintf("Version: %s\nCommit: %s\nBuild Date: %s\n", info.Version, info.Commit, info.BuildDate)
}