first commit, mate!
This commit is contained in:
@@ -0,0 +1,684 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user