685 lines
19 KiB
Go
685 lines
19 KiB
Go
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
_ "github.com/go-sql-driver/mysql"
|
|
"github.com/google/uuid"
|
|
"github.com/gorilla/mux"
|
|
"github.com/gorilla/sessions"
|
|
"github.com/joho/godotenv"
|
|
markdown "github.com/markdownit/markdown-it-go"
|
|
)
|
|
|
|
var (
|
|
db *sql.DB
|
|
sessionStore *sessions.CookieStore
|
|
markdownParser *markdown.MarkdownIt
|
|
)
|
|
|
|
// Models
|
|
type User struct {
|
|
ID string `json:"id"`
|
|
Username string `json:"username"`
|
|
Email string `json:"email"`
|
|
PasswordHash string `json:"password_hash"`
|
|
Role string `json:"role"` // admin, author, reader
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
type Post struct {
|
|
ID string `json:"id"`
|
|
AuthorID string `json:"author_id"`
|
|
Title string `json:"title"`
|
|
Slug string `json:"slug"`
|
|
Content string `json:"content"`
|
|
Excerpt string `json:"excerpt"`
|
|
Type string `json:"type"` // post, micro
|
|
Category string `json:"category"`
|
|
Tags []string `json:"tags"`
|
|
Published bool `json:"published"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
type Comment struct {
|
|
ID string `json:"id"`
|
|
PostID string `json:"post_id"`
|
|
AuthorID string `json:"author_id"`
|
|
Content string `json:"content"`
|
|
Approved bool `json:"approved"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
type APIResponse struct {
|
|
Success bool `json:"success"`
|
|
Message string `json:"message"`
|
|
Data interface{} `json:"data,omitempty"`
|
|
}
|
|
|
|
func init() {
|
|
// Load environment variables
|
|
godotenv.Load()
|
|
|
|
// Initialize markdown parser
|
|
markdownParser = markdown.New()
|
|
|
|
// Initialize session store
|
|
sessionStore = sessions.NewCookieStore([]byte(getEnv("SESSION_KEY", "dev-session-key")))
|
|
}
|
|
|
|
func main() {
|
|
// Database connection
|
|
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true",
|
|
getEnv("DB_USER", "root"),
|
|
getEnv("DB_PASSWORD", "password"),
|
|
getEnv("DB_HOST", "mariadb"),
|
|
getEnv("DB_PORT", "3306"),
|
|
getEnv("DB_NAME", "blogging_cms"),
|
|
)
|
|
|
|
var err error
|
|
db, err = sql.Open("mysql", dsn)
|
|
if err != nil {
|
|
log.Fatalf("Database connection failed: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
if err := db.Ping(); err != nil {
|
|
log.Fatalf("Database ping failed: %v", err)
|
|
}
|
|
|
|
log.Println("Database connected successfully")
|
|
|
|
// Initialize database schema
|
|
if err := initDB(); err != nil {
|
|
log.Fatalf("Failed to initialize database: %v", err)
|
|
}
|
|
|
|
// Router setup
|
|
router := mux.NewRouter()
|
|
|
|
// Static files
|
|
router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
|
|
|
|
// Public routes
|
|
router.HandleFunc("/", handleHome).Methods("GET")
|
|
router.HandleFunc("/post/{slug}", handleViewPost).Methods("GET")
|
|
router.HandleFunc("/category/{category}", handleCategory).Methods("GET")
|
|
router.HandleFunc("/feed", handleRSSFeed).Methods("GET")
|
|
router.HandleFunc("/api/posts", handleGetPosts).Methods("GET")
|
|
router.HandleFunc("/api/posts/{id}", handleGetPost).Methods("GET")
|
|
router.HandleFunc("/api/search", handleSearch).Methods("GET")
|
|
|
|
// Auth routes
|
|
router.HandleFunc("/auth/register", handleRegister).Methods("POST")
|
|
router.HandleFunc("/auth/login", handleLogin).Methods("POST")
|
|
router.HandleFunc("/auth/logout", handleLogout).Methods("POST")
|
|
|
|
// Protected routes
|
|
router.HandleFunc("/dashboard", handleDashboard).Methods("GET")
|
|
router.HandleFunc("/api/posts", handleCreatePost).Methods("POST")
|
|
router.HandleFunc("/api/posts/{id}", handleUpdatePost).Methods("PUT")
|
|
router.HandleFunc("/api/posts/{id}", handleDeletePost).Methods("DELETE")
|
|
router.HandleFunc("/api/comments", handleCreateComment).Methods("POST")
|
|
|
|
// Middleware
|
|
router.Use(loggingMiddleware)
|
|
|
|
port := getEnv("PORT", "8080")
|
|
log.Printf("Server starting on port %s", port)
|
|
log.Fatal(http.ListenAndServe(":"+port, router))
|
|
}
|
|
|
|
func initDB() error {
|
|
schema := `
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
username VARCHAR(100) UNIQUE NOT NULL,
|
|
email VARCHAR(255) UNIQUE NOT NULL,
|
|
password_hash VARCHAR(255) NOT NULL,
|
|
role VARCHAR(50) DEFAULT 'author',
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS posts (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
author_id VARCHAR(36) NOT NULL,
|
|
title VARCHAR(255) NOT NULL,
|
|
slug VARCHAR(255) UNIQUE NOT NULL,
|
|
content LONGTEXT NOT NULL,
|
|
excerpt VARCHAR(500),
|
|
type VARCHAR(50) DEFAULT 'post',
|
|
category VARCHAR(100),
|
|
published BOOLEAN DEFAULT FALSE,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (author_id) REFERENCES users(id),
|
|
INDEX (slug),
|
|
INDEX (published),
|
|
INDEX (created_at),
|
|
INDEX (category)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS tags (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
post_id VARCHAR(36) NOT NULL,
|
|
tag VARCHAR(100),
|
|
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS comments (
|
|
id VARCHAR(36) PRIMARY KEY,
|
|
post_id VARCHAR(36) NOT NULL,
|
|
author_id VARCHAR(36),
|
|
content TEXT NOT NULL,
|
|
approved BOOLEAN DEFAULT FALSE,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE
|
|
);
|
|
`
|
|
|
|
for _, statement := range strings.Split(schema, ";") {
|
|
statement = strings.TrimSpace(statement)
|
|
if statement != "" {
|
|
if _, err := db.Exec(statement); err != nil {
|
|
return fmt.Errorf("failed to create table: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Handlers
|
|
func handleHome(w http.ResponseWriter, r *http.Request) {
|
|
rows, err := db.Query(`
|
|
SELECT id, author_id, title, slug, excerpt, type, created_at
|
|
FROM posts
|
|
WHERE published = TRUE
|
|
ORDER BY created_at DESC
|
|
LIMIT 20
|
|
`)
|
|
if err != nil {
|
|
http.Error(w, "Database error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var posts []Post
|
|
for rows.Next() {
|
|
var post Post
|
|
rows.Scan(&post.ID, &post.AuthorID, &post.Title, &post.Slug, &post.Excerpt, &post.Type, &post.CreatedAt)
|
|
posts = append(posts, post)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
renderTemplate(w, "home.html", map[string]interface{}{
|
|
"posts": posts,
|
|
})
|
|
}
|
|
|
|
func handleViewPost(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
slug := vars["slug"]
|
|
|
|
var post Post
|
|
var authorUsername string
|
|
var commentCount int
|
|
|
|
err := db.QueryRow(`
|
|
SELECT p.id, p.author_id, p.title, p.slug, p.content, p.excerpt, p.type, p.category, p.created_at, p.updated_at, u.username
|
|
FROM posts p
|
|
JOIN users u ON p.author_id = u.id
|
|
WHERE p.slug = ? AND p.published = TRUE
|
|
`, slug).Scan(&post.ID, &post.AuthorID, &post.Title, &post.Slug, &post.Content, &post.Excerpt, &post.Type, &post.Category, &post.CreatedAt, &post.UpdatedAt, &authorUsername)
|
|
|
|
if err != nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
// Render markdown
|
|
post.Content = markdownParser.Render(post.Content)
|
|
|
|
// Get tags
|
|
tagRows, _ := db.Query("SELECT tag FROM tags WHERE post_id = ?", post.ID)
|
|
defer tagRows.Close()
|
|
for tagRows.Next() {
|
|
var tag string
|
|
tagRows.Scan(&tag)
|
|
post.Tags = append(post.Tags, tag)
|
|
}
|
|
|
|
// Get comments
|
|
commentRows, _ := db.Query("SELECT COUNT(*) FROM comments WHERE post_id = ? AND approved = TRUE", post.ID)
|
|
commentRows.Next()
|
|
commentRows.Scan(&commentCount)
|
|
commentRows.Close()
|
|
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
renderTemplate(w, "post.html", map[string]interface{}{
|
|
"post": post,
|
|
"author": authorUsername,
|
|
"commentCount": commentCount,
|
|
})
|
|
}
|
|
|
|
func handleCategory(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
category := vars["category"]
|
|
|
|
rows, err := db.Query(`
|
|
SELECT id, author_id, title, slug, excerpt, type, created_at
|
|
FROM posts
|
|
WHERE published = TRUE AND category = ?
|
|
ORDER BY created_at DESC
|
|
`, category)
|
|
if err != nil {
|
|
http.Error(w, "Database error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var posts []Post
|
|
for rows.Next() {
|
|
var post Post
|
|
rows.Scan(&post.ID, &post.AuthorID, &post.Title, &post.Slug, &post.Excerpt, &post.Type, &post.CreatedAt)
|
|
posts = append(posts, post)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
renderTemplate(w, "category.html", map[string]interface{}{
|
|
"category": category,
|
|
"posts": posts,
|
|
})
|
|
}
|
|
|
|
func handleGetPosts(w http.ResponseWriter, r *http.Request) {
|
|
pageStr := r.URL.Query().Get("page")
|
|
page := 1
|
|
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
|
|
page = p
|
|
}
|
|
|
|
limit := 20
|
|
offset := (page - 1) * limit
|
|
|
|
rows, err := db.Query(`
|
|
SELECT id, author_id, title, slug, excerpt, type, category, created_at
|
|
FROM posts
|
|
WHERE published = TRUE
|
|
ORDER BY created_at DESC
|
|
LIMIT ? OFFSET ?
|
|
`, limit, offset)
|
|
if err != nil {
|
|
respondJSON(w, http.StatusInternalServerError, APIResponse{Success: false, Message: "Database error"})
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var posts []Post
|
|
for rows.Next() {
|
|
var post Post
|
|
rows.Scan(&post.ID, &post.AuthorID, &post.Title, &post.Slug, &post.Excerpt, &post.Type, &post.Category, &post.CreatedAt)
|
|
posts = append(posts, post)
|
|
}
|
|
|
|
respondJSON(w, http.StatusOK, APIResponse{Success: true, Data: posts})
|
|
}
|
|
|
|
func handleGetPost(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
postID := vars["id"]
|
|
|
|
var post Post
|
|
err := db.QueryRow(`
|
|
SELECT id, author_id, title, slug, content, excerpt, type, category, published, created_at, updated_at
|
|
FROM posts
|
|
WHERE id = ?
|
|
`, postID).Scan(&post.ID, &post.AuthorID, &post.Title, &post.Slug, &post.Content, &post.Excerpt, &post.Type, &post.Category, &post.Published, &post.CreatedAt, &post.UpdatedAt)
|
|
|
|
if err != nil {
|
|
respondJSON(w, http.StatusNotFound, APIResponse{Success: false, Message: "Post not found"})
|
|
return
|
|
}
|
|
|
|
respondJSON(w, http.StatusOK, APIResponse{Success: true, Data: post})
|
|
}
|
|
|
|
func handleCreatePost(w http.ResponseWriter, r *http.Request) {
|
|
userID, ok := r.Context().Value("userID").(string)
|
|
if !ok {
|
|
respondJSON(w, http.StatusUnauthorized, APIResponse{Success: false, Message: "Unauthorized"})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Title string `json:"title"`
|
|
Content string `json:"content"`
|
|
Excerpt string `json:"excerpt"`
|
|
Type string `json:"type"`
|
|
Category string `json:"category"`
|
|
Tags []string `json:"tags"`
|
|
Published bool `json:"published"`
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
respondJSON(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid request"})
|
|
return
|
|
}
|
|
|
|
postID := uuid.New().String()
|
|
slug := slugify(req.Title)
|
|
|
|
_, err := db.Exec(`
|
|
INSERT INTO posts (id, author_id, title, slug, content, excerpt, type, category, published)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`, postID, userID, req.Title, slug, req.Content, req.Excerpt, req.Type, req.Category, req.Published)
|
|
|
|
if err != nil {
|
|
respondJSON(w, http.StatusInternalServerError, APIResponse{Success: false, Message: "Failed to create post"})
|
|
return
|
|
}
|
|
|
|
// Save tags
|
|
for _, tag := range req.Tags {
|
|
db.Exec("INSERT INTO tags (id, post_id, tag) VALUES (?, ?, ?)", uuid.New().String(), postID, tag)
|
|
}
|
|
|
|
respondJSON(w, http.StatusCreated, APIResponse{Success: true, Message: "Post created", Data: map[string]string{"id": postID}})
|
|
}
|
|
|
|
func handleUpdatePost(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
postID := vars["id"]
|
|
|
|
userID, ok := r.Context().Value("userID").(string)
|
|
if !ok {
|
|
respondJSON(w, http.StatusUnauthorized, APIResponse{Success: false, Message: "Unauthorized"})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Title string `json:"title"`
|
|
Content string `json:"content"`
|
|
Excerpt string `json:"excerpt"`
|
|
Type string `json:"type"`
|
|
Category string `json:"category"`
|
|
Tags []string `json:"tags"`
|
|
Published bool `json:"published"`
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
respondJSON(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid request"})
|
|
return
|
|
}
|
|
|
|
// Verify ownership
|
|
var authorID string
|
|
db.QueryRow("SELECT author_id FROM posts WHERE id = ?", postID).Scan(&authorID)
|
|
if authorID != userID {
|
|
respondJSON(w, http.StatusForbidden, APIResponse{Success: false, Message: "Forbidden"})
|
|
return
|
|
}
|
|
|
|
slug := slugify(req.Title)
|
|
|
|
_, err := db.Exec(`
|
|
UPDATE posts
|
|
SET title = ?, slug = ?, content = ?, excerpt = ?, type = ?, category = ?, published = ?
|
|
WHERE id = ?
|
|
`, req.Title, slug, req.Content, req.Excerpt, req.Type, req.Category, req.Published, postID)
|
|
|
|
if err != nil {
|
|
respondJSON(w, http.StatusInternalServerError, APIResponse{Success: false, Message: "Failed to update post"})
|
|
return
|
|
}
|
|
|
|
// Update tags
|
|
db.Exec("DELETE FROM tags WHERE post_id = ?", postID)
|
|
for _, tag := range req.Tags {
|
|
db.Exec("INSERT INTO tags (id, post_id, tag) VALUES (?, ?, ?)", uuid.New().String(), postID, tag)
|
|
}
|
|
|
|
respondJSON(w, http.StatusOK, APIResponse{Success: true, Message: "Post updated"})
|
|
}
|
|
|
|
func handleDeletePost(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
postID := vars["id"]
|
|
|
|
userID, ok := r.Context().Value("userID").(string)
|
|
if !ok {
|
|
respondJSON(w, http.StatusUnauthorized, APIResponse{Success: false, Message: "Unauthorized"})
|
|
return
|
|
}
|
|
|
|
var authorID string
|
|
db.QueryRow("SELECT author_id FROM posts WHERE id = ?", postID).Scan(&authorID)
|
|
if authorID != userID {
|
|
respondJSON(w, http.StatusForbidden, APIResponse{Success: false, Message: "Forbidden"})
|
|
return
|
|
}
|
|
|
|
_, err := db.Exec("DELETE FROM posts WHERE id = ?", postID)
|
|
if err != nil {
|
|
respondJSON(w, http.StatusInternalServerError, APIResponse{Success: false, Message: "Failed to delete post"})
|
|
return
|
|
}
|
|
|
|
respondJSON(w, http.StatusOK, APIResponse{Success: true, Message: "Post deleted"})
|
|
}
|
|
|
|
func handleCreateComment(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
PostID string `json:"post_id"`
|
|
Content string `json:"content"`
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
respondJSON(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid request"})
|
|
return
|
|
}
|
|
|
|
commentID := uuid.New().String()
|
|
_, err := db.Exec(`
|
|
INSERT INTO comments (id, post_id, content, approved)
|
|
VALUES (?, ?, ?, FALSE)
|
|
`, commentID, req.PostID, req.Content)
|
|
|
|
if err != nil {
|
|
respondJSON(w, http.StatusInternalServerError, APIResponse{Success: false, Message: "Failed to create comment"})
|
|
return
|
|
}
|
|
|
|
respondJSON(w, http.StatusCreated, APIResponse{Success: true, Message: "Comment submitted for moderation"})
|
|
}
|
|
|
|
func handleSearch(w http.ResponseWriter, r *http.Request) {
|
|
query := r.URL.Query().Get("q")
|
|
if query == "" {
|
|
respondJSON(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Search query required"})
|
|
return
|
|
}
|
|
|
|
searchTerm := "%" + query + "%"
|
|
rows, err := db.Query(`
|
|
SELECT id, author_id, title, slug, excerpt, type, created_at
|
|
FROM posts
|
|
WHERE published = TRUE AND (title LIKE ? OR content LIKE ? OR excerpt LIKE ?)
|
|
ORDER BY created_at DESC
|
|
LIMIT 50
|
|
`, searchTerm, searchTerm, searchTerm)
|
|
|
|
if err != nil {
|
|
respondJSON(w, http.StatusInternalServerError, APIResponse{Success: false, Message: "Search failed"})
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var posts []Post
|
|
for rows.Next() {
|
|
var post Post
|
|
rows.Scan(&post.ID, &post.AuthorID, &post.Title, &post.Slug, &post.Excerpt, &post.Type, &post.CreatedAt)
|
|
posts = append(posts, post)
|
|
}
|
|
|
|
respondJSON(w, http.StatusOK, APIResponse{Success: true, Data: posts})
|
|
}
|
|
|
|
func handleRSSFeed(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/rss+xml; charset=utf-8")
|
|
|
|
rows, _ := db.Query(`
|
|
SELECT id, title, slug, excerpt, created_at
|
|
FROM posts
|
|
WHERE published = TRUE
|
|
ORDER BY created_at DESC
|
|
LIMIT 20
|
|
`)
|
|
defer rows.Close()
|
|
|
|
fmt.Fprint(w, `<?xml version="1.0" encoding="UTF-8"?>
|
|
<rss version="2.0">
|
|
<channel>
|
|
<title>Blog</title>
|
|
<link>`)
|
|
fmt.Fprint(w, getEnv("SITE_URL", "http://localhost:8080"))
|
|
fmt.Fprint(w, `</link>
|
|
<description>Latest posts</description>
|
|
`)
|
|
|
|
for rows.Next() {
|
|
var id, title, slug, excerpt string
|
|
var createdAt time.Time
|
|
rows.Scan(&id, &title, &slug, &excerpt, &createdAt)
|
|
|
|
fmt.Fprintf(w, `<item>
|
|
<title>%s</title>
|
|
<link>%s/post/%s</link>
|
|
<description>%s</description>
|
|
<pubDate>%s</pubDate>
|
|
</item>
|
|
`, title, getEnv("SITE_URL", "http://localhost:8080"), slug, excerpt, createdAt.Format(time.RFC1123Z))
|
|
}
|
|
|
|
fmt.Fprint(w, `</channel>
|
|
</rss>`)
|
|
}
|
|
|
|
func handleRegister(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
Username string `json:"username"`
|
|
Email string `json:"email"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
json.NewDecoder(r.Body).Decode(&req)
|
|
|
|
userID := uuid.New().String()
|
|
passwordHash := hashPassword(req.Password)
|
|
|
|
_, err := db.Exec(`
|
|
INSERT INTO users (id, username, email, password_hash, role)
|
|
VALUES (?, ?, ?, ?, 'author')
|
|
`, userID, req.Username, req.Email, passwordHash)
|
|
|
|
if err != nil {
|
|
respondJSON(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Registration failed"})
|
|
return
|
|
}
|
|
|
|
respondJSON(w, http.StatusCreated, APIResponse{Success: true, Message: "User created"})
|
|
}
|
|
|
|
func handleLogin(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
Email string `json:"email"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
json.NewDecoder(r.Body).Decode(&req)
|
|
|
|
var userID, passwordHash string
|
|
err := db.QueryRow("SELECT id, password_hash FROM users WHERE email = ?", req.Email).Scan(&userID, &passwordHash)
|
|
|
|
if err != nil || !verifyPassword(req.Password, passwordHash) {
|
|
respondJSON(w, http.StatusUnauthorized, APIResponse{Success: false, Message: "Invalid credentials"})
|
|
return
|
|
}
|
|
|
|
session, _ := sessionStore.Get(r, "auth")
|
|
session.Values["userID"] = userID
|
|
session.Save(r, w)
|
|
|
|
respondJSON(w, http.StatusOK, APIResponse{Success: true, Message: "Logged in"})
|
|
}
|
|
|
|
func handleLogout(w http.ResponseWriter, r *http.Request) {
|
|
session, _ := sessionStore.Get(r, "auth")
|
|
session.Options.MaxAge = -1
|
|
session.Save(r, w)
|
|
|
|
respondJSON(w, http.StatusOK, APIResponse{Success: true, Message: "Logged out"})
|
|
}
|
|
|
|
func handleDashboard(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
renderTemplate(w, "dashboard.html", nil)
|
|
}
|
|
|
|
// Utility functions
|
|
func getEnv(key, defaultVal string) string {
|
|
if value := os.Getenv(key); value != "" {
|
|
return value
|
|
}
|
|
return defaultVal
|
|
}
|
|
|
|
func slugify(s string) string {
|
|
return strings.ToLower(strings.ReplaceAll(strings.TrimSpace(s), " ", "-"))
|
|
}
|
|
|
|
func hashPassword(password string) string {
|
|
// In production, use bcrypt
|
|
return password
|
|
}
|
|
|
|
func verifyPassword(password, hash string) bool {
|
|
// In production, use bcrypt
|
|
return password == hash
|
|
}
|
|
|
|
func respondJSON(w http.ResponseWriter, statusCode int, data interface{}) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(statusCode)
|
|
json.NewEncoder(w).Encode(data)
|
|
}
|
|
|
|
func renderTemplate(w http.ResponseWriter, name string, data interface{}) {
|
|
t, err := template.ParseFiles("templates/" + name)
|
|
if err != nil {
|
|
http.Error(w, "Template error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
t.Execute(w, data)
|
|
}
|
|
|
|
func loggingMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
log.Printf("%s %s %s", r.Method, r.RequestURI, r.RemoteAddr)
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|