add user articles

This commit is contained in:
2025-12-18 00:13:27 +03:00
parent b72974ef62
commit f1a65f2fe4
25 changed files with 690 additions and 8 deletions

View File

@@ -1,14 +1,20 @@
package main
import (
"bytes"
"compress/gzip"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
"archive.local/app"
"github.com/golang-jwt/jwt/v5"
"github.com/jmoiron/sqlx"
"github.com/joho/godotenv"
@@ -29,6 +35,16 @@ type User struct {
UpdatedAt string `json:"updated_at" db:"updated_at"`
}
type Article struct {
ID int64 `json:"id" db:"id"`
Title string `json:"title" db:"title"`
URL string `json:"url" db:"url"`
Body []byte `json:"-" db:"body"`
UserID int64 `json:"-" db:"user_id"`
CreatedAt string `json:"created_at" db:"created_at"`
UpdatedAt string `json:"updated_at" db:"updated_at"`
}
func readJSON(r *http.Request, s any) error {
return json.NewDecoder(r.Body).Decode(s)
}
@@ -74,6 +90,39 @@ func generateAccessToken(userId int64) (string, error) {
return token.SignedString(jwtSecretKey)
}
func getUserIdFromAccessToken(accessToken string) (int64, error) {
token, err := jwt.Parse(accessToken, func(t *jwt.Token) (any, error) {
return jwtSecretKey, nil
})
if err != nil {
return -1, err
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
if id, ok := claims["id"].(float64); ok {
return int64(id), nil
} else {
return -1, fmt.Errorf("couldn't convert id to float64")
}
} else {
return -1, fmt.Errorf("invalid token")
}
}
func getUserIdFromRequest(r *http.Request) (int64, error) {
token, err := r.Cookie("access_token")
if err != nil {
return -1, err
}
userId, err := getUserIdFromAccessToken(token.Value)
if err != nil {
return -1, err
}
return userId, nil
}
type AuthResponse struct {
AccessToken string `json:"access_token"`
}
@@ -195,6 +244,179 @@ func main() {
sendJSON(w, AuthResponse{token}, 200)
})
mux.HandleFunc("GET /articles", func(w http.ResponseWriter, r *http.Request) {
userId, err := getUserIdFromRequest(r)
if err != nil {
sendApiError(w, "invalid token", err, 401)
return
}
rows, err := db.Queryx("SELECT * FROM articles WHERE user_id = ?", userId)
if err != nil {
sendApiError(w, "couldn't find articles", err, 500)
return
}
articles := []Article{}
for rows.Next() {
var article Article
if err := rows.StructScan(&article); err != nil {
continue
}
articles = append(articles, article)
}
sendJSON(w, articles, 200)
})
mux.HandleFunc("POST /articles", func(w http.ResponseWriter, r *http.Request) {
userId, err := getUserIdFromRequest(r)
if err != nil {
sendApiError(w, "invalid token", err, 401)
return
}
var body struct {
URL string `json:"url"`
}
if err := readJSON(r, &body); err != nil {
sendApiError(w, "couldn't parse body", err, 500)
return
}
if body.URL == "" {
sendApiError(w, "invalid request", nil, 400)
return
}
title, html, err := app.FetchArticleHTML(body.URL)
if err != nil {
sendApiError(w, "couldn't fetch article", err, 500)
return
}
var b bytes.Buffer
gw := gzip.NewWriter(&b)
gw.Write([]byte(html))
gw.Close()
res, err := db.Exec("INSERT INTO articles (title, url, body, user_id) VALUES (?, ?, ?, ?)", title, body.URL, b.Bytes(), userId)
if err != nil {
sendApiError(w, "couldn't save article", err, 500)
return
}
_ = res
fmt.Fprint(w, "ok")
})
mux.HandleFunc("PATCH /articles/{id}", func(w http.ResponseWriter, r *http.Request) {
userId, err := getUserIdFromRequest(r)
if err != nil {
sendApiError(w, "invalid token", err, 401)
return
}
articleIdStr := r.PathValue("id")
articleId, err := strconv.Atoi(articleIdStr)
if err != nil {
sendApiError(w, "couldn't parse article id from url", err, 500)
return
}
var body struct {
Title string `json:"title"`
}
if err := readJSON(r, &body); err != nil {
sendApiError(w, "couldn't parse body", err, 500)
return
}
clauses := []string{}
args := map[string]any{"id": articleId, "user_id": userId}
if body.Title != "" {
clauses = append(clauses, "title = :title")
args["title"] = body.Title
}
q := "UPDATE articles SET " + strings.Join(clauses, ", ") + " WHERE id = :id AND user_id = :user_id"
res, err := db.NamedExec(q, args)
if err != nil {
sendApiError(w, "couldn't update article", err, 500)
return
}
_ = res
fmt.Fprint(w, "ok")
})
mux.HandleFunc("GET /articles/{id}/body", func(w http.ResponseWriter, r *http.Request) {
userId, err := getUserIdFromRequest(r)
if err != nil {
sendApiError(w, "invalid token", err, 401)
return
}
articleIdStr := r.PathValue("id")
articleId, err := strconv.Atoi(articleIdStr)
if err != nil {
sendApiError(w, "couldn't parse article id from url", err, 500)
return
}
row := db.QueryRowx("SELECT * FROM articles WHERE id = ? AND user_id = ?", articleId, userId)
if row.Err() != nil {
sendApiError(w, "article not found", err, 404)
return
}
var article Article
if err := row.StructScan(&article); err != nil {
sendApiError(w, "couldn't scan article to struct", err, 500)
return
}
gzr, err := gzip.NewReader(bytes.NewReader(article.Body))
if err != nil {
sendApiError(w, "couldn't parse article", err, 500)
return
}
data, err := io.ReadAll(gzr)
if err != nil {
sendApiError(w, "couldn't decompress article", err, 500)
return
}
w.WriteHeader(200)
w.Header().Set("Content-Type", "text/html")
w.Write(data)
})
mux.HandleFunc("DELETE /articles/{id}", func(w http.ResponseWriter, r *http.Request) {
userId, err := getUserIdFromRequest(r)
if err != nil {
sendApiError(w, "invalid token", err, 401)
return
}
articleIdStr := r.PathValue("id")
articleId, err := strconv.Atoi(articleIdStr)
if err != nil {
sendApiError(w, "couldn't parse article id from url", err, 500)
return
}
_, err = db.Exec("DELETE FROM articles WHERE id = ? AND user_id = ?", articleId, userId)
if err != nil {
sendApiError(w, "article not found", err, 404)
return
}
fmt.Fprint(w, "ok")
})
if err := http.ListenAndServe(*addr, handler(mux)); err != nil {
log.Fatalf("failed to start http server: %v\n", err)
}