📝 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"
|
"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() {
|
func main() {
|
||||||
cfg := config.Load()
|
cfg := config.Load()
|
||||||
middleware.Init()
|
middleware.Init()
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ type Config struct {
|
|||||||
|
|
||||||
var Current *Config
|
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 {
|
func Load() *Config {
|
||||||
if err := godotenv.Load(); err != nil {
|
if err := godotenv.Load(); err != nil {
|
||||||
log.Println("No .env file found, using environment variables")
|
log.Println("No .env file found, using environment variables")
|
||||||
@@ -59,6 +62,8 @@ func Load() *Config {
|
|||||||
return cfg
|
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 {
|
func getEnv(key, defaultValue string) string {
|
||||||
value := os.Getenv(key)
|
value := os.Getenv(key)
|
||||||
if value == "" {
|
if value == "" {
|
||||||
@@ -67,6 +72,9 @@ func getEnv(key, defaultValue string) string {
|
|||||||
return value
|
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 {
|
func getEnvInt(key string, defaultValue int) int {
|
||||||
value := os.Getenv(key)
|
value := os.Getenv(key)
|
||||||
if value == "" {
|
if value == "" {
|
||||||
|
|||||||
@@ -19,6 +19,13 @@ type Admin struct {
|
|||||||
Password string
|
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) {
|
func Init(cfg *config.Config) {
|
||||||
password := url.QueryEscape(cfg.PGPassword)
|
password := url.QueryEscape(cfg.PGPassword)
|
||||||
|
|
||||||
@@ -47,12 +54,17 @@ func Init(cfg *config.Config) {
|
|||||||
createDefaultAdmin(cfg)
|
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() {
|
func Close() {
|
||||||
if db != nil {
|
if db != nil {
|
||||||
db.Close()
|
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() {
|
func createTables() {
|
||||||
queries := []string{
|
queries := []string{
|
||||||
`CREATE TABLE IF NOT EXISTS subscribers (
|
`CREATE TABLE IF NOT EXISTS subscribers (
|
||||||
@@ -81,6 +93,8 @@ func createTables() {
|
|||||||
log.Println("Database tables ready.")
|
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) {
|
func GetAllEmails() ([]string, error) {
|
||||||
rows, err := db.Query("SELECT email FROM subscribers")
|
rows, err := db.Query("SELECT email FROM subscribers")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -101,6 +115,8 @@ func GetAllEmails() ([]string, error) {
|
|||||||
return emails, rows.Err()
|
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) {
|
func GetAdmin(username string) (*Admin, error) {
|
||||||
var admin Admin
|
var admin Admin
|
||||||
err := db.QueryRow(
|
err := db.QueryRow(
|
||||||
@@ -118,6 +134,10 @@ func GetAdmin(username string) (*Admin, error) {
|
|||||||
return &admin, nil
|
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) {
|
func createDefaultAdmin(cfg *config.Config) {
|
||||||
hashedPassword, err := hashPassword(cfg.AdminPassword)
|
hashedPassword, err := hashPassword(cfg.AdminPassword)
|
||||||
if err != nil {
|
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 {
|
func LogNewsletter(subject, body string) error {
|
||||||
_, err := db.Exec(
|
_, err := db.Exec(
|
||||||
"INSERT INTO newsletters (subject, body) VALUES ($1, $2)",
|
"INSERT INTO newsletters (subject, body) VALUES ($1, $2)",
|
||||||
@@ -144,6 +166,8 @@ func LogNewsletter(subject, body string) error {
|
|||||||
return err
|
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) {
|
func hashPassword(password string) (string, error) {
|
||||||
hash, err := bcrypt.GenerateFromPassword(
|
hash, err := bcrypt.GenerateFromPassword(
|
||||||
[]byte(password),
|
[]byte(password),
|
||||||
@@ -152,6 +176,8 @@ func hashPassword(password string) (string, error) {
|
|||||||
return string(hash), err
|
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 {
|
func VerifyPassword(hash, password string) bool {
|
||||||
return bcrypt.CompareHashAndPassword(
|
return bcrypt.CompareHashAndPassword(
|
||||||
[]byte(hash),
|
[]byte(hash),
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ import (
|
|||||||
"github.com/wneessen/go-mail"
|
"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) {
|
func SendUpdate(subject, body string) (string, error) {
|
||||||
subscribers, err := database.GetAllEmails()
|
subscribers, err := database.GetAllEmails()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -34,6 +41,8 @@ func SendUpdate(subject, body string) (string, error) {
|
|||||||
return "Email has been sent to all subscribers.", nil
|
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 {
|
func send(subject, body, recipient string) bool {
|
||||||
cfg := config.Current
|
cfg := config.Current
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,13 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// LoginGet renders the login page using the "login.html" template with HTTP 200 status.
|
||||||
func LoginGet(c *gin.Context) {
|
func LoginGet(c *gin.Context) {
|
||||||
c.HTML(http.StatusOK, "login.html", gin.H{})
|
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) {
|
func LoginPost(c *gin.Context) {
|
||||||
username := c.PostForm("username")
|
username := c.PostForm("username")
|
||||||
password := c.PostForm("password")
|
password := c.PostForm("password")
|
||||||
@@ -39,6 +42,8 @@ func LoginPost(c *gin.Context) {
|
|||||||
c.Redirect(http.StatusFound, "/")
|
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) {
|
func Logout(c *gin.Context) {
|
||||||
session, err := middleware.GetStore().Get(c.Request, "session")
|
session, err := middleware.GetStore().Get(c.Request, "session")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@@ -8,10 +8,15 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"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) {
|
func SendUpdateGet(c *gin.Context) {
|
||||||
c.HTML(http.StatusOK, "send_update.html", gin.H{})
|
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) {
|
func SendUpdatePost(c *gin.Context) {
|
||||||
subject := c.PostForm("subject")
|
subject := c.PostForm("subject")
|
||||||
body := c.PostForm("body")
|
body := c.PostForm("body")
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"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) {
|
func IndexGet(c *gin.Context) {
|
||||||
emails, err := database.GetAllEmails()
|
emails, err := database.GetAllEmails()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import (
|
|||||||
|
|
||||||
var store *sessions.CookieStore
|
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() {
|
func Init() {
|
||||||
if config.Current.SecretKey == "" {
|
if config.Current.SecretKey == "" {
|
||||||
panic("SECRET_KEY not set")
|
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 {
|
func GetStore() *sessions.CookieStore {
|
||||||
return store
|
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 {
|
func Auth() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
session, err := store.Get(c.Request, "session")
|
session, err := store.Get(c.Request, "session")
|
||||||
|
|||||||
Reference in New Issue
Block a user