package main
import (
"context"
"log/slog"
"net/http"
"net/url"
"os"
"os/signal"
"syscall"
"time"
"github.com/iabhishekrajput/anekdote-auth/internal/auth"
"github.com/iabhishekrajput/anekdote-auth/internal/config"
"github.com/iabhishekrajput/anekdote-auth/internal/crypto"
"github.com/iabhishekrajput/anekdote-auth/internal/handlers"
"github.com/iabhishekrajput/anekdote-auth/internal/mailer"
"github.com/iabhishekrajput/anekdote-auth/internal/server"
"github.com/iabhishekrajput/anekdote-auth/internal/store/postgres"
"github.com/iabhishekrajput/anekdote-auth/internal/store/redis"
"github.com/justinas/nosurf"
)
func main() {
// Initialize structured logger
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
slog.Info("Starting anekdote auth server...")
cfg := config.Load()
// 1. Initialize Datastores
db, err := postgres.InitDB(cfg.DBDsn)
if err != nil {
slog.Error("Failed to connect to Postgres", "error", err)
os.Exit(1)
}
defer db.Close()
rdb, err := redis.InitRedis(cfg.RedisDSN)
if err != nil {
slog.Error("Failed to connect to Redis", "error", err)
os.Exit(1)
}
// 2. Load Crypto Keys
keys, err := crypto.LoadKeys(cfg.RSAPrivateKey, cfg.RSAPublicKey)
if err != nil {
slog.Error("Failed to load RSA Keys", "error", err)
os.Exit(1)
}
// 3. Initialize Stores
userStore := postgres.NewUserStore(db)
clientStore := postgres.NewClientStore(db)
sessionStore := redis.NewSessionStore(rdb)
revocStore := redis.NewRevocationStore(rdb)
tokenStore := redis.NewTokenStore(rdb)
// 4. Initialize Core Server
issuer := cfg.AppURL
oauth2Srv := auth.BuildServer(clientStore, tokenStore, revocStore, keys, issuer)
// 5. Initialize Mailer
mailSvc, err := mailer.NewMailer(cfg)
if err != nil {
slog.Warn("Failed to initialize mailer, forgot password emails may not work", "error", err)
}
// 6. Initialize Handlers
identH := handlers.NewIdentityHandler(cfg, userStore, sessionStore, mailSvc)
oauthH := handlers.NewOAuth2Handler(oauth2Srv, sessionStore, revocStore, keys)
discH := handlers.NewDiscoveryHandler(keys)
accountH := handlers.NewAccountHandler(userStore)
// 7. Init Router
router := server.NewRouter(cfg, identH, oauthH, discH, accountH, sessionStore, rdb)
csrfHandler := nosurf.New(router)
csrfHandler.SetBaseCookie(http.Cookie{
Path: "/",
HttpOnly: true,
Secure: cfg.AppEnv == "production",
SameSite: http.SameSiteLaxMode,
})
// API endpoints for OAuth2 that don't use forms should be exempt from CSRF
csrfHandler.ExemptPath("/token")
csrfHandler.ExemptPath("/revoke")
csrfHandler.SetFailureHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
errStr := nosurf.Reason(r).Error()
ref := r.Referer()
if ref == "" {
ref = r.URL.Path
}
u, err := url.Parse(ref)
if err != nil {
u = &url.URL{Path: "/"}
}
q := u.Query()
q.Set("error", "Security Error: "+errStr)
u.RawQuery = q.Encode()
http.Redirect(w, r, u.String(), http.StatusFound)
}))
srv := &http.Server{
Addr: ":" + cfg.Port,
Handler: csrfHandler,
}
// 8. Start Server with Graceful Shutdown
go func() {
slog.Info("Server listening", "port", cfg.Port)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
slog.Error("ListenAndServe crashed", "error", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
<-quit
slog.Info("Server is shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
slog.Error("Server forced to shutdown", "error", err)
}
slog.Info("Server exited.")
}
package auth
import (
"context"
"errors"
"time"
"github.com/go-oauth2/oauth2/v4"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/iabhishekrajput/anekdote-auth/internal/crypto"
)
// JWTGenerator implements oauth2.AccessGenerate
type JWTGenerator struct {
keyStore *crypto.KeyStore
issuer string
}
func NewJWTGenerator(keyStore *crypto.KeyStore, issuer string) *JWTGenerator {
return &JWTGenerator{
keyStore: keyStore,
issuer: issuer,
}
}
// Token creates a signed JWT Access Token and an optional ID Token
func (g *JWTGenerator) Token(ctx context.Context, data *oauth2.GenerateBasic, isGenRefresh bool) (access, refresh string, err error) {
jti := uuid.New().String()
// 1. Generate Access Token (JWT)
claims := jwt.MapClaims{
"iss": g.issuer,
"sub": data.UserID, // Resource Owner ID
"aud": data.Client.GetID(), // Client ID
"exp": time.Now().Add(data.TokenInfo.GetAccessExpiresIn()).Unix(),
"iat": time.Now().Unix(),
"jti": jti, // JWT ID for fast revocation tracking
"scope": data.TokenInfo.GetScope(),
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
token.Header["kid"] = g.keyStore.KeyID // Standard for JWKS mapping
access, err = token.SignedString(g.keyStore.PrivateKey)
if err != nil {
return "", "", errors.New("internal server error signing jwt")
}
// 2. Refresh Token (Opaque string, no need for it to be a massive JWT)
if isGenRefresh {
refresh = uuid.New().String()
}
return access, refresh, nil
}
package auth
import (
"log/slog"
"time"
"github.com/go-oauth2/oauth2/v4/manage"
"github.com/go-oauth2/oauth2/v4/server"
oredis "github.com/go-oauth2/redis/v4"
"github.com/iabhishekrajput/anekdote-auth/internal/crypto"
"github.com/iabhishekrajput/anekdote-auth/internal/store/postgres"
"github.com/iabhishekrajput/anekdote-auth/internal/store/redis"
)
func BuildServer(
clientStore *postgres.ClientStore,
tokenStore *oredis.TokenStore,
revStore *redis.RevocationStore,
keyStore *crypto.KeyStore,
issuer string,
) *server.Server {
manager := manage.NewDefaultManager()
// 1. Token Expiration Configuration
manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg)
// 2. Map the Postgres Client Store
manager.MapClientStorage(clientStore)
// 3. For Tokens, use the Redis store
manager.MapTokenStorage(tokenStore)
// 4. Custom JWT Generation
jwtGen := NewJWTGenerator(keyStore, issuer)
manager.MapAccessGenerate(jwtGen)
// 5. Build the Server
srv := server.NewDefaultServer(manager)
srv.SetAllowGetAccessRequest(false) // Security: Require POST for tokens
srv.SetClientInfoHandler(server.ClientFormHandler)
// Enable PKCE (Proof Key for Code Exchange) Support
// go-oauth2 handles PKCE generation and validation automatically if requested by the client.
manager.SetAuthorizeCodeExp(time.Minute * 10)
slog.Info("OAuth2 Server Manager Initialized", "issuer", issuer)
return srv
}
package config
import (
"log/slog"
"os"
)
type Config struct {
Port string
AppURL string
DBDsn string
RedisDSN string
RSAPrivateKey string
RSAPublicKey string
SessionSecret string
SMTPHost string
SMTPPort string
SMTPUsername string
SMTPPassword string
SMTPFrom string
SMTPInsecureSkipVerify bool
AppEnv string
CORSAllowedOrigins string
}
func Load() *Config {
port := getEnvOrDefault("PORT", "8080")
dbDsn := getEnvOrDefault("DB_DSN", "postgres://authuser:authpassword@localhost:5432/authdb?sslmode=disable")
redisDsn := getEnvOrDefault("REDIS_URL", "redis://localhost:6379/0")
rsaPrivate := getEnvOrDefault("RSA_PRIVATE_KEY_PATH", "certs/private.pem")
rsaPublic := getEnvOrDefault("RSA_PUBLIC_KEY_PATH", "certs/public.pem")
sessionSecret := getEnvOrDefault("SESSION_SECRET", "super-secret-session-key-change-in-prod")
smtpHost := getEnvOrDefault("SMTP_HOST", "localhost")
smtpPort := getEnvOrDefault("SMTP_PORT", "1025")
smtpUser := getEnvOrDefault("SMTP_USERNAME", "test")
smtpPass := getEnvOrDefault("SMTP_PASSWORD", "test")
smtpFrom := getEnvOrDefault("SMTP_FROM", "noreply@anekdoteauth.local")
smtpInsecureSkipVerify := getEnvOrDefault("SMTP_INSECURE_SKIP_VERIFY", "false") == "true"
appEnv := getEnvOrDefault("APP_ENV", "development")
appURL := getEnvOrDefault("APP_URL", "http://localhost:"+port)
corsAllowed := getEnvOrDefault("CORS_ALLOWED_ORIGINS", "http://localhost:8080")
slog.Info("Configuration loaded", "port", port, "env", appEnv)
return &Config{
Port: port,
AppURL: appURL,
DBDsn: dbDsn,
RedisDSN: redisDsn,
RSAPrivateKey: rsaPrivate,
RSAPublicKey: rsaPublic,
SessionSecret: sessionSecret,
SMTPHost: smtpHost,
SMTPPort: smtpPort,
SMTPUsername: smtpUser,
SMTPPassword: smtpPass,
SMTPFrom: smtpFrom,
SMTPInsecureSkipVerify: smtpInsecureSkipVerify,
AppEnv: appEnv,
CORSAllowedOrigins: corsAllowed,
}
}
func getEnvOrDefault(key, fallback string) string {
if value, exists := os.LookupEnv(key); exists {
return value
}
return fallback
}
package crypto
import (
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"os"
"github.com/golang-jwt/jwt/v5"
)
type KeyStore struct {
PrivateKey *rsa.PrivateKey
PublicKey *rsa.PublicKey
KeyID string
}
func LoadKeys(privPath, pubPath string) (*KeyStore, error) {
privBytes, err := os.ReadFile(privPath)
if err != nil {
return nil, err
}
privKey, err := jwt.ParseRSAPrivateKeyFromPEM(privBytes)
if err != nil {
return nil, err
}
pubBytes, err := os.ReadFile(pubPath)
if err != nil {
return nil, err
}
pubKey, err := jwt.ParseRSAPublicKeyFromPEM(pubBytes)
if err != nil {
return nil, err
}
derBytes := x509.MarshalPKCS1PublicKey(pubKey)
hash := sha256.Sum256(derBytes)
keyID := base64.RawURLEncoding.EncodeToString(hash[:])
return &KeyStore{
PrivateKey: privKey,
PublicKey: pubKey,
KeyID: keyID,
}, nil
}
package handlers
import (
"net/http"
"net/url"
"github.com/google/uuid"
"github.com/iabhishekrajput/anekdote-auth/internal/models"
"github.com/iabhishekrajput/anekdote-auth/internal/store/postgres"
"github.com/iabhishekrajput/anekdote-auth/internal/types"
"github.com/iabhishekrajput/anekdote-auth/web/ui"
"github.com/julienschmidt/httprouter"
"github.com/justinas/nosurf"
"golang.org/x/crypto/bcrypt"
)
type AccountHandler struct {
userStore *postgres.UserStore
}
func NewAccountHandler(uStore *postgres.UserStore) *AccountHandler {
return &AccountHandler{
userStore: uStore,
}
}
func (h *AccountHandler) render(w http.ResponseWriter, r *http.Request, name string, data map[string]interface{}) {
if data == nil {
data = make(map[string]interface{})
}
if errStr := r.URL.Query().Get("error"); errStr != "" {
if _, exists := data["Error"]; !exists {
data["Error"] = errStr
}
}
if msgStr := r.URL.Query().Get("message"); msgStr != "" {
if _, exists := data["Success"]; !exists {
data["Success"] = msgStr
}
}
var errorMsg, successMsg string
if v, ok := data["Error"].(string); ok {
errorMsg = v
}
if v, ok := data["Success"].(string); ok {
successMsg = v
}
csrfToken := nosurf.Token(r)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
switch name {
case "account.tmpl":
user, _ := data["User"].(*models.User)
component := ui.AccountPage(csrfToken, user, errorMsg, successMsg)
_ = component.Render(r.Context(), w)
default:
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte("template not found"))
}
}
func (h *AccountHandler) ViewAccount(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
userID := r.Context().Value(types.UserContextKey).(uuid.UUID)
user, err := h.userStore.GetByID(userID)
if err != nil {
http.Redirect(w, r, "/login?error="+url.QueryEscape("Session user not found"), http.StatusFound)
return
}
errMsg := r.URL.Query().Get("error")
successMsg := r.URL.Query().Get("message")
h.render(w, r, "account.tmpl", map[string]interface{}{
"User": user,
"Error": errMsg,
"Success": successMsg,
})
}
func (h *AccountHandler) UpdateProfile(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
userID := r.Context().Value(types.UserContextKey).(uuid.UUID)
newName := r.FormValue("name")
if newName == "" {
http.Redirect(w, r, "/account?error="+url.QueryEscape("Name cannot be empty"), http.StatusFound)
return
}
err := h.userStore.UpdateName(userID, newName)
if err != nil {
http.Redirect(w, r, "/account?error="+url.QueryEscape("Failed to update profile"), http.StatusFound)
return
}
http.Redirect(w, r, "/account?message="+url.QueryEscape("Profile updated"), http.StatusFound)
}
func (h *AccountHandler) UpdatePassword(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
userID := r.Context().Value(types.UserContextKey).(uuid.UUID)
oldPassword := r.FormValue("old_password")
newPassword := r.FormValue("new_password")
if oldPassword == "" || newPassword == "" {
http.Redirect(w, r, "/account?error="+url.QueryEscape("Missing passwords"), http.StatusFound)
return
}
if err := validatePassword(newPassword); err != nil {
http.Redirect(w, r, "/account?error="+url.QueryEscape(err.Error()), http.StatusFound)
return
}
user, err := h.userStore.GetByID(userID)
if err != nil {
http.Redirect(w, r, "/account?error="+url.QueryEscape("User not found"), http.StatusFound)
return
}
// Verify old password
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(oldPassword))
if err != nil {
http.Redirect(w, r, "/account?error="+url.QueryEscape("Incorrect old password"), http.StatusFound)
return
}
// Hash new password
hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil {
http.Redirect(w, r, "/account?error="+url.QueryEscape("Server Error"), http.StatusFound)
return
}
err = h.userStore.UpdatePassword(userID, string(hash))
if err != nil {
http.Redirect(w, r, "/account?error="+url.QueryEscape("Failed to update password"), http.StatusFound)
return
}
http.Redirect(w, r, "/account?message="+url.QueryEscape("Password updated"), http.StatusFound)
}
package handlers
import (
"encoding/json"
"math/big"
"net/http"
"encoding/base64"
"github.com/iabhishekrajput/anekdote-auth/internal/crypto"
"github.com/julienschmidt/httprouter"
)
type DiscoveryHandler struct {
keyStore *crypto.KeyStore
}
func NewDiscoveryHandler(ks *crypto.KeyStore) *DiscoveryHandler {
return &DiscoveryHandler{keyStore: ks}
}
// JWK represents a single JSON Web Key
type JWK struct {
Kty string `json:"kty"`
Kid string `json:"kid"`
Use string `json:"use"`
N string `json:"n"`
E string `json:"e"`
Alg string `json:"alg"`
}
// JWKS represents the set of JSON Web Keys
type JWKS struct {
Keys []JWK `json:"keys"`
}
func (h *DiscoveryHandler) WellKnownJWKS(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
pubKey := h.keyStore.PublicKey
// Convert the RSA Exponent integer to bytes and base64url encode them
eBytes := big.NewInt(int64(pubKey.E)).Bytes()
eStr := base64.RawURLEncoding.EncodeToString(eBytes)
// Convert the RSA Modulus to bytes and base64url encode them
nStr := base64.RawURLEncoding.EncodeToString(pubKey.N.Bytes())
jwks := JWKS{
Keys: []JWK{
{
Kty: "RSA",
Kid: h.keyStore.KeyID,
Use: "sig",
N: nStr,
E: eStr,
Alg: "RS256",
},
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(jwks)
}
package handlers
import (
"context"
"crypto/rand"
"fmt"
"math/big"
"net/http"
"regexp"
"time"
"github.com/google/uuid"
"github.com/iabhishekrajput/anekdote-auth/internal/config"
"github.com/iabhishekrajput/anekdote-auth/internal/mailer"
"github.com/iabhishekrajput/anekdote-auth/internal/store/postgres"
"github.com/iabhishekrajput/anekdote-auth/internal/store/redis"
"github.com/iabhishekrajput/anekdote-auth/web/ui"
"github.com/julienschmidt/httprouter"
"github.com/justinas/nosurf"
"golang.org/x/crypto/bcrypt"
)
type IdentityHandler struct {
config *config.Config
userStore *postgres.UserStore
sessionStore *redis.SessionStore
mailer *mailer.Mailer
}
func NewIdentityHandler(cfg *config.Config, uStore *postgres.UserStore, sStore *redis.SessionStore, mailSvc *mailer.Mailer) *IdentityHandler {
return &IdentityHandler{
config: cfg,
userStore: uStore,
sessionStore: sStore,
mailer: mailSvc,
}
}
func (h *IdentityHandler) render(w http.ResponseWriter, r *http.Request, name string, data map[string]interface{}) {
if data == nil {
data = make(map[string]interface{})
}
if errStr := r.URL.Query().Get("error"); errStr != "" {
if _, exists := data["Error"]; !exists {
data["Error"] = errStr
}
}
if msgStr := r.URL.Query().Get("message"); msgStr != "" {
if _, exists := data["Success"]; !exists {
data["Success"] = msgStr
}
}
var errorMsg, successMsg string
if v, ok := data["Error"].(string); ok {
errorMsg = v
}
if v, ok := data["Success"].(string); ok {
successMsg = v
}
csrfToken := nosurf.Token(r)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
switch name {
case "register.tmpl":
component := ui.RegisterPage(csrfToken, errorMsg, successMsg)
_ = component.Render(r.Context(), w)
case "login.tmpl":
req, _ := data["Req"].(string)
component := ui.LoginPage(csrfToken, req, errorMsg, successMsg)
_ = component.Render(r.Context(), w)
case "forgot_password.tmpl":
component := ui.ForgotPasswordPage(csrfToken, errorMsg, successMsg)
_ = component.Render(r.Context(), w)
case "reset_password.tmpl":
token, _ := data["Token"].(string)
component := ui.ResetPasswordPage(csrfToken, token, errorMsg, successMsg)
_ = component.Render(r.Context(), w)
case "verify_email.tmpl":
userID, _ := data["UserID"].(string)
component := ui.VerifyEmailPage(csrfToken, userID, errorMsg, successMsg)
_ = component.Render(r.Context(), w)
default:
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte("template not found"))
}
}
func (h *IdentityHandler) RegisterFunc(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
if r.Method == http.MethodGet {
h.render(w, r, "register.tmpl", nil)
return
}
email := r.FormValue("email")
password := r.FormValue("password")
name := r.FormValue("name")
if email == "" || password == "" {
w.WriteHeader(http.StatusBadRequest)
h.render(w, r, "register.tmpl", map[string]interface{}{"Error": "Email and password required"})
return
}
if err := validatePassword(password); err != nil {
w.WriteHeader(http.StatusBadRequest)
h.render(w, r, "register.tmpl", map[string]interface{}{"Error": err.Error()})
return
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
h.render(w, r, "register.tmpl", map[string]interface{}{"Error": "Server Error"})
return
}
user, err := h.userStore.Create(email, name, string(hash))
if err != nil {
w.WriteHeader(http.StatusConflict)
h.render(w, r, "register.tmpl", map[string]interface{}{"Error": "Error creating user (maybe email exists)"})
return
}
// Generate 6-digit OTP
otp, _ := generateOTP()
if h.mailer != nil {
_ = h.sessionStore.CreateOTP(context.Background(), user.ID, otp)
_ = h.mailer.SendOTP(context.Background(), user.Email, otp)
} else {
// Log the OTP if no mailer is configured for dev
fmt.Printf("[DEV] OTP for %s: %s\n", email, otp)
_ = h.sessionStore.CreateOTP(context.Background(), user.ID, otp)
}
http.Redirect(w, r, fmt.Sprintf("/verify-email?user_id=%s", user.ID.String()), http.StatusFound)
}
// Helper to generate a 6-digit cryptographic OTP
func generateOTP() (string, error) {
max := big.NewInt(1000000)
n, err := rand.Int(rand.Reader, max)
if err != nil {
return "", err
}
return fmt.Sprintf("%06d", n.Int64()), nil
}
// validatePassword checks if a password meets complexity requirements
func validatePassword(password string) error {
if len(password) < 8 {
return fmt.Errorf("password must be at least 8 characters long")
}
if !regexp.MustCompile(`[a-z]`).MatchString(password) {
return fmt.Errorf("password must contain at least one lowercase letter")
}
if !regexp.MustCompile(`[A-Z]`).MatchString(password) {
return fmt.Errorf("password must contain at least one uppercase letter")
}
if !regexp.MustCompile(`[0-9]`).MatchString(password) {
return fmt.Errorf("password must contain at least one number")
}
if !regexp.MustCompile(`[!@#~$%^&*(),.?":{}|<>]`).MatchString(password) {
return fmt.Errorf("password must contain at least one special character")
}
return nil
}
func (h *IdentityHandler) VerifyEmailFunc(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
if r.Method == http.MethodGet {
userID := r.URL.Query().Get("user_id")
h.render(w, r, "verify_email.tmpl", map[string]interface{}{
"UserID": userID,
})
return
}
userIDStr := r.FormValue("user_id")
otp := r.FormValue("otp")
if userIDStr == "" || otp == "" {
w.WriteHeader(http.StatusBadRequest)
h.render(w, r, "verify_email.tmpl", map[string]interface{}{"Error": "User ID and OTP required", "UserID": userIDStr})
return
}
userID, err := uuid.Parse(userIDStr)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
h.render(w, r, "verify_email.tmpl", map[string]interface{}{"Error": "Invalid User ID format", "UserID": userIDStr})
return
}
valid, err := h.sessionStore.VerifyOTP(context.Background(), userID, otp)
if err != nil || !valid {
w.WriteHeader(http.StatusUnauthorized)
h.render(w, r, "verify_email.tmpl", map[string]interface{}{"Error": "Invalid or expired OTP", "UserID": userIDStr})
return
}
// Update the database to mark user as verified
err = h.userStore.UpdateVerified(userID)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
h.render(w, r, "verify_email.tmpl", map[string]interface{}{"Error": "Failed to update user status", "UserID": userIDStr})
return
}
// Automatically log the user in by creating a session
sessionID, err := h.sessionStore.Create(context.Background(), userID)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
h.render(w, r, "login.tmpl", map[string]interface{}{"Error": "Verified but failed to create session. Please login."})
return
}
http.SetCookie(w, &http.Cookie{
Name: "auth_session",
Value: sessionID,
Path: "/",
Expires: time.Now().Add(24 * time.Hour),
HttpOnly: true,
Secure: h.config.AppEnv == "production",
SameSite: http.SameSiteLaxMode,
})
http.Redirect(w, r, "/account", http.StatusFound)
}
func (h *IdentityHandler) LoginFunc(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
if r.Method == http.MethodGet {
reqURI := r.URL.Query().Get("req")
h.render(w, r, "login.tmpl", map[string]interface{}{
"Req": reqURI,
})
return
}
email := r.FormValue("email")
password := r.FormValue("password")
oauthReq := r.FormValue("req") // Originating OAuth request URL
fails, _ := h.sessionStore.GetFailedLogin(context.Background(), email)
if fails >= 5 {
w.WriteHeader(http.StatusTooManyRequests)
h.render(w, r, "login.tmpl", map[string]interface{}{"Error": "Account locked due to too many failed attempts. Try again in 15 minutes.", "Req": oauthReq})
return
}
user, err := h.userStore.GetByEmail(email)
if err != nil {
h.sessionStore.IncrementFailedLogin(context.Background(), email)
w.WriteHeader(http.StatusUnauthorized)
h.render(w, r, "login.tmpl", map[string]interface{}{"Error": "Invalid credentials", "Req": oauthReq})
return
}
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password))
if err != nil {
h.sessionStore.IncrementFailedLogin(context.Background(), email)
w.WriteHeader(http.StatusUnauthorized)
h.render(w, r, "login.tmpl", map[string]interface{}{"Error": "Invalid credentials", "Req": oauthReq})
return
}
h.sessionStore.ResetFailedLogin(context.Background(), email)
if !user.IsVerified {
w.WriteHeader(http.StatusForbidden)
h.render(w, r, "login.tmpl", map[string]interface{}{
"Error": "Please check your email and verify your account first, then enter the verification code for your account.",
"Req": oauthReq,
})
return
}
// Create Session in Redis
sessionID, err := h.sessionStore.Create(context.Background(), user.ID)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
h.render(w, r, "login.tmpl", map[string]interface{}{"Error": "Server Error", "Req": oauthReq})
return
}
// Set Cookie
http.SetCookie(w, &http.Cookie{
Name: "auth_session",
Value: sessionID,
Path: "/",
Expires: time.Now().Add(24 * time.Hour),
HttpOnly: true,
Secure: h.config.AppEnv == "production",
SameSite: http.SameSiteLaxMode,
})
// Redirect back to Authorization flow if it exists
if oauthReq != "" {
http.Redirect(w, r, oauthReq, http.StatusFound)
return
}
http.Redirect(w, r, "/account", http.StatusFound)
}
func (h *IdentityHandler) LogoutFunc(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
cookie, err := r.Cookie("auth_session")
if err == nil && cookie.Value != "" {
// Invalidate session in Redis
_ = h.sessionStore.Delete(context.Background(), cookie.Value)
}
// Clear the cookie in the browser
http.SetCookie(w, &http.Cookie{
Name: "auth_session",
Value: "",
Path: "/",
Expires: time.Unix(0, 0),
HttpOnly: true,
})
http.Redirect(w, r, "/login", http.StatusFound)
}
func (h *IdentityHandler) ForgotPasswordFunc(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
if r.Method == http.MethodGet {
h.render(w, r, "forgot_password.tmpl", nil)
return
}
email := r.FormValue("email")
user, err := h.userStore.GetByEmail(email)
if err != nil {
// Do not reveal if email exists or not to prevent enumeration
h.render(w, r, "forgot_password.tmpl", map[string]interface{}{"Success": "If your email is registered, you will receive a reset link shortly."})
return
}
resetToken, err := h.sessionStore.CreateResetToken(context.Background(), user.ID)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
h.render(w, r, "forgot_password.tmpl", map[string]interface{}{"Error": "Error generating token"})
return
}
resetLink := "http://" + r.Host + "/reset-password?token=" + resetToken
if h.mailer != nil {
err = h.mailer.SendPasswordReset(context.Background(), user.Email, resetLink)
if err != nil {
// Do not log the specific email out to the client
w.WriteHeader(http.StatusInternalServerError)
h.render(w, r, "forgot_password.tmpl", map[string]interface{}{"Error": "Failed to dispatch email"})
return
}
} else {
// Fallback logging for local testing if SMTP config is missing
h.render(w, r, "forgot_password.tmpl", map[string]interface{}{
"Success": "Reset link generated (check logs/console).",
})
return
}
h.render(w, r, "forgot_password.tmpl", map[string]interface{}{"Success": "Reset link dispatched! Please check your email inbox."})
}
func (h *IdentityHandler) ResetPasswordFunc(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
token := r.URL.Query().Get("token")
if token == "" {
token = r.FormValue("token") // Try Post body
}
if r.Method == http.MethodGet {
h.render(w, r, "reset_password.tmpl", map[string]interface{}{
"Token": token,
})
return
}
password := r.FormValue("password")
if token == "" || password == "" {
w.WriteHeader(http.StatusBadRequest)
h.render(w, r, "reset_password.tmpl", map[string]interface{}{"Error": "Missing inputs", "Token": token})
return
}
if err := validatePassword(password); err != nil {
w.WriteHeader(http.StatusBadRequest)
h.render(w, r, "reset_password.tmpl", map[string]interface{}{"Error": err.Error(), "Token": token})
return
}
userID, err := h.sessionStore.GetUserByResetToken(context.Background(), token)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
h.render(w, r, "reset_password.tmpl", map[string]interface{}{"Error": "Invalid or expired token", "Token": token})
return
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
h.render(w, r, "reset_password.tmpl", map[string]interface{}{"Error": "Server Error", "Token": token})
return
}
err = h.userStore.UpdatePassword(userID, string(hash))
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
h.render(w, r, "reset_password.tmpl", map[string]interface{}{"Error": "Failed to update password", "Token": token})
return
}
// Invalidate the token so it can't be reused
h.sessionStore.DeleteResetToken(context.Background(), token)
h.render(w, r, "login.tmpl", map[string]interface{}{"Success": "Password updated successfully! Please login."})
}
package handlers
import (
"log/slog"
"net/http"
"net/url"
"strings"
"time"
oautherrors "github.com/go-oauth2/oauth2/v4/errors"
"github.com/go-oauth2/oauth2/v4/server"
"github.com/golang-jwt/jwt/v5"
"github.com/iabhishekrajput/anekdote-auth/internal/crypto"
"github.com/iabhishekrajput/anekdote-auth/internal/store/redis"
"github.com/iabhishekrajput/anekdote-auth/web/ui"
"github.com/julienschmidt/httprouter"
"github.com/justinas/nosurf"
)
type OAuth2Handler struct {
server *server.Server
sessionStore *redis.SessionStore
revocStore *redis.RevocationStore
keyStore *crypto.KeyStore
}
func NewOAuth2Handler(srv *server.Server, sess *redis.SessionStore, rev *redis.RevocationStore, keys *crypto.KeyStore) *OAuth2Handler {
h := &OAuth2Handler{
server: srv,
sessionStore: sess,
revocStore: rev,
keyStore: keys,
}
h.server.SetUserAuthorizationHandler(h.userAuthorizeHandler)
return h
}
// Authorize handles the initial redirect from the client
func (h *OAuth2Handler) Authorize(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
// 1. Check if the user is logged in
userID, err := h.sessionStore.GetUserFromSession(r)
if err != nil || userID.String() == "00000000-0000-0000-0000-000000000000" {
// Store the current URL to redirect back after login
loginURL := "/login?req=" + url.QueryEscape(r.URL.String())
http.Redirect(w, r, loginURL, http.StatusFound)
return
}
// 2. Parse the request form so go-oauth2 can process both URL query params and POST values
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
err = h.server.HandleAuthorizeRequest(w, r)
if err != nil {
slog.Error("Authorize Request Error", "error", err)
http.Error(w, err.Error(), http.StatusBadRequest)
}
}
func (h *OAuth2Handler) userAuthorizeHandler(w http.ResponseWriter, r *http.Request) (userID string, err error) {
// 1. Double check user is logged in
uid, err := h.sessionStore.GetUserFromSession(r)
if err != nil || uid.String() == "00000000-0000-0000-0000-000000000000" {
http.Redirect(w, r, "/login?req="+url.QueryEscape(r.URL.String()), http.StatusFound)
return "", nil // returning empty userID stops go-oauth2 processing
}
// 2. Handle Consent Form Submission
if r.Method == http.MethodPost {
if r.FormValue("accept") == "true" {
// User approved!
return uid.String(), nil
}
// User rejected request
return "", oautherrors.ErrAccessDenied
}
// 3. Render the Consent UI for GET request
clientID := r.FormValue("client_id")
if clientID == "" { // Fallback just in case
clientID = "Unknown Application"
}
// Parse requested scopes
var requestedScopes []string
if scope := r.FormValue("scope"); scope != "" {
requestedScopes = strings.Split(scope, " ")
} else {
requestedScopes = []string{"openid", "profile"}
}
csrfToken := nosurf.Token(r)
ui.ConsentPage(clientID, requestedScopes, csrfToken, "", "", "").Render(r.Context(), w)
// Since we rendered the HTML response, we return empty userID and NO error
// to tell go-oauth2 to halt and not overwrite the response.
return "", nil
}
// Token handles the exchange of an Authorization Code (or Refresh Token) for an Access JWT
func (h *OAuth2Handler) Token(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
err := h.server.HandleTokenRequest(w, r)
if err != nil {
slog.Error("Token Request Error", "error", err)
// The `go-oauth2` engine writes standard JSON error responses natively here.
}
}
// Revoke handles invalidating a specific JWT by its JTI blocklist, or deleting a refresh token
func (h *OAuth2Handler) Revoke(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
tokenStr := r.FormValue("token")
if tokenStr == "" {
http.Error(w, "missing token parameter", http.StatusBadRequest)
return
}
tokenTypeHint := r.FormValue("token_type_hint")
// RFC 7009: The server responds with HTTP 200 OK regardless of whether the token
// was valid/found or not, to prevent leaking information. Only 500s or 400s on bad requests.
// Try JWT parsing first (Access Tokens) unless explicitly hinted heavily otherwise
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (any, error) {
return h.keyStore.PublicKey, nil
}, jwt.WithoutClaimsValidation(), jwt.WithValidMethods([]string{"RS256"}))
if err == nil && token.Valid {
if claims, ok := token.Claims.(jwt.MapClaims); ok {
if jti, ok := claims["jti"].(string); ok && jti != "" {
// Blocklist the JTI in Redis
_ = h.revocStore.RevokeJTI(r.Context(), jti, 10*time.Hour)
w.WriteHeader(http.StatusOK)
return
}
}
}
// If parsing as JWT failed, or it lacked a JTI, it's likely a Refresh Token (which our generator makes as UUIDs).
// Or maybe the token type hint specifically suggests it.
if tokenTypeHint == "refresh_token" || err != nil {
_ = h.server.Manager.RemoveRefreshToken(r.Context(), tokenStr)
} else {
// Just to be safe, try removing it as both if neither hint nor JWT structural match worked.
_ = h.server.Manager.RemoveAccessToken(r.Context(), tokenStr)
_ = h.server.Manager.RemoveRefreshToken(r.Context(), tokenStr)
}
w.WriteHeader(http.StatusOK)
}
package handlers
import (
"encoding/json"
"net/http"
"github.com/julienschmidt/httprouter"
)
// OIDCConfig represents the OpenID Connect discovery document
type OIDCConfig struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
JwksURI string `json:"jwks_uri"`
ResponseTypesSupported []string `json:"response_types_supported"`
SubjectTypesSupported []string `json:"subject_types_supported"`
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
RevocationEndpoint string `json:"revocation_endpoint,omitempty"`
}
// OpenIDConfiguration serves the OIDC discovery document
func (h *DiscoveryHandler) OpenIDConfiguration(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
baseURL := "http://" + r.Host // In prod, determine HTTPS appropriately
config := OIDCConfig{
Issuer: baseURL,
AuthorizationEndpoint: baseURL + "/authorize",
TokenEndpoint: baseURL + "/token",
JwksURI: baseURL + "/.well-known/jwks.json",
RevocationEndpoint: baseURL + "/revoke",
ResponseTypesSupported: []string{"code", "token", "id_token"},
SubjectTypesSupported: []string{"public"},
IDTokenSigningAlgValuesSupported: []string{"RS256"},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(config)
}
package mailer
import (
"bytes"
"context"
"crypto/tls"
"strconv"
"github.com/iabhishekrajput/anekdote-auth/internal/config"
uiemail "github.com/iabhishekrajput/anekdote-auth/web/ui/email"
"github.com/wneessen/go-mail"
)
type Mailer struct {
config *config.Config
client *mail.Client
}
func NewMailer(cfg *config.Config) (*Mailer, error) {
port, err := strconv.Atoi(cfg.SMTPPort)
if err != nil {
port = 587
}
opts := []mail.Option{
mail.WithPort(port),
mail.WithSMTPAuth(mail.SMTPAuthPlain),
mail.WithUsername(cfg.SMTPUsername),
mail.WithPassword(cfg.SMTPPassword),
}
if cfg.SMTPInsecureSkipVerify {
opts = append(opts, mail.WithTLSPolicy(mail.TLSMandatory))
opts = append(opts, mail.WithTLSConfig(&tls.Config{InsecureSkipVerify: true}))
}
client, err := mail.NewClient(cfg.SMTPHost, opts...)
if err != nil {
return nil, err
}
return &Mailer{
config: cfg,
client: client,
}, nil
}
func (m *Mailer) SendPasswordReset(ctx context.Context, toEmail, resetLink string) error {
msg := mail.NewMsg()
if err := msg.From(m.config.SMTPFrom); err != nil {
return err
}
if err := msg.To(toEmail); err != nil {
return err
}
msg.Subject("Password Reset - anekdote")
var body bytes.Buffer
if err := uiemail.PasswordResetEmail(resetLink).Render(ctx, &body); err != nil {
return err
}
msg.SetBodyString(mail.TypeTextHTML, body.String())
return m.client.DialAndSendWithContext(ctx, msg)
}
func (m *Mailer) SendOTP(ctx context.Context, toEmail, otp string) error {
msg := mail.NewMsg()
if err := msg.From(m.config.SMTPFrom); err != nil {
return err
}
if err := msg.To(toEmail); err != nil {
return err
}
msg.Subject("Verify Your Email - anekdote")
var body bytes.Buffer
if err := uiemail.VerifyEmailOTPEmail(otp).Render(ctx, &body); err != nil {
return err
}
msg.SetBodyString(mail.TypeTextHTML, body.String())
return m.client.DialAndSendWithContext(ctx, msg)
}
package middleware
import (
"context"
"net/http"
"github.com/iabhishekrajput/anekdote-auth/internal/store/redis"
"github.com/iabhishekrajput/anekdote-auth/internal/types"
"github.com/julienschmidt/httprouter"
)
// RequireAuth is a middleware that enforces an active user session.
func RequireAuth(sessionStore *redis.SessionStore, next httprouter.Handle) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
userID, err := sessionStore.GetUserFromSession(r)
if err != nil {
// No valid session, redirect to login
http.Redirect(w, r, "/login?req="+r.URL.Path, http.StatusFound)
return
}
// Inject User ID into request context
ctx := context.WithValue(r.Context(), types.UserContextKey, userID)
r = r.WithContext(ctx)
next(w, r, ps)
}
}
// RedirectIfAuthenticated is a middleware that redirects already logged-in users away from auth pages.
func RedirectIfAuthenticated(sessionStore *redis.SessionStore, next httprouter.Handle) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
_, err := sessionStore.GetUserFromSession(r)
if err == nil {
// User is already logged in, redirect to account
http.Redirect(w, r, "/account", http.StatusFound)
return
}
next(w, r, ps)
}
}
package middleware
import (
"context"
"net/http"
"net/url"
"time"
"github.com/go-redis/redis/v8"
"github.com/julienschmidt/httprouter"
)
// SecurityHeadersMiddleware adds standard web security headers to responses
func SecurityHeadersMiddleware(corsAllowed string) func(httprouter.Handle) httprouter.Handle {
return func(next httprouter.Handle) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-XSS-Protection", "1; mode=block")
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
w.Header().Set("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com")
// Configured CORS headers for OIDC/OAuth2 APIs
w.Header().Set("Access-Control-Allow-Origin", corsAllowed)
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next(w, r, ps)
}
}
}
// RateLimitMiddleware provides a basic Redis-backed fixed-window rate limiter
func RateLimitMiddleware(client *redis.Client, prefix string, limit int, window time.Duration, next httprouter.Handle) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
clientIP := r.Header.Get("X-Forwarded-For")
if clientIP == "" {
clientIP = r.Header.Get("X-Real-IP")
}
if clientIP == "" {
clientIP = r.RemoteAddr
}
key := "rate_limit:" + prefix + ":" + clientIP
ctx := context.Background()
// Increment request count
count, err := client.Incr(ctx, key).Result()
if err != nil {
redirectErr(w, r, "Internal router error")
return
}
// Set expiry on first request in window
if count == 1 {
client.Expire(ctx, key, window)
}
if count > int64(limit) {
redirectErr(w, r, "Rate limit exceeded. Please try again later.")
return
}
next(w, r, ps)
}
}
func redirectErr(w http.ResponseWriter, r *http.Request, errMsg string) {
ref := r.Referer()
if ref == "" {
ref = r.URL.Path
}
u, err := url.Parse(ref)
if err != nil {
u = &url.URL{Path: "/"}
}
q := u.Query()
q.Set("error", errMsg)
u.RawQuery = q.Encode()
http.Redirect(w, r, u.String(), http.StatusFound)
}
// Chain allows wrapping a handler in multiple middlewares easily
func Chain(handler httprouter.Handle, middlewares ...func(httprouter.Handle) httprouter.Handle) httprouter.Handle {
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
package server
import (
"log/slog"
"net/http"
"time"
"github.com/go-redis/redis/v8"
"github.com/iabhishekrajput/anekdote-auth/internal/config"
"github.com/iabhishekrajput/anekdote-auth/internal/handlers"
"github.com/iabhishekrajput/anekdote-auth/internal/middleware"
redisstore "github.com/iabhishekrajput/anekdote-auth/internal/store/redis"
"github.com/julienschmidt/httprouter"
)
func NewRouter(
cfg *config.Config,
identH *handlers.IdentityHandler,
oauthH *handlers.OAuth2Handler,
discH *handlers.DiscoveryHandler,
accountH *handlers.AccountHandler,
sessionStore *redisstore.SessionStore,
redisClient *redis.Client,
) *httprouter.Router {
router := httprouter.New()
// Apply Middlewares
secure := func(h httprouter.Handle) httprouter.Handle {
return middleware.Chain(h,
middleware.SecurityHeadersMiddleware(cfg.CORSAllowedOrigins),
func(next httprouter.Handle) httprouter.Handle {
return middleware.RateLimitMiddleware(redisClient, "global", 100, time.Minute, next)
},
)
}
authRateLimit := func(h httprouter.Handle) httprouter.Handle {
return middleware.Chain(h, func(next httprouter.Handle) httprouter.Handle {
return middleware.RateLimitMiddleware(redisClient, "auth", 10, time.Minute, next)
})
}
secureUnauth := func(h httprouter.Handle) httprouter.Handle {
return secure(authRateLimit(middleware.RedirectIfAuthenticated(sessionStore, h)))
}
// 1. Identity Endpoints (UI / Form Submissions)
router.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
http.Redirect(w, r, "/login", http.StatusFound)
})
router.GET("/register", secureUnauth(identH.RegisterFunc))
router.POST("/register", secureUnauth(identH.RegisterFunc))
router.GET("/login", secureUnauth(identH.LoginFunc))
router.POST("/login", secureUnauth(identH.LoginFunc))
router.GET("/verify-email", secureUnauth(identH.VerifyEmailFunc))
router.POST("/verify-email", secureUnauth(identH.VerifyEmailFunc))
router.GET("/forgot-password", secureUnauth(identH.ForgotPasswordFunc))
router.POST("/forgot-password", secureUnauth(identH.ForgotPasswordFunc))
router.GET("/reset-password", secureUnauth(identH.ResetPasswordFunc))
router.POST("/reset-password", secureUnauth(identH.ResetPasswordFunc))
router.POST("/logout", secure(identH.LogoutFunc))
router.GET("/account", secure(middleware.RequireAuth(sessionStore, accountH.ViewAccount)))
router.POST("/account/profile", secure(middleware.RequireAuth(sessionStore, accountH.UpdateProfile)))
router.POST("/account/password", secure(middleware.RequireAuth(sessionStore, accountH.UpdatePassword)))
// 2. OAuth2 Endpoints
router.GET("/authorize", secure(oauthH.Authorize))
router.POST("/authorize", secure(oauthH.Authorize)) // Depending on flow
router.POST("/token", secure(oauthH.Token))
router.POST("/revoke", secure(oauthH.Revoke))
// 3. Discovery (OIDC/JWKS)
router.GET("/.well-known/jwks.json", secure(discH.WellKnownJWKS))
router.GET("/.well-known/openid-configuration", secure(discH.OpenIDConfiguration))
// 4. Static Files
router.ServeFiles("/static/*filepath", http.Dir("web/static"))
slog.Info("Router initialized with endpoints")
return router
}
package postgres
import (
"context"
"database/sql"
"errors"
"github.com/go-oauth2/oauth2/v4"
"github.com/go-oauth2/oauth2/v4/models"
)
// ClientStore implements oauth2.ClientStore interface using PostgreSQL
type ClientStore struct {
db *sql.DB
}
// NewClientStore creates a new PostgreSQL backed client store
func NewClientStore(db *sql.DB) *ClientStore {
return &ClientStore{db: db}
}
// GetByID retrieves a client by its ID
func (s *ClientStore) GetByID(ctx context.Context, id string) (oauth2.ClientInfo, error) {
var (
secret string
domain string
public bool
)
err := s.db.QueryRowContext(ctx, "SELECT secret, domain, public FROM oauth2_clients WHERE id = $1", id).Scan(&secret, &domain, &public)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil // oauth2 framework expects nil, nil when client is not found
}
return nil, err
}
return &models.Client{
ID: id,
Secret: secret,
Domain: domain,
Public: public,
}, nil
}
package postgres
import (
"database/sql"
"log/slog"
_ "github.com/lib/pq"
)
// InitDB initializes the postgres database connection pool
func InitDB(dsn string) (*sql.DB, error) {
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, err
}
if err = db.Ping(); err != nil {
return nil, err
}
slog.Info("Connected to PostgreSQL successfully")
return db, nil
}
package postgres
import (
"database/sql"
"errors"
"github.com/google/uuid"
"github.com/iabhishekrajput/anekdote-auth/internal/models"
)
var ErrUserNotFound = errors.New("user not found")
type UserStore struct {
db *sql.DB
}
func NewUserStore(db *sql.DB) *UserStore {
return &UserStore{db: db}
}
func (s *UserStore) GetByEmail(email string) (*models.User, error) {
u := &models.User{}
err := s.db.QueryRow(`
SELECT id, email, name, password_hash, is_verified, created_at, updated_at
FROM users WHERE email = $1`, email).
Scan(&u.ID, &u.Email, &u.Name, &u.PasswordHash, &u.IsVerified, &u.CreatedAt, &u.UpdatedAt)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrUserNotFound
}
return nil, err
}
return u, nil
}
func (s *UserStore) GetByID(id uuid.UUID) (*models.User, error) {
u := &models.User{}
err := s.db.QueryRow(`
SELECT id, email, name, password_hash, is_verified, created_at, updated_at
FROM users WHERE id = $1`, id).
Scan(&u.ID, &u.Email, &u.Name, &u.PasswordHash, &u.IsVerified, &u.CreatedAt, &u.UpdatedAt)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrUserNotFound
}
return nil, err
}
return u, nil
}
func (s *UserStore) Create(email, name, passwordHash string) (*models.User, error) {
u := &models.User{}
err := s.db.QueryRow(`
INSERT INTO users (email, name, password_hash)
VALUES ($1, $2, $3)
RETURNING id, email, name, password_hash, is_verified, created_at, updated_at`,
email, name, passwordHash).
Scan(&u.ID, &u.Email, &u.Name, &u.PasswordHash, &u.IsVerified, &u.CreatedAt, &u.UpdatedAt)
if err != nil {
return nil, err
}
return u, nil
}
func (s *UserStore) UpdateName(id uuid.UUID, newName string) error {
_, err := s.db.Exec(`UPDATE users SET name = $1, updated_at = NOW() WHERE id = $2`, newName, id)
return err
}
func (s *UserStore) UpdatePassword(id uuid.UUID, newHash string) error {
_, err := s.db.Exec(`UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2`, newHash, id)
return err
}
func (s *UserStore) UpdateVerified(id uuid.UUID) error {
_, err := s.db.Exec(`UPDATE users SET is_verified = TRUE, updated_at = NOW() WHERE id = $1`, id)
return err
}
package redis
import (
"context"
"log/slog"
"github.com/go-redis/redis/v8"
)
// InitRedis initializes the go-redis client
func InitRedis(dsn string) (*redis.Client, error) {
opt, err := redis.ParseURL(dsn)
if err != nil {
return nil, err
}
client := redis.NewClient(opt)
// Ping to verify connection
ctx := context.Background()
if err := client.Ping(ctx).Err(); err != nil {
return nil, err
}
slog.Info("Connected to Redis successfully")
return client, nil
}
package redis
import (
"context"
"time"
"github.com/go-redis/redis/v8"
)
type RevocationStore struct {
client *redis.Client
}
func NewRevocationStore(client *redis.Client) *RevocationStore {
return &RevocationStore{client: client}
}
// RevokeJTI adds a token's JTI to the blocklist in Redis for the remainder of its TTL.
func (s *RevocationStore) RevokeJTI(ctx context.Context, jti string, duration time.Duration) error {
key := "revoked_jti:" + jti
return s.client.Set(ctx, key, "revoked", duration).Err()
}
// IsRevoked checks if a JTI is currently in the blocklist.
func (s *RevocationStore) IsRevoked(ctx context.Context, jti string) (bool, error) {
key := "revoked_jti:" + jti
val, err := s.client.Get(ctx, key).Result()
if err == redis.Nil {
return false, nil // Not revoked
} else if err != nil {
return false, err // Redis error
}
return val == "revoked", nil
}
package redis
import (
"context"
"errors"
"net/http"
"time"
"github.com/go-redis/redis/v8"
"github.com/google/uuid"
)
const (
sessionTTL = 24 * time.Hour
otpTTL = 15 * time.Minute
)
var ErrSessionNotFound = errors.New("session not found")
type SessionStore struct {
client *redis.Client
}
func NewSessionStore(client *redis.Client) *SessionStore {
return &SessionStore{client: client}
}
// Create generates a new session ID for a given userID and stores it in Redis
func (s *SessionStore) Create(ctx context.Context, userID uuid.UUID) (string, error) {
sessionID := uuid.New().String()
key := "session:" + sessionID
err := s.client.Set(ctx, key, userID.String(), sessionTTL).Err()
if err != nil {
return "", err
}
return sessionID, nil
}
// Get retrieves the userID associated with a session ID
func (s *SessionStore) Get(ctx context.Context, sessionID string) (uuid.UUID, error) {
key := "session:" + sessionID
val, err := s.client.Get(ctx, key).Result()
if err != nil {
if errors.Is(err, redis.Nil) {
return uuid.Nil, ErrSessionNotFound
}
return uuid.Nil, err
}
return uuid.Parse(val)
}
// Delete removes a session ID from Redis (Logout)
func (s *SessionStore) Delete(ctx context.Context, sessionID string) error {
key := "session:" + sessionID
return s.client.Del(ctx, key).Err()
}
// GetUserFromSession is a helper to extract the UUID from the request cookie
func (s *SessionStore) GetUserFromSession(r *http.Request) (uuid.UUID, error) {
cookie, err := r.Cookie("auth_session")
if err != nil {
return uuid.Nil, err
}
return s.Get(context.Background(), cookie.Value)
}
// CreateOTP generates and stores a 6-digit OTP for the specified userID in Redis
func (s *SessionStore) CreateOTP(ctx context.Context, userID uuid.UUID, otp string) error {
key := "otp:" + userID.String()
return s.client.Set(ctx, key, otp, otpTTL).Err()
}
// VerifyOTP checks if the provided OTP matches what is stored in Redis
// Returns a bool indicating success.
func (s *SessionStore) VerifyOTP(ctx context.Context, userID uuid.UUID, submittedOTP string) (bool, error) {
key := "otp:" + userID.String()
val, err := s.client.Get(ctx, key).Result()
if err != nil {
if errors.Is(err, redis.Nil) {
return false, nil // Code doesn't exist or expired
}
return false, err // Redis connection error
}
if val == submittedOTP {
// Valid OTP, immediately consume it to prevent reuse
s.client.Del(ctx, key)
return true, nil
}
return false, nil
}
// IncrementFailedLogin tracks failed login attempts for an email and returns the new count
func (s *SessionStore) IncrementFailedLogin(ctx context.Context, email string) (int, error) {
key := "failed_login:" + email
count, err := s.client.Incr(ctx, key).Result()
if err != nil {
return 0, err
}
if count == 1 {
s.client.Expire(ctx, key, 15*time.Minute)
}
return int(count), nil
}
// ResetFailedLogin clears the failed login attempts
func (s *SessionStore) ResetFailedLogin(ctx context.Context, email string) error {
key := "failed_login:" + email
return s.client.Del(ctx, key).Err()
}
// GetFailedLogin returns the current failed login count
func (s *SessionStore) GetFailedLogin(ctx context.Context, email string) (int, error) {
key := "failed_login:" + email
val, err := s.client.Get(ctx, key).Int()
if err != nil {
if errors.Is(err, redis.Nil) {
return 0, nil
}
return 0, err
}
return val, nil
}
// CreateResetToken generates a short-lived token for password recovery
func (s *SessionStore) CreateResetToken(ctx context.Context, userID uuid.UUID) (string, error) {
resetToken := uuid.New().String()
key := "reset_token:" + resetToken
// Reset tokens expire in 15 minutes for security
err := s.client.Set(ctx, key, userID.String(), 15*time.Minute).Err()
if err != nil {
return "", err
}
return resetToken, nil
}
// GetUserByResetToken retrieves the user ID from a valid reset token
func (s *SessionStore) GetUserByResetToken(ctx context.Context, resetToken string) (uuid.UUID, error) {
key := "reset_token:" + resetToken
val, err := s.client.Get(ctx, key).Result()
if err != nil {
return uuid.Nil, err // Could be redis.Nil if expired
}
return uuid.Parse(val)
}
// DeleteResetToken invalidates a reset token after use
func (s *SessionStore) DeleteResetToken(ctx context.Context, resetToken string) error {
key := "reset_token:" + resetToken
return s.client.Del(ctx, key).Err()
}
package redis
import (
oredis "github.com/go-oauth2/redis/v4"
"github.com/go-redis/redis/v8"
)
func NewTokenStore(client *redis.Client) *oredis.TokenStore {
return oredis.NewRedisStore(client.Options(), "token:")
}
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1001
package ui
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "github.com/iabhishekrajput/anekdote-auth/internal/models"
func AccountPage(csrfToken string, user *models.User, errorMsg string, successMsg string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = BaseLayout("Account Center - anekdote", AccountPageBody(csrfToken, user, errorMsg, successMsg)).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func AccountPageBody(csrfToken string, user *models.User, errorMsg string, successMsg string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil {
templ_7745c5c3_Var2 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"w-full max-w-2xl space-y-6 rounded-lg border border-zinc-800 bg-zinc-950/80 p-8 shadow\"><div class=\"flex flex-col items-center gap-1 text-center\"><h2 class=\"text-xl font-semibold tracking-tight\">Account Center</h2><p class=\"text-sm text-zinc-400\">Welcome back, ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(user.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/ui/account.templ`, Line: 13, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = AlertContainer(errorMsg, successMsg).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"space-y-6\"><section class=\"space-y-4 rounded-md border border-zinc-800 bg-transparent p-6\"><div class=\"space-y-1\"><h3 class=\"text-base font-semibold\">Profile Details</h3><p class=\"text-sm text-zinc-400\">Update your account information</p></div><form method=\"POST\" action=\"/account/profile\" class=\"space-y-3\"><input type=\"hidden\" name=\"csrf_token\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(csrfToken)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/ui/account.templ`, Line: 26, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\"><div class=\"flex flex-col gap-1.5\"><label for=\"name\" class=\"text-sm font-medium text-zinc-50\">Full Name</label> <input type=\"text\" id=\"name\" name=\"name\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(user.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/ui/account.templ`, Line: 33, Col: 24}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" required class=\"h-9 w-full rounded-md border border-zinc-800 bg-transparent px-3 text-sm text-zinc-50 placeholder:text-zinc-500 outline-none ring-0 transition focus:border-zinc-300 focus:ring-1 focus:ring-zinc-300\"></div><div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = OutlineButton("Update Profile").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div></form></section><section class=\"space-y-4 rounded-md border border-zinc-800 bg-transparent p-6\"><div class=\"space-y-1\"><h3 class=\"text-base font-semibold\">Change Password</h3><p class=\"text-sm text-zinc-400\">Update your security credentials</p></div><form method=\"POST\" action=\"/account/password\" class=\"space-y-3\"><input type=\"hidden\" name=\"csrf_token\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(csrfToken)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/ui/account.templ`, Line: 51, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\"><div class=\"flex flex-col gap-1.5\"><label for=\"old_password\" class=\"text-sm font-medium text-zinc-50\">Current Password</label> <input type=\"password\" id=\"old_password\" name=\"old_password\" placeholder=\"********\" required class=\"h-9 w-full rounded-md border border-zinc-800 bg-transparent px-3 text-sm text-zinc-50 placeholder:text-zinc-500 outline-none ring-0 transition focus:border-zinc-300 focus:ring-1 focus:ring-zinc-300\"></div><div class=\"flex flex-col gap-1.5\"><label for=\"new_password\" class=\"text-sm font-medium text-zinc-50\">New Password</label> <input type=\"password\" id=\"new_password\" name=\"new_password\" placeholder=\"********\" required class=\"h-9 w-full rounded-md border border-zinc-800 bg-transparent px-3 text-sm text-zinc-50 placeholder:text-zinc-500 outline-none ring-0 transition focus:border-zinc-300 focus:ring-1 focus:ring-zinc-300\"></div><div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = OutlineButton("Change Password").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div></form></section><div class=\"pt-2\"><form method=\"POST\" action=\"/logout\" class=\"w-full\"><input type=\"hidden\" name=\"csrf_token\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(csrfToken)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/ui/account.templ`, Line: 82, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\"> <button type=\"submit\" class=\"inline-flex h-9 w-full items-center justify-center rounded-md bg-red-500 px-3 text-sm font-medium text-zinc-50 transition hover:bg-red-600 active:scale-[0.98]\">Logout</button></form></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1001
package ui
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func Alert(kind string, message string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
if message != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"flex w-full items-center justify-between rounded-md border border-zinc-800 px-3 py-2 text-sm font-medium\"><div class=\"flex-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(message)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/ui/components.templ`, Line: 7, Col: 13}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div><button type=\"button\" class=\"ml-3 text-lg leading-none opacity-70 hover:opacity-100\" onclick=\"this.parentElement.remove()\">×</button></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
func AlertContainer(errMsg string, success string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
if templ_7745c5c3_Var3 == nil {
templ_7745c5c3_Var3 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
if errMsg != "" || success != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"mb-3 flex flex-col gap-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Alert("error", errMsg).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Alert("success", success).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
return nil
})
}
func PrimaryButton(text string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var4 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<button class=\"inline-flex h-9 w-full items-center justify-center rounded-md bg-zinc-50 px-3 text-sm font-medium text-zinc-900 transition hover:bg-zinc-200 active:scale-[0.98]\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/ui/components.templ`, Line: 33, Col: 8}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func OutlineButton(text string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var6 := templ.GetChildren(ctx)
if templ_7745c5c3_Var6 == nil {
templ_7745c5c3_Var6 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<button class=\"inline-flex h-9 w-full items-center justify-center rounded-md border border-zinc-800 bg-transparent px-3 text-sm font-medium text-zinc-50 transition hover:bg-zinc-900 active:scale-[0.98]\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/ui/components.templ`, Line: 41, Col: 8}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1001
package ui
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func ConsentPage(clientName string, scopes []string, csrfToken string, req string, errorMsg string, successMsg string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = BaseLayout("Authorize application - anekdote", ConsentPageBody(clientName, scopes, csrfToken, req, errorMsg, successMsg)).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func ConsentPageBody(clientName string, scopes []string, csrfToken string, req string, errorMsg string, successMsg string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil {
templ_7745c5c3_Var2 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"card w-full space-y-6 rounded-lg border border-zinc-800 bg-zinc-950/80 p-6 shadow\"><div class=\"flex flex-col items-center gap-2 text-center\"><div class=\"flex h-12 w-12 items-center justify-center rounded-full border border-zinc-800 bg-zinc-900/50 mb-2\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"text-zinc-400\"><path d=\"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10\"></path></svg></div><h2 class=\"text-xl font-semibold tracking-tight\">Authorize Access</h2><p class=\"text-sm text-zinc-400\"><span class=\"font-medium text-zinc-100\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(clientName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/ui/consent.templ`, Line: 15, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</span> is requesting access to your account.</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = AlertContainer(errorMsg, successMsg).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if len(scopes) > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"space-y-4 rounded-md border border-zinc-800/50 bg-zinc-900/30 p-4\"><p class=\"text-sm font-medium text-zinc-200\">This application will be able to:</p><ul class=\"space-y-2.5 text-sm text-zinc-400\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, scope := range scopes {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<li class=\"flex items-start gap-2.5\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"mt-0.5 text-zinc-400 shrink-0\"><path d=\"M20 6 9 17l-5-5\"></path></svg> <span class=\"leading-relaxed\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(scope)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/ui/consent.templ`, Line: 28, Col: 44}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</ul></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<form method=\"POST\" action=\"\" class=\"flex flex-col gap-3 pt-2\"><input type=\"hidden\" name=\"csrf_token\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(csrfToken)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/ui/consent.templ`, Line: 36, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if req != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<input type=\"hidden\" name=\"consent_challenge\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(req)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/ui/consent.templ`, Line: 38, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"flex flex-col gap-3\"><button type=\"submit\" name=\"accept\" value=\"true\" class=\"inline-flex h-9 w-full items-center justify-center rounded-md bg-zinc-50 px-3 text-sm font-medium text-zinc-900 transition hover:bg-zinc-200 active:scale-[0.98]\">Authorize</button> <button type=\"submit\" name=\"reject\" value=\"true\" class=\"inline-flex h-9 w-full items-center justify-center rounded-md border border-zinc-800 bg-transparent px-3 text-sm font-medium text-zinc-50 transition hover:bg-zinc-900 active:scale-[0.98]\">Cancel</button></div></form></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1001
package email
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func PasswordResetEmail(resetLink string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Password Reset - anekdote</title><style>\n\t\t\t\tbody {\n\t\t\t\t\tfont-family: 'Inter', -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n\t\t\t\t\tbackground-color: #09090b;\n\t\t\t\t\tcolor: #fafafa;\n\t\t\t\t\tmargin: 0;\n\t\t\t\t\tpadding: 0;\n\t\t\t\t\tline-height: 1.6;\n\t\t\t\t\t-webkit-font-smoothing: antialiased;\n\t\t\t\t}\n\t\t\t\t.container {\n\t\t\t\t\tmax-width: 600px;\n\t\t\t\t\tmargin: 40px auto;\n\t\t\t\t\tpadding: 20px;\n\t\t\t\t}\n\t\t\t\t.card {\n\t\t\t\t\tbackground-color: #09090b;\n\t\t\t\t\tborder: 1px solid #27272a;\n\t\t\t\t\tborder-radius: 8px;\n\t\t\t\t\tpadding: 32px;\n\t\t\t\t\ttext-align: center;\n\t\t\t\t\tbox-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);\n\t\t\t\t}\n\t\t\t\t.header {\n\t\t\t\t\tmargin-bottom: 24px;\n\t\t\t\t}\n\t\t\t\t.header h2 {\n\t\t\t\t\tmargin: 0;\n\t\t\t\t\tcolor: #fafafa;\n\t\t\t\t\tfont-size: 24px;\n\t\t\t\t\tfont-weight: 600;\n\t\t\t\t\tletter-spacing: -0.025em;\n\t\t\t\t}\n\t\t\t\t.content {\n\t\t\t\t\tfont-size: 16px;\n\t\t\t\t\tcolor: #a1a1aa;\n\t\t\t\t\tmargin-bottom: 32px;\n\t\t\t\t}\n\t\t\t\t.button {\n\t\t\t\t\tdisplay: inline-block;\n\t\t\t\t\tbackground-color: #fafafa;\n\t\t\t\t\tcolor: #18181b !important;\n\t\t\t\t\tfont-weight: 500;\n\t\t\t\t\ttext-decoration: none;\n\t\t\t\t\tpadding: 12px 24px;\n\t\t\t\t\tborder-radius: 6px;\n\t\t\t\t\tmargin-bottom: 24px;\n\t\t\t\t}\n\t\t\t\t.footer {\n\t\t\t\t\tfont-size: 12px;\n\t\t\t\t\tcolor: #a1a1aa;\n\t\t\t\t\tmargin-top: 32px;\n\t\t\t\t\tborder-top: 1px solid #27272a;\n\t\t\t\t\tpadding-top: 24px;\n\t\t\t\t}\n\t\t\t\ta {\n\t\t\t\t\tcolor: #fafafa;\n\t\t\t\t\ttext-decoration: underline;\n\t\t\t\t}\n\t\t\t\t.link-text {\n\t\t\t\t\tfont-size: 14px;\n\t\t\t\t\tword-wrap: break-word;\n\t\t\t\t\tcolor: #a1a1aa;\n\t\t\t\t}\n\t\t\t</style></head><body><div class=\"container\"><div class=\"card\"><div class=\"header\"><h2>Password Reset Request</h2></div><div class=\"content\"><p>Hello,</p><p>We received a request to reset your password for your <strong>anekdote</strong> account.</p><p>Click the button below to securely create a new password:</p></div><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 templ.SafeURL
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinURLErrs(resetLink)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/ui/email/reset_password.templ`, Line: 88, Col: 24}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" class=\"button\">Reset Password</a><div class=\"content\"><p>If the button doesn't work, copy and paste this link into your browser:</p><p class=\"link-text\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 templ.SafeURL
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(resetLink)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/ui/email/reset_password.templ`, Line: 93, Col: 26}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(resetLink)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/ui/email/reset_password.templ`, Line: 93, Col: 40}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</a></p></div><div class=\"footer\"><p>If you did not request this, please ignore this email. Your password will remain unchanged.</p><p>© 2026 anekdote. All rights reserved.</p></div></div></div></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1001
package email
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func VerifyEmailOTPEmail(otp string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Verify Your Email - anekdote</title><style>\n\t\t\t\tbody {\n\t\t\t\t\tfont-family: 'Inter', -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n\t\t\t\t\tbackground-color: #09090b;\n\t\t\t\t\tcolor: #fafafa;\n\t\t\t\t\tmargin: 0;\n\t\t\t\t\tpadding: 0;\n\t\t\t\t\tline-height: 1.6;\n\t\t\t\t\t-webkit-font-smoothing: antialiased;\n\t\t\t\t}\n\t\t\t\t.container {\n\t\t\t\t\tmax-width: 600px;\n\t\t\t\t\tmargin: 40px auto;\n\t\t\t\t\tpadding: 20px;\n\t\t\t\t}\n\t\t\t\t.card {\n\t\t\t\t\tbackground-color: #09090b;\n\t\t\t\t\tborder: 1px solid #27272a;\n\t\t\t\t\tborder-radius: 8px;\n\t\t\t\t\tpadding: 32px;\n\t\t\t\t\ttext-align: center;\n\t\t\t\t\tbox-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);\n\t\t\t\t}\n\t\t\t\t.header {\n\t\t\t\t\tmargin-bottom: 24px;\n\t\t\t\t}\n\t\t\t\t.header h2 {\n\t\t\t\t\tmargin: 0;\n\t\t\t\t\tcolor: #fafafa;\n\t\t\t\t\tfont-size: 24px;\n\t\t\t\t\tfont-weight: 600;\n\t\t\t\t\tletter-spacing: -0.025em;\n\t\t\t\t}\n\t\t\t\t.content {\n\t\t\t\t\tfont-size: 16px;\n\t\t\t\t\tcolor: #a1a1aa;\n\t\t\t\t\tmargin-bottom: 32px;\n\t\t\t\t}\n\t\t\t\t.otp-box {\n\t\t\t\t\tdisplay: inline-block;\n\t\t\t\t\tbackground-color: transparent;\n\t\t\t\t\tborder: 1px solid #27272a;\n\t\t\t\t\tcolor: #fafafa;\n\t\t\t\t\tfont-weight: bold;\n\t\t\t\t\tfont-size: 28px;\n\t\t\t\t\tletter-spacing: 4px;\n\t\t\t\t\tpadding: 16px 32px;\n\t\t\t\t\tborder-radius: 6px;\n\t\t\t\t\tmargin-bottom: 24px;\n\t\t\t\t}\n\t\t\t\t.footer {\n\t\t\t\t\tfont-size: 12px;\n\t\t\t\t\tcolor: #a1a1aa;\n\t\t\t\t\tmargin-top: 32px;\n\t\t\t\t\tborder-top: 1px solid #27272a;\n\t\t\t\t\tpadding-top: 24px;\n\t\t\t\t}\n\t\t\t</style></head><body><div class=\"container\"><div class=\"card\"><div class=\"header\"><h2>Verify Your Email</h2></div><div class=\"content\"><p>Hello,</p><p>Thank you for registering with <strong>anekdote</strong>! Please use the following 6-digit code to verify your email address and activate your account.</p><p>This code is valid for the next 15 minutes.</p></div><div class=\"otp-box\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(otp)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/ui/email/verify_email.templ`, Line: 85, Col: 11}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div><div class=\"footer\"><p>If you did not request this, please ignore this email. Your account will remain inactive.</p><p>© 2026 anekdote. All rights reserved.</p></div></div></div></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1001
package ui
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func ForgotPasswordPage(csrfToken string, errorMsg string, successMsg string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = BaseLayout("Forgot Password - anekdote", ForgotPasswordPageBody(csrfToken, errorMsg, successMsg)).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func ForgotPasswordPageBody(csrfToken string, errorMsg string, successMsg string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil {
templ_7745c5c3_Var2 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"w-full space-y-6 rounded-lg border border-zinc-800 bg-zinc-950/80 p-6 shadow\"><div class=\"flex flex-col items-center gap-2 text-center\"><h2 class=\"text-xl font-semibold tracking-tight\">Forgot Password</h2><p class=\"text-sm text-zinc-400\">Enter your email and we will send you a reset link</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = AlertContainer(errorMsg, successMsg).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<form method=\"POST\" action=\"/forgot-password\" class=\"flex flex-col gap-6\"><input type=\"hidden\" name=\"csrf_token\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(csrfToken)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/ui/forgot_password.templ`, Line: 19, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><div class=\"flex flex-col gap-1.5\"><label for=\"email\" class=\"text-sm font-medium text-zinc-50\">Email address</label> <input type=\"email\" id=\"email\" name=\"email\" placeholder=\"user@example.com\" required class=\"h-9 w-full rounded-md border border-zinc-800 bg-transparent px-3 text-sm text-zinc-50 placeholder:text-zinc-500 outline-none ring-0 transition focus:border-zinc-300 focus:ring-1 focus:ring-zinc-300\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = PrimaryButton("Send Reset Link").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</form><div class=\"text-center text-xs\"><a href=\"/login\" class=\"text-zinc-400 font-medium underline underline-offset-4\">Back to Login</a></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1001
package ui
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func BaseLayout(title string, body templ.Component) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/ui/layout.templ`, Line: 9, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><link rel=\"preconnect\" href=\"https://fonts.googleapis.com\"><link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin=\"anonymous\"><link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\"><link rel=\"stylesheet\" href=\"/static/app.css\"></head><body class=\"min-h-screen bg-zinc-950 text-zinc-50 flex items-center justify-center px-6 py-6\"><main class=\"w-full max-w-md md:max-w-lg\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = body.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</main></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1001
package ui
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func LoginPage(csrfToken string, req string, errorMsg string, successMsg string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = BaseLayout("Login - anekdote", LoginPageBody(csrfToken, req, errorMsg, successMsg)).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func LoginPageBody(csrfToken string, req string, errorMsg string, successMsg string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil {
templ_7745c5c3_Var2 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"card w-full space-y-6 rounded-lg border border-zinc-800 bg-zinc-950/80 p-6 shadow\"><div class=\"flex flex-col items-center gap-2 text-center\"><h2 class=\"text-xl font-semibold tracking-tight\">Login</h2><p class=\"text-sm text-zinc-400\">Enter your email below to login to your account</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = AlertContainer(errorMsg, successMsg).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<form method=\"POST\" action=\"/login\" class=\"flex flex-col gap-6\"><input type=\"hidden\" name=\"csrf_token\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(csrfToken)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/ui/login.templ`, Line: 17, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if req != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<input type=\"hidden\" name=\"req\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(req)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/ui/login.templ`, Line: 19, Col: 47}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"flex flex-col gap-4\"><div class=\"flex flex-col gap-1.5\"><label for=\"email\" class=\"text-sm font-medium text-zinc-50\">Email</label> <input type=\"email\" id=\"email\" name=\"email\" placeholder=\"user@example.com\" required class=\"h-9 w-full rounded-md border border-zinc-800 bg-transparent px-3 text-sm text-zinc-50 placeholder:text-zinc-500 outline-none ring-0 transition focus:border-zinc-300 focus:ring-1 focus:ring-zinc-300\"></div><div class=\"flex flex-col gap-1.5\"><div class=\"flex items-center text-sm\"><label for=\"password\" class=\"font-medium text-zinc-50\">Password</label> <a href=\"/forgot-password\" class=\"ml-auto text-xs font-medium text-zinc-200 underline underline-offset-4\">Forgot your password?</a></div><input type=\"password\" id=\"password\" name=\"password\" placeholder=\"********\" required class=\"h-9 w-full rounded-md border border-zinc-800 bg-transparent px-3 text-sm text-zinc-50 placeholder:text-zinc-500 outline-none ring-0 transition focus:border-zinc-300 focus:ring-1 focus:ring-zinc-300\"></div><div class=\"flex flex-col gap-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = PrimaryButton("Login").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div></div><div class=\"text-center text-xs text-zinc-400\"><span>Don't have an account?</span> <a href=\"/register\" class=\"ml-1 font-medium text-zinc-50 underline underline-offset-4\">Sign up</a></div></form></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1001
package ui
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func RegisterPage(csrfToken string, errorMsg string, successMsg string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = BaseLayout("Register - anekdote", RegisterPageBody(csrfToken, errorMsg, successMsg)).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func RegisterPageBody(csrfToken string, errorMsg string, successMsg string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil {
templ_7745c5c3_Var2 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"w-full space-y-6 rounded-lg border border-zinc-800 bg-zinc-950/80 p-6 shadow\"><div class=\"flex flex-col items-center gap-2 text-center\"><h2 class=\"text-xl font-semibold tracking-tight\">Create an account</h2><p class=\"text-sm text-zinc-400\">Enter your email below to create your account</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = AlertContainer(errorMsg, successMsg).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<form method=\"POST\" action=\"/register\" class=\"flex flex-col gap-6\"><input type=\"hidden\" name=\"csrf_token\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(csrfToken)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/ui/register.templ`, Line: 17, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><div class=\"flex flex-col gap-4\"><div class=\"flex flex-col gap-1.5\"><label for=\"name\" class=\"text-sm font-medium text-zinc-50\">Full Name</label> <input type=\"text\" id=\"name\" name=\"name\" placeholder=\"John Doe\" required class=\"h-9 w-full rounded-md border border-zinc-800 bg-transparent px-3 text-sm text-zinc-50 placeholder:text-zinc-500 outline-none ring-0 transition focus:border-zinc-300 focus:ring-1 focus:ring-zinc-300\"></div><div class=\"flex flex-col gap-1.5\"><label for=\"email\" class=\"text-sm font-medium text-zinc-50\">Email address</label> <input type=\"email\" id=\"email\" name=\"email\" placeholder=\"user@example.com\" required class=\"h-9 w-full rounded-md border border-zinc-800 bg-transparent px-3 text-sm text-zinc-50 placeholder:text-zinc-500 outline-none ring-0 transition focus:border-zinc-300 focus:ring-1 focus:ring-zinc-300\"></div><div class=\"flex flex-col gap-1.5\"><label for=\"password\" class=\"text-sm font-medium text-zinc-50\">Password</label> <input type=\"password\" id=\"password\" name=\"password\" placeholder=\"********\" required class=\"h-9 w-full rounded-md border border-zinc-800 bg-transparent px-3 text-sm text-zinc-50 placeholder:text-zinc-500 outline-none ring-0 transition focus:border-zinc-300 focus:ring-1 focus:ring-zinc-300\"></div></div><div class=\"flex flex-col gap-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = PrimaryButton("Sign Up").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div><div class=\"text-center text-xs text-zinc-400\"><span>Already have an account?</span> <a href=\"/login\" class=\"ml-1 font-medium text-zinc-50 underline underline-offset-4\">Log in</a></div></form></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1001
package ui
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func ResetPasswordPage(csrfToken string, token string, errorMsg string, successMsg string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = BaseLayout("Reset Password - anekdote", ResetPasswordPageBody(csrfToken, token, errorMsg, successMsg)).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func ResetPasswordPageBody(csrfToken string, token string, errorMsg string, successMsg string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil {
templ_7745c5c3_Var2 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"w-full space-y-6 rounded-lg border border-zinc-800 bg-zinc-950/80 p-6 shadow\"><div class=\"flex flex-col items-center gap-2 text-center\"><h2 class=\"text-xl font-semibold tracking-tight\">Reset Password</h2><p class=\"text-sm text-zinc-400\">Enter your new password below</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = AlertContainer(errorMsg, successMsg).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<form method=\"POST\" action=\"/reset-password\" class=\"flex flex-col gap-6\"><input type=\"hidden\" name=\"csrf_token\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(csrfToken)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/ui/reset_password.templ`, Line: 17, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"> <input type=\"hidden\" name=\"token\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(token)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/ui/reset_password.templ`, Line: 18, Col: 50}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\"><div class=\"flex flex-col gap-1.5\"><label for=\"password\" class=\"text-sm font-medium text-zinc-50\">New Password</label> <input type=\"password\" id=\"password\" name=\"password\" placeholder=\"********\" required minlength=\"6\" class=\"h-9 w-full rounded-md border border-zinc-800 bg-transparent px-3 text-sm text-zinc-50 placeholder:text-zinc-500 outline-none ring-0 transition focus:border-zinc-300 focus:ring-1 focus:ring-zinc-300\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = PrimaryButton("Update Password").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</form></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1001
package ui
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func VerifyEmailPage(csrfToken string, userID string, errorMsg string, successMsg string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = BaseLayout("Verify Email - anekdote", VerifyEmailPageBody(csrfToken, userID, errorMsg, successMsg)).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func VerifyEmailPageBody(csrfToken string, userID string, errorMsg string, successMsg string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil {
templ_7745c5c3_Var2 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"w-full max-w-sm mx-auto space-y-6 rounded-lg border border-zinc-800 bg-zinc-950/80 p-6 text-center shadow\"><div class=\"mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-zinc-900 text-zinc-50\"><span class=\"text-xl\">✉️</span></div><div class=\"space-y-1\"><h2 class=\"text-xl font-semibold tracking-tight\">Verify Email</h2><p class=\"text-sm text-zinc-400\">Enter the 6-digit code sent to your inbox.</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = AlertContainer(errorMsg, successMsg).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<form method=\"POST\" action=\"/verify-email\" class=\"space-y-4\"><input type=\"hidden\" name=\"csrf_token\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(csrfToken)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/ui/verify_email.templ`, Line: 20, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"> <input type=\"hidden\" name=\"user_id\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(userID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/ui/verify_email.templ`, Line: 21, Col: 53}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\"><div class=\"flex flex-col gap-2\"><div class=\"relative flex items-center\"><input type=\"text\" id=\"otp\" name=\"otp\" placeholder=\"6-digit OTP code\" required maxlength=\"6\" pattern=\"\\d{6}\" class=\"h-11 w-full rounded-md border border-zinc-800 bg-transparent px-3 pr-10 text-center text-lg tracking-[0.35em] text-zinc-50 placeholder:text-zinc-500 outline-none ring-0 transition focus:border-zinc-300 focus:ring-1 focus:ring-zinc-300\"></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = PrimaryButton("Verify & Login").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</form></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate