📝 Add docstrings to feat/go-rewrite
Docstrings generation was requested by @blakeridgway. * https://github.com/RideAware/admin-panel/pull/1#issuecomment-3528008426 The following files were modified: * `cmd/admin-panel/main.go` * `internal/config/config.go` * `internal/database/database.go` * `internal/email/email.go` * `internal/handlers/auth.go` * `internal/handlers/newsletter.go` * `internal/handlers/subscribers.go` * `internal/middleware/auth.go`
This commit is contained in:
committed by
GitHub
parent
899313dcac
commit
2adb7e3605
@@ -11,6 +11,10 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// main is the program entry point for the admin panel. It loads configuration,
|
||||
// initializes middleware and the database (closed on exit), configures a Gin
|
||||
// router with HTML templates and static assets, registers public and
|
||||
// authenticated routes, and starts the HTTP server on the configured port.
|
||||
func main() {
|
||||
cfg := config.Load()
|
||||
middleware.Init()
|
||||
|
||||
@@ -28,6 +28,9 @@ type Config struct {
|
||||
|
||||
var Current *Config
|
||||
|
||||
// Load loads configuration from environment variables or a .env file and initializes the package-level Current configuration.
|
||||
// It constructs a Config with sensible defaults for server, PostgreSQL, SMTP, admin credentials, secret key, and base URL.
|
||||
// If SENDER_EMAIL is not set, it falls back to SMTP_USER. The created Config is assigned to Current and returned.
|
||||
func Load() *Config {
|
||||
if err := godotenv.Load(); err != nil {
|
||||
log.Println("No .env file found, using environment variables")
|
||||
@@ -59,6 +62,8 @@ func Load() *Config {
|
||||
return cfg
|
||||
}
|
||||
|
||||
// getEnv returns the value of the environment variable named by key, or defaultValue if that variable is not set or is empty.
|
||||
// If the environment variable exists but is the empty string, defaultValue is returned.
|
||||
func getEnv(key, defaultValue string) string {
|
||||
value := os.Getenv(key)
|
||||
if value == "" {
|
||||
@@ -67,6 +72,9 @@ func getEnv(key, defaultValue string) string {
|
||||
return value
|
||||
}
|
||||
|
||||
// getEnvInt retrieves the environment variable named by key and returns its integer value or defaultValue.
|
||||
// If the variable is not set, it returns defaultValue. If the variable is set but cannot be parsed as an integer,
|
||||
// it logs the parse error and returns defaultValue.
|
||||
func getEnvInt(key string, defaultValue int) int {
|
||||
value := os.Getenv(key)
|
||||
if value == "" {
|
||||
|
||||
@@ -19,6 +19,13 @@ type Admin struct {
|
||||
Password string
|
||||
}
|
||||
|
||||
// Init initializes the package database connection using values from cfg, sets connection pool limits,
|
||||
// creates required tables, and ensures a default admin user exists.
|
||||
// It assigns the opened *sql.DB to the package-level db and will terminate the program if establishing
|
||||
// or verifying the connection fails.
|
||||
//
|
||||
// cfg provides PostgreSQL connection parameters and the default admin credentials used to create the
|
||||
// default admin user when missing.
|
||||
func Init(cfg *config.Config) {
|
||||
password := url.QueryEscape(cfg.PGPassword)
|
||||
|
||||
@@ -47,12 +54,17 @@ func Init(cfg *config.Config) {
|
||||
createDefaultAdmin(cfg)
|
||||
}
|
||||
|
||||
// Close closes the package-level database connection if it has been initialized.
|
||||
// It is safe to call multiple times; if no connection exists, the call is a no-op.
|
||||
func Close() {
|
||||
if db != nil {
|
||||
db.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// createTables creates the required database tables if they do not already exist.
|
||||
// It ensures the subscribers, admin_users, and newsletters tables are present; errors
|
||||
// encountered while creating individual tables are logged but do not abort the process.
|
||||
func createTables() {
|
||||
queries := []string{
|
||||
`CREATE TABLE IF NOT EXISTS subscribers (
|
||||
@@ -81,6 +93,8 @@ func createTables() {
|
||||
log.Println("Database tables ready.")
|
||||
}
|
||||
|
||||
// GetAllEmails retrieves all subscriber email addresses from the database.
|
||||
// It returns a slice of email strings and any error encountered while querying or scanning rows.
|
||||
func GetAllEmails() ([]string, error) {
|
||||
rows, err := db.Query("SELECT email FROM subscribers")
|
||||
if err != nil {
|
||||
@@ -101,6 +115,8 @@ func GetAllEmails() ([]string, error) {
|
||||
return emails, rows.Err()
|
||||
}
|
||||
|
||||
// GetAdmin retrieves the admin user with the given username.
|
||||
// It returns a pointer to the Admin when a matching row exists. If no admin is found, it returns an error "admin not found"; other database errors are returned unchanged.
|
||||
func GetAdmin(username string) (*Admin, error) {
|
||||
var admin Admin
|
||||
err := db.QueryRow(
|
||||
@@ -118,6 +134,10 @@ func GetAdmin(username string) (*Admin, error) {
|
||||
return &admin, nil
|
||||
}
|
||||
|
||||
// createDefaultAdmin ensures a default admin user exists by inserting cfg.AdminUsername
|
||||
// with a bcrypt-hashed cfg.AdminPassword into the admin_users table; the insert is
|
||||
// idempotent (no-op if the username already exists). If password hashing fails the
|
||||
// function terminates the process; insertion errors are logged.
|
||||
func createDefaultAdmin(cfg *config.Config) {
|
||||
hashedPassword, err := hashPassword(cfg.AdminPassword)
|
||||
if err != nil {
|
||||
@@ -136,6 +156,8 @@ func createDefaultAdmin(cfg *config.Config) {
|
||||
}
|
||||
}
|
||||
|
||||
// LogNewsletter inserts a newsletter record with the provided subject and body into the newsletters table.
|
||||
// It returns any error encountered while inserting the record.
|
||||
func LogNewsletter(subject, body string) error {
|
||||
_, err := db.Exec(
|
||||
"INSERT INTO newsletters (subject, body) VALUES ($1, $2)",
|
||||
@@ -144,6 +166,8 @@ func LogNewsletter(subject, body string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// hashPassword generates a bcrypt hash for the given plaintext password.
|
||||
// It uses bcrypt.DefaultCost and returns the hashed password as a string and any error encountered.
|
||||
func hashPassword(password string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword(
|
||||
[]byte(password),
|
||||
@@ -152,6 +176,8 @@ func hashPassword(password string) (string, error) {
|
||||
return string(hash), err
|
||||
}
|
||||
|
||||
// VerifyPassword reports whether the provided plaintext password matches the given bcrypt hash.
|
||||
// It returns true if the password matches, false otherwise.
|
||||
func VerifyPassword(hash, password string) bool {
|
||||
return bcrypt.CompareHashAndPassword(
|
||||
[]byte(hash),
|
||||
|
||||
@@ -11,6 +11,13 @@ import (
|
||||
"github.com/wneessen/go-mail"
|
||||
)
|
||||
|
||||
// SendUpdate sends a newsletter with the given subject and body to all subscriber emails stored in the database.
|
||||
// It returns a human-readable status message and, when subscriber retrieval fails, the underlying error.
|
||||
// - If retrieving subscribers fails: returns "Failed to retrieve subscribers" and the error.
|
||||
// - If no subscribers are found: returns "No subscribers found." and nil.
|
||||
// - If sending to a specific subscriber fails: returns "Failed to send to <email>" and nil.
|
||||
// - On success: returns "Email has been sent to all subscribers." and nil.
|
||||
// Note: logging the newsletter entry in the database is attempted after sending and any logging failure is non-fatal.
|
||||
func SendUpdate(subject, body string) (string, error) {
|
||||
subscribers, err := database.GetAllEmails()
|
||||
if err != nil {
|
||||
@@ -34,6 +41,8 @@ func SendUpdate(subject, body string) (string, error) {
|
||||
return "Email has been sent to all subscribers.", nil
|
||||
}
|
||||
|
||||
// send constructs and sends an HTML newsletter update to the specified recipient using the current SMTP configuration.
|
||||
// It embeds an unsubscribe link for the recipient and returns true if the message was sent successfully, false if client creation, message setup, or sending fails.
|
||||
func send(subject, body, recipient string) bool {
|
||||
cfg := config.Current
|
||||
|
||||
|
||||
@@ -9,10 +9,13 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// LoginGet renders the login page using the "login.html" template with HTTP 200 status.
|
||||
func LoginGet(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "login.html", gin.H{})
|
||||
}
|
||||
|
||||
// LoginPost handles POST /login form submissions, authenticates the user, creates a session, and redirects to "/" on success.
|
||||
// On invalid credentials it renders the login page with HTTP 401 and an error message; if session retrieval or saving fails it aborts with HTTP 500.
|
||||
func LoginPost(c *gin.Context) {
|
||||
username := c.PostForm("username")
|
||||
password := c.PostForm("password")
|
||||
@@ -39,6 +42,8 @@ func LoginPost(c *gin.Context) {
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
|
||||
// Logout invalidates the current user session if one exists and redirects the client to the login page.
|
||||
// If the session cannot be retrieved, the handler still redirects to "/login".
|
||||
func Logout(c *gin.Context) {
|
||||
session, err := middleware.GetStore().Get(c.Request, "session")
|
||||
if err == nil {
|
||||
|
||||
@@ -8,10 +8,15 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// SendUpdateGet renders the update form page using the "send_update.html" template and responds with HTTP 200 OK.
|
||||
func SendUpdateGet(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "send_update.html", gin.H{})
|
||||
}
|
||||
|
||||
// SendUpdatePost handles POST requests to submit a newsletter update.
|
||||
// It reads "subject" and "body" from the form, calls email.SendUpdate(subject, body),
|
||||
// and renders the "send_update.html" template with gin.H{"error": message} when sending fails
|
||||
// or gin.H{"success": message} when sending succeeds, returning HTTP 200 in both cases.
|
||||
func SendUpdatePost(c *gin.Context) {
|
||||
subject := c.PostForm("subject")
|
||||
body := c.PostForm("body")
|
||||
|
||||
@@ -8,6 +8,9 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// IndexGet handles requests for the admin index page by retrieving all subscriber emails
|
||||
// and rendering the "admin_index.html" template with those emails.
|
||||
// If retrieving emails fails, it aborts the request with HTTP 500 and the error.
|
||||
func IndexGet(c *gin.Context) {
|
||||
emails, err := database.GetAllEmails()
|
||||
if err != nil {
|
||||
|
||||
@@ -11,6 +11,9 @@ import (
|
||||
|
||||
var store *sessions.CookieStore
|
||||
|
||||
// Init initializes the package-level cookie store used for session management.
|
||||
// It panics if config.Current.SecretKey is empty.
|
||||
// The created store is configured with Path "/", MaxAge one week, HttpOnly true, Secure false, and SameSite 0.
|
||||
func Init() {
|
||||
if config.Current.SecretKey == "" {
|
||||
panic("SECRET_KEY not set")
|
||||
@@ -25,10 +28,16 @@ func Init() {
|
||||
}
|
||||
}
|
||||
|
||||
// GetStore returns the package-level Gorilla cookie store used for session management.
|
||||
// It may be nil if Init has not been called.
|
||||
func GetStore() *sessions.CookieStore {
|
||||
return store
|
||||
}
|
||||
|
||||
// Auth enforces session-based authentication for Gin handlers.
|
||||
// If the request has no session named "session" or the session lacks a "username" value,
|
||||
// the middleware redirects to "/login" (HTTP 302) and aborts further handling.
|
||||
// Otherwise the middleware calls the next handler in the chain.
|
||||
func Auth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
session, err := store.Get(c.Request, "session")
|
||||
|
||||
Reference in New Issue
Block a user