Documentation
¶
Index ¶
Constants ¶
This section is empty.
Variables ¶
View Source
var ConfigViper = `` /* 863-byte string literal not displayed */
View Source
var ConfigYml = `` /* 419-byte string literal not displayed */
View Source
var EntityFile = `
package {{ .ApiName }}
import "time"
type {{ .TypeName }} struct {
ID int
CreatedAt time.Time
}
`
View Source
var GitIgnore = `` /* 711-byte string literal not displayed */
View Source
var HTTPResponse = `package response
import (
"errors"
"net/http"
"github.com/gofiber/fiber/v2"
)
type Envelope struct {
Status int ` + "`json:\"status\"`" + `
Message string ` + "`json:\"message\"`" + `
Data any ` + "`json:\"data\"`" + `
}
func Send(c *fiber.Ctx, status int, data any) error {
return c.Status(status).JSON(Envelope{Status: status, Message: http.StatusText(status), Data: data})
}
func SendMessage(c *fiber.Ctx, status int, message string, data any) error {
if message == "" { message = http.StatusText(status) }
return c.Status(status).JSON(Envelope{Status: status, Message: message, Data: data})
}
func ErrorHandler(c *fiber.Ctx, err error) error {
status := fiber.StatusInternalServerError
message := http.StatusText(status)
var fiberError *fiber.Error
if errors.As(err, &fiberError) { status= fiberError.Code; message=fiberError.Message }
return c.Status(status).JSON(Envelope{Status: status, Message: message, Data: nil})
}
`
View Source
var HandlerInterface = `` /* 560-byte string literal not displayed */
View Source
var HandlerMethod = `` /* 664-byte string literal not displayed */
View Source
var JWTManager = `package auth
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/spf13/viper"
)
const (
AccessTokenType = "access"
RefreshTokenType = "refresh"
minimumSecretSize = 32
)
type Config struct {
Issuer string
Audience string
AccessSecret string
RefreshSecret string
AccessTTL time.Duration
RefreshTTL time.Duration
}
type Claims struct {
Roles []string ` + "`json:\"roles,omitempty\"`" + `
TokenType string ` + "`json:\"token_type\"`" + `
jwt.RegisteredClaims
}
type TokenPair struct {
AccessToken string ` + "`json:\"access_token\"`" + `
RefreshToken string ` + "`json:\"refresh_token\"`" + `
TokenType string ` + "`json:\"token_type\"`" + `
ExpiresIn int64 ` + "`json:\"expires_in\"`" + `
}
type Manager struct{ config Config }
func NewManager(config Config) (*Manager, error) {
if len(config.AccessSecret) < minimumSecretSize || len(config.RefreshSecret) < minimumSecretSize {
return nil, fmt.Errorf("JWT access and refresh secrets must each contain at least %d bytes", minimumSecretSize)
}
if config.AccessSecret == config.RefreshSecret {
return nil, errors.New("JWT access and refresh secrets must be different")
}
if config.Issuer == "" || config.Audience == "" {
return nil, errors.New("JWT issuer and audience are required")
}
if config.AccessTTL <= 0 || config.RefreshTTL <= 0 || config.AccessTTL >= config.RefreshTTL {
return nil, errors.New("JWT TTL values must be positive and access TTL must be shorter than refresh TTL")
}
return &Manager{config: config}, nil
}
func NewManagerFromViper() (*Manager, error) {
return NewManager(Config{
Issuer: viper.GetString("JWT_ISSUER"), Audience: viper.GetString("JWT_AUDIENCE"),
AccessSecret: viper.GetString("JWT_ACCESS_SECRET"), RefreshSecret: viper.GetString("JWT_REFRESH_SECRET"),
AccessTTL: viper.GetDuration("JWT_ACCESS_TTL"), RefreshTTL: viper.GetDuration("JWT_REFRESH_TTL"),
})
}
func (m *Manager) IssuePair(subject string, roles []string) (TokenPair, error) {
if subject == "" { return TokenPair{}, errors.New("JWT subject is required") }
now := time.Now().UTC()
access, err := m.sign(subject, roles, AccessTokenType, now, m.config.AccessTTL, m.config.AccessSecret)
if err != nil { return TokenPair{}, err }
refresh, err := m.sign(subject, roles, RefreshTokenType, now, m.config.RefreshTTL, m.config.RefreshSecret)
if err != nil { return TokenPair{}, err }
return TokenPair{AccessToken: access, RefreshToken: refresh, TokenType: "Bearer", ExpiresIn: int64(m.config.AccessTTL.Seconds())}, nil
}
func (m *Manager) Refresh(refreshToken string) (TokenPair, error) {
claims, err := m.ParseRefresh(refreshToken)
if err != nil { return TokenPair{}, err }
return m.IssuePair(claims.Subject, claims.Roles)
}
func (m *Manager) ParseAccess(token string) (*Claims, error) {
return m.parse(token, AccessTokenType, m.config.AccessSecret)
}
func (m *Manager) ParseRefresh(token string) (*Claims, error) {
return m.parse(token, RefreshTokenType, m.config.RefreshSecret)
}
func (m *Manager) sign(subject string, roles []string, tokenType string, now time.Time, ttl time.Duration, secret string) (string, error) {
id, err := tokenID()
if err != nil { return "", fmt.Errorf("create token ID: %w", err) }
claims := Claims{Roles: roles, TokenType: tokenType, RegisteredClaims: jwt.RegisteredClaims{
Issuer: m.config.Issuer, Subject: subject, Audience: jwt.ClaimStrings{m.config.Audience}, ID: id,
IssuedAt: jwt.NewNumericDate(now), NotBefore: jwt.NewNumericDate(now), ExpiresAt: jwt.NewNumericDate(now.Add(ttl)),
}}
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(secret))
}
func (m *Manager) parse(raw, expectedType, secret string) (*Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(raw, claims, func(token *jwt.Token) (any, error) {
if token.Method != jwt.SigningMethodHS256 { return nil, fmt.Errorf("unexpected JWT signing method %q", token.Method.Alg()) }
return []byte(secret), nil
}, jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}), jwt.WithIssuer(m.config.Issuer),
jwt.WithAudience(m.config.Audience), jwt.WithExpirationRequired(), jwt.WithIssuedAt())
if err != nil { return nil, fmt.Errorf("invalid %s token: %w", expectedType, err) }
if !token.Valid { return nil, fmt.Errorf("invalid %s token", expectedType) }
if claims.TokenType != expectedType { return nil, fmt.Errorf("expected %s token", expectedType) }
return claims, nil
}
func tokenID() (string, error) {
value := make([]byte, 16)
if _, err := rand.Read(value); err != nil { return "", err }
return hex.EncodeToString(value), nil
}
`
View Source
var JWTMiddleware = `` /* 804-byte string literal not displayed */
View Source
var MainFile = `` /* 1045-byte string literal not displayed */
View Source
var MicroserviceDockerIgnore = `.git
.github
bin
logs
uploads
*.log
.env
README.md
`
View Source
var MicroserviceDockerfile = `` /* 370-byte string literal not displayed */
View Source
var MicroserviceHealth = `` /* 768-byte string literal not displayed */
View Source
var MicroserviceMain = `` /* 1704-byte string literal not displayed */
View Source
var ORMInterface = `` /* 1453-byte string literal not displayed */
View Source
var RepositoryInterface = `` /* 554-byte string literal not displayed */
View Source
var RepositoryMethod = `` /* 549-byte string literal not displayed */
View Source
var RequestLoggerMiddleware = `` /* 1208-byte string literal not displayed */
View Source
var RouteDynamic = `` /* 727-byte string literal not displayed */
View Source
var RouteRoot = `` /* 446-byte string literal not displayed */
View Source
var RouteV1 = `` /* 333-byte string literal not displayed */
View Source
var SQLInterface = `` /* 599-byte string literal not displayed */
View Source
var SchemaFile = `` /* 362-byte string literal not displayed */
View Source
var ServiceInterface = `` /* 745-byte string literal not displayed */
View Source
var ServiceMethod = `` /* 655-byte string literal not displayed */
View Source
var StructuredLogger = `` /* 4769-byte string literal not displayed */
View Source
var SwaggerDocument = `` /* 147-byte string literal not displayed */
View Source
var SwaggerRoute = `package routes
import (
"encoding/json"
"strings"
"github.com/gofiber/fiber/v2"
"github.com/spf13/viper"
"{{ .ModuleName }}/docs"
)
const swaggerUI = ` + "`" + `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>API documentation</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css">
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
<script>
window.onload = () => SwaggerUIBundle({
url: "/swagger/openapi.json",
dom_id: "#swagger-ui",
deepLinking: true,
persistAuthorization: true
});
</script>
</body>
</html>` + "`" + `
func RegisterSwaggerRoutes(app *fiber.App) {
if !viper.GetBool("SWAGGER_ENABLED") || strings.EqualFold(viper.GetString("APP_ENV"), "production") {
return
}
app.Get("/swagger", func(c *fiber.Ctx) error {
return c.Redirect("/swagger/index.html", fiber.StatusTemporaryRedirect)
})
app.Get("/swagger/index.html", func(c *fiber.Ctx) error {
return c.Type("html", "utf-8").SendString(swaggerUI)
})
app.Get("/swagger/openapi.json", func(c *fiber.Ctx) error {
return c.Type("json", "utf-8").Send(buildOpenAPISpec(app))
})
}
func buildOpenAPISpec(app *fiber.App) []byte {
var document map[string]any
if err := json.Unmarshal(docs.OpenAPI, &document); err != nil {
return docs.OpenAPI
}
paths, ok := document["paths"].(map[string]any)
if !ok {
paths = make(map[string]any)
document["paths"] = paths
}
for _, route := range app.GetRoutes() {
method := strings.ToLower(route.Method)
if method == "head" || method == "options" || strings.HasPrefix(route.Path, "/swagger") {
continue
}
path, parameters := openAPIPath(route.Path)
operations, ok := paths[path].(map[string]any)
if !ok {
operations = make(map[string]any)
paths[path] = operations
}
if _, exists := operations[method]; exists {
continue
}
operation := map[string]any{
"summary": strings.ToUpper(route.Method) + " " + route.Path,
"tags": []string{"Generated routes"},
"responses": map[string]any{
"200": map[string]any{
"description": "Successful response",
"content": map[string]any{
"application/json": map[string]any{
"schema": map[string]any{"$ref": "#/components/schemas/Envelope"},
},
},
},
},
}
if len(parameters) > 0 {
operation["parameters"] = parameters
}
operations[method] = operation
}
encoded, err := json.MarshalIndent(document, "", " ")
if err != nil {
return docs.OpenAPI
}
return encoded
}
func openAPIPath(path string) (string, []map[string]any) {
segments := strings.Split(path, "/")
parameters := make([]map[string]any, 0)
for index, segment := range segments {
if !strings.HasPrefix(segment, ":") {
continue
}
name := strings.TrimSuffix(strings.TrimPrefix(segment, ":"), "?")
if name == "" {
continue
}
segments[index] = "{" + name + "}"
parameters = append(parameters, map[string]any{
"name": name, "in": "path", "required": true,
"schema": map[string]any{"type": "string"},
})
}
return strings.Join(segments, "/"), parameters
}
`
View Source
var SwaggerSpec = `` /* 1440-byte string literal not displayed */
View Source
var UploadHandler = `` /* 785-byte string literal not displayed */
View Source
var UploadRoute = `` /* 599-byte string literal not displayed */
View Source
var UploadService = `` /* 1275-byte string literal not displayed */
View Source
var UploadStorage = `package upload
import (
"bytes"
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strings"
)
type File struct {
OriginalName string ` + "`json:\"original_name\"`" + `
StoredName string ` + "`json:\"stored_name\"`" + `
Size int64 ` + "`json:\"size\"`" + `
ContentType string ` + "`json:\"content_type\"`" + `
}
type Storage interface { Save(ctx context.Context, file *multipart.FileHeader) (File,error) }
type LocalStorage struct{ directory string }
func NewLocalStorage(directory string) *LocalStorage {
if strings.TrimSpace(directory)=="" { directory="uploads" }
return &LocalStorage{directory:directory}
}
func (s *LocalStorage) Save(ctx context.Context, header *multipart.FileHeader) (File,error) {
if err:=ctx.Err();err!=nil{return File{},err}
if err:=os.MkdirAll(s.directory,0o750);err!=nil{return File{},fmt.Errorf("create upload directory: %w",err)}
source,err:=header.Open();if err!=nil{return File{},fmt.Errorf("open uploaded file: %w",err)}
defer source.Close()
random:=make([]byte,16);if _,err:=rand.Read(random);err!=nil{return File{},fmt.Errorf("create upload name: %w",err)}
extension:=strings.ToLower(filepath.Ext(filepath.Base(header.Filename)))
storedName:=hex.EncodeToString(random)+extension
targetPath:=filepath.Join(s.directory,storedName)
target,err:=os.OpenFile(targetPath,os.O_WRONLY|os.O_CREATE|os.O_EXCL,0o640)
if err!=nil{return File{},fmt.Errorf("create upload target: %w",err)}
prefix:=make([]byte,512);count,readErr:=source.Read(prefix);if readErr!=nil && readErr!=io.EOF { _=target.Close();_=os.Remove(targetPath);return File{},fmt.Errorf("inspect uploaded file: %w",readErr) }
written,copyErr:=io.Copy(target,io.MultiReader(bytes.NewReader(prefix[:count]),source));closeErr:=target.Close()
if copyErr!=nil { _=os.Remove(targetPath); return File{},fmt.Errorf("store uploaded file: %w",copyErr) }
if closeErr!=nil { _=os.Remove(targetPath); return File{},fmt.Errorf("close uploaded file: %w",closeErr) }
return File{OriginalName:filepath.Base(header.Filename),StoredName:storedName,Size:written,ContentType:http.DetectContentType(prefix[:count])},nil
}
`
Functions ¶
This section is empty.
Types ¶
This section is empty.
Click to show internal directories.
Click to hide internal directories.