update configuration

This commit is contained in:
juancwu 2026-01-03 16:48:31 -05:00
commit fda0f59fd3
5 changed files with 144 additions and 48 deletions

View file

@ -8,6 +8,11 @@ PORT=9000
DB_DRIVER=sqlite DB_DRIVER=sqlite
DB_CONNECTION="./data/local.db?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)" DB_CONNECTION="./data/local.db?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)"
#Generate all secret values by running go run ./cmd/generate_secrets
ID_ENCODING_ALPHABET=
ID_ENCODING_PRIME=
ID_ENCODING_INVERSE=
ID_ENCODINDG_XOR_KEY=
JWT_SECRET= JWT_SECRET=
# Go duration format # Go duration format
JWT_EXPIRY=168h JWT_EXPIRY=168h
@ -19,6 +24,3 @@ MAILER_IMAP_PORT=
MAILER_USERNAME= MAILER_USERNAME=
MAILER_PASSWORD= MAILER_PASSWORD=
MAILER_EMAIL_FROM= MAILER_EMAIL_FROM=
MAILER_ENVELOPE_FROM=
MAILER_SUPPORT_EMAIL=
MAILER_SUPPORT_ENVELOPE_FROM=

View file

@ -0,0 +1,61 @@
package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
"math/big"
"strings"
)
func main() {
// 1. Generate Shuffled Alphabet
// We start with standard base62
chars := []rune("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")
// Fisher-Yates Shuffle
// We use crypto/rand for the swap index to ensure high entropy
for i := len(chars) - 1; i > 0; i-- {
nBig, _ := rand.Int(rand.Reader, big.NewInt(int64(i+1)))
j := int(nBig.Int64())
chars[i], chars[j] = chars[j], chars[i]
}
alphabet := string(chars)
// 2. Generate Random 64-bit Prime
// crypto/rand.Prime automatically generates a number of the given bit length
// that is prime with high probability.
primeBig, err := rand.Prime(rand.Reader, 64)
if err != nil {
panic(fmt.Errorf("failed to generate prime: %v", err))
}
// 3. Calculate Modular Inverse
// (Prime * Inverse) % 2^64 == 1
// We use 1 << 64 as the modulus
modulus := new(big.Int).Lsh(big.NewInt(1), 64)
inverseBig := new(big.Int).ModInverse(primeBig, modulus)
// 4. Generate Random 64-bit XOR Key
// Just a random 64-bit integer
xorKeyBig, _ := rand.Int(rand.Reader, modulus)
// 5. Generate Hex-encoded JWT Secret (32 bytes / 256 bits)
jwtBytes := make([]byte, 32)
if _, err := rand.Read(jwtBytes); err != nil {
panic(fmt.Errorf("failed to generate jwt secret: %v", err))
}
jwtSecret := hex.EncodeToString(jwtBytes)
// --- OUTPUT ---
fmt.Println("Here are your generated secrets. Copy these into your .env or config file.")
fmt.Println(strings.Repeat("-", 60))
fmt.Printf("ALPHABET = \"%s\"\n", alphabet)
fmt.Printf("PRIME = %s\n", primeBig.String())
fmt.Printf("INVERSE = %s\n", inverseBig.String())
fmt.Printf("XOR_KEY = %s\n", xorKeyBig.String())
fmt.Printf("JWT_SECRET = \"%s\"\n", jwtSecret)
fmt.Println(strings.Repeat("-", 60))
}

View file

