package service import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "errors" "os" "strings" "time" "golang.org/x/crypto/bcrypt" ) const defaultTokenTTL = 24 * time.Hour func hashPassword(password string) (string, error) { bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return "", err } return string(bytes), nil } func checkPassword(hash, password string) error { if strings.TrimSpace(hash) == "" { return errors.New("password hash is empty") } return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) } func tokenSecret() string { secret := strings.TrimSpace(os.Getenv("TOKEN_SECRET")) if secret == "" { return "local-dev-secret-change-me" } return secret } func tokenTTL() time.Duration { raw := strings.TrimSpace(os.Getenv("TOKEN_TTL")) if raw == "" { return defaultTokenTTL } ttl, err := time.ParseDuration(raw) if err != nil || ttl <= 0 { return defaultTokenTTL } return ttl } func signToken(payload tokenPayload) (string, error) { bytes, err := json.Marshal(payload) if err != nil { return "", err } body := base64.RawURLEncoding.EncodeToString(bytes) signature := computeTokenSignature(body) return "fable-token." + body + "." + signature, nil } func parseToken(token string) (tokenPayload, error) { parts := strings.Split(token, ".") if len(parts) != 3 || parts[0] != "fable-token" { return tokenPayload{}, errors.New("invalid token format") } if !hmac.Equal([]byte(parts[2]), []byte(computeTokenSignature(parts[1]))) { return tokenPayload{}, errors.New("invalid token signature") } bytes, err := base64.RawURLEncoding.DecodeString(parts[1]) if err != nil { return tokenPayload{}, err } var payload tokenPayload if err := json.Unmarshal(bytes, &payload); err != nil { return tokenPayload{}, err } if payload.ExpiresAt > 0 && time.Now().UTC().Unix() > payload.ExpiresAt { return tokenPayload{}, errors.New("token expired") } return payload, nil } func computeTokenSignature(body string) string { mac := hmac.New(sha256.New, []byte(tokenSecret())) _, _ = mac.Write([]byte(body)) return base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) }