@ -29,13 +29,26 @@ func New(cfg *config.Config) (*App, error) {
return nil, fmt.Errorf("failed to run migrations: %w", err) return nil, fmt.Errorf("failed to run migrations: %w", err)
} }
emailClient := service.NewEmailClient(cfg.MailerSMTPHost, cfg.MailerSMTPPort, cfg.MailerIMAPHost, cfg.MailerIMAPPort, cfg.MailerUsername, cfg.MailerPassword) emailClient := service.NewEmailClient(
cfg.MailerSMTPHost,
cfg.MailerSMTPPort,
cfg.MailerIMAPHost,
cfg.MailerIMAPPort,
cfg.MailerUsername,
cfg.MailerPassword,
)
userRepository := repository.NewUserRepository(database) userRepository := repository.NewUserRepository(database)
userService := service.NewUserService(userRepository) userService := service.NewUserService(userRepository)
authService := service.NewAuthService(userRepository) authService := service.NewAuthService(userRepository)
emailService := service.NewEmailService(emailClient, cfg.MailerEmailFrom, cfg.MailerEnvelopeFrom, cfg.MailerSupportFrom, cfg.MailerSupportEnvelopeFrom, cfg.AppURL, cfg.AppName, cfg.AppEnv == "development") emailService := service.NewEmailService(
emailClient,
cfg.MailerEmailFrom,
cfg.AppURL,
cfg.AppName,
cfg.AppEnv == "development",
)
return &App{ return &App{
Cfg: cfg, Cfg: cfg,

View file

@ -20,19 +20,21 @@ type Config struct {
DBDriver string DBDriver string
DBConnection string DBConnection string
IDEncodingAlphabet string
IDEncodingPrime uint64
IDEncodingInverse uint64
IDEncodingXorKey uint64
JWTSecret string JWTSecret string
JWTExpiry time.Duration JWTExpiry time.Duration
MailerSMTPHost string MailerSMTPHost string
MailerSMTPPort int MailerSMTPPort int
MailerIMAPHost string MailerIMAPHost string
MailerIMAPPort int MailerIMAPPort int
MailerUsername string MailerUsername string
MailerPassword string MailerPassword string
MailerEmailFrom string MailerEmailFrom string
MailerEnvelopeFrom string
MailerSupportFrom string
MailerSupportEnvelopeFrom string
} }
func Load() *Config { func Load() *Config {
@ -52,19 +54,21 @@ func Load() *Config {
DBDriver: envString("DB_DRIVER", "sqlite"), DBDriver: envString("DB_DRIVER", "sqlite"),
DBConnection: envString("DB_CONNECTION", "./data/local.db?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)"), DBConnection: envString("DB_CONNECTION", "./data/local.db?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)"),
IDEncodingAlphabet: envRequired("ID_ENCODING_ALPHABET"),
IDEncodingPrime: envRequiredUint64("ID_ENCODING_PRIME"),
IDEncodingInverse: envRequiredUint64("ID_ENCODING_INVERSE"),
IDEncodingXorKey: envRequiredUint64("ID_ENCODING_XOR_KEY"),
JWTSecret: envRequired("JWT_SECRET"), JWTSecret: envRequired("JWT_SECRET"),
JWTExpiry: envDuration("JWT_EXPIRY", 168*time.Hour), // 7 days default JWTExpiry: envDuration("JWT_EXPIRY", 168*time.Hour), // 7 days default
MailerSMTPHost: envString("MAILER_SMTP_HOST", ""), MailerSMTPHost: envString("MAILER_SMTP_HOST", ""),
MailerSMTPPort: envInt("MAILER_SMTP_PORT", 587), MailerSMTPPort: envInt("MAILER_SMTP_PORT", 587),
MailerIMAPHost: envString("MAILER_IMAP_HOST", ""), MailerIMAPHost: envString("MAILER_IMAP_HOST", ""),
MailerIMAPPort: envInt("MAILER_IMAP_PORT", 993), MailerIMAPPort: envInt("MAILER_IMAP_PORT", 993),
MailerUsername: envString("MAILER_USERNAME", ""), MailerUsername: envString("MAILER_USERNAME", ""),
MailerPassword: envString("MAILER_PASSWORD", ""), MailerPassword: envString("MAILER_PASSWORD", ""),
MailerEmailFrom: envString("MAILER_EMAIL_FROM", ""), MailerEmailFrom: envString("MAILER_EMAIL_FROM", ""),
MailerEnvelopeFrom: envString("MAILER_ENVELOPE_FROM", ""),
MailerSupportFrom: envString("MAILER_SUPPORT_EMAIL_FROM", ""),
MailerSupportEnvelopeFrom: envString("MAILER_SUPPORT_ENVELOPE_FROM", ""),
} }
return cfg return cfg
@ -85,8 +89,7 @@ func (c *Config) Sanitized() *Config {
Port: c.Port, Port: c.Port,
AppTagline: c.AppTagline, AppTagline: c.AppTagline,
MailerEmailFrom: c.MailerEmailFrom, MailerEmailFrom: c.MailerEmailFrom,
MailerEnvelopeFrom: c.MailerEnvelopeFrom,
} }
} }
@ -111,6 +114,20 @@ func envInt(key string, def int) int {
return int(i) return int(i)
} }
func envRequiredUint64(key string) uint64 {
if value := os.Getenv(key); value != "" {
i64, err := parseUint64(value)
if err != nil {
slog.Error("config invalid required uint64", "key", key, "value", value, "error", err)
os.Exit(1)
}
return i64
}
slog.Error("config required uint64 env var missing", "key", key)
os.Exit(1)
return 0
}
func envDuration(key string, def time.Duration) time.Duration { func envDuration(key string, def time.Duration) time.Duration {
value, ok := os.LookupEnv(key) value, ok := os.LookupEnv(key)
if !ok || value == "" { if !ok || value == "" {
@ -132,3 +149,7 @@ func envRequired(key string) string {
os.Exit(1) os.Exit(1)
return "" return ""
} }
func parseUint64(s string) (uint64, error) {
return strconv.ParseUint(s, 10, 64)
}

View file

@ -126,26 +126,20 @@ func (nc *EmailClient) connectToIMAP() (*client.Client, error) {
} }
type EmailService struct { type EmailService struct {
client *EmailClient client *EmailClient
fromEmail string fromEmail string
fromEnvelope string isDev bool
supportEmail string appURL string
supportEnvelope string appName string
isDev bool
appURL string
appName string
} }
func NewEmailService(client *EmailClient, fromEmail, fromEnvelope, supportEmail, supportEnvelope, appURL, appName string, isDev bool) *EmailService { func NewEmailService(client *EmailClient, fromEmail, appURL, appName string, isDev bool) *EmailService {
return &EmailService{ return &EmailService{
client: client, client: client,
fromEmail: fromEmail, fromEmail: fromEmail,
fromEnvelope: fromEnvelope, isDev: isDev,
supportEmail: supportEmail, appURL: appURL,
supportEnvelope: supportEnvelope, appName: appName,
isDev: isDev,
appURL: appURL,
appName: appName,
} }
} }
@ -159,10 +153,11 @@ func (s *EmailService) SendMagicLinkEmail(email, token, name string) error {
} }
params := &EmailParams{ params := &EmailParams{
From: s.fromEmail, From: s.fromString(),
To: []string{email}, EnvelopeFrom: s.fromEmail,
Subject: subject, To: []string{email},
Text: body, Subject: subject,
Text: body,
} }
_, err := s.client.SendWithContext(context.Background(), params) _, err := s.client.SendWithContext(context.Background(), params)
@ -172,6 +167,10 @@ func (s *EmailService) SendMagicLinkEmail(email, token, name string) error {
return err return err
} }
func (s *EmailService) fromString() string {
return fmt.Sprintf("%s <%s>", s.appName, s.fromEmail)
}
func magicLinkEmailTemplate(magicURL, appName string) (string, string) { func magicLinkEmailTemplate(magicURL, appName string) (string, string) {
subject := fmt.Sprintf("Sign in to %s", appName) subject := fmt.Sprintf("Sign in to %s", appName)
body := fmt.Sprintf(`Click this link to sign in to your account: body := fmt.Sprintf(`Click this link to sign in to your account: