diff --git a/.air.toml b/.air.toml index 6118030..2157d00 100644 --- a/.air.toml +++ b/.air.toml @@ -4,6 +4,7 @@ tmp_dir = "tmp" [build] args_bin = [ + "-articles tmp/articles", "-desc 'Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität'", "-domain localhost", "-key tmp/key.gob", diff --git a/cmd/backend/articles.go b/cmd/backend/articles.go index 392c882..36a14d7 100644 --- a/cmd/backend/articles.go +++ b/cmd/backend/articles.go @@ -13,6 +13,7 @@ type Article struct { Created time.Time Description string Content string + Link string Published bool Rejected bool ID int64 @@ -26,8 +27,8 @@ func (db *DB) AddArticle(a *Article) (int64, error) { selectQuery := "SELECT id FROM issues WHERE published = false" insertQuery := ` INSERT INTO articles - (title, description, content, published, rejected, author_id, issue_id) - VALUES (?, ?, ?, ?, ?, ?, ?) + (title, description, content, link, published, rejected, author_id, issue_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) ` for i := 0; i < TxMaxRetries; i++ { @@ -45,7 +46,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) { } result, err := tx.Exec(insertQuery, a.Title, a.Description, - a.Content, a.Published, a.Rejected, a.AuthorID, id) + a.Content, a.Link, a.Published, a.Rejected, a.AuthorID, id) if err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) @@ -79,7 +80,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) { func (db *DB) GetArticle(id int64) (*Article, error) { query := ` - SELECT title, created, description, content, published, author_id + SELECT title, created, description, content, link, published, author_id FROM articles WHERE id = ? ` @@ -90,7 +91,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) { var err error if err := row.Scan(&article.Title, &created, &article.Description, - &article.Content, &article.Published, &article.AuthorID); err != nil { + &article.Content, &article.Link, &article.Published, &article.AuthorID); err != nil { return nil, fmt.Errorf("error scanning article row: %v", err) } @@ -105,7 +106,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) { func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) { query := ` - SELECT id, title, created, description, content, author_id, issue_id + SELECT id, title, created, description, content, link, author_id, issue_id FROM articles WHERE published = ? AND rejected = ? @@ -121,8 +122,8 @@ func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) { var created []byte if err = rows.Scan(&article.ID, &article.Title, &created, - &article.Description, &article.Content, &article.AuthorID, - &article.IssueID); err != nil { + &article.Description, &article.Content, &article.Link, + &article.AuthorID, &article.IssueID); err != nil { return nil, fmt.Errorf("error scanning article row: %v", err) } @@ -143,7 +144,7 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) { txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable} issueQuery := "SELECT id FROM issues WHERE published = false" articlesQuery := ` - SELECT id, title, created, description, content, author_id + SELECT id, title, created, description, content, link, author_id FROM articles WHERE issue_id = ? AND published = true ` @@ -177,7 +178,7 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) { var created []byte if err = rows.Scan(&article.ID, &article.Title, &created, - &article.Description, &article.Content, &article.AuthorID); err != nil { + &article.Description, &article.Content, &article.Link, &article.AuthorID); err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) } diff --git a/cmd/backend/config.go b/cmd/backend/config.go index 7f5441b..416b325 100644 --- a/cmd/backend/config.go +++ b/cmd/backend/config.go @@ -11,6 +11,7 @@ import ( ) type Config struct { + ArticleDir string DBName string Description string Domain string @@ -28,6 +29,7 @@ type Config struct { func newConfig() *Config { return &Config{ + ArticleDir: "/var/www/cpolis/articles", DBName: "cpolis", FirebaseKey: "/var/www/cpolis/serviceAccountKey.json", KeyFile: "/var/www/cpolis/cpolis.key", @@ -77,6 +79,7 @@ func (c *Config) handleCliArgs() error { var err error port := 8080 + flag.StringVar(&c.ArticleDir, "articles", c.ArticleDir, "articles directory") flag.StringVar(&c.DBName, "db", c.DBName, "DB name") flag.StringVar(&c.Description, "desc", c.Description, "channel description") flag.StringVar(&c.Domain, "domain", c.Domain, "domain name") @@ -92,6 +95,14 @@ func (c *Config) handleCliArgs() error { flag.IntVar(&port, "port", port, "port") flag.Parse() + c.ArticleDir, err = filepath.Abs(c.ArticleDir) + if err != nil { + return fmt.Errorf("error finding absolute path for articles directory: %v", err) + } + if err = os.MkdirAll(c.ArticleDir, 0755); err != nil { + return fmt.Errorf("error creating articles directory: %v", err) + } + c.FirebaseKey, err = filepath.Abs(c.FirebaseKey) if err != nil { return fmt.Errorf("error finding absolute path for Firebase service account key file: %v", err) @@ -109,12 +120,18 @@ func (c *Config) handleCliArgs() error { c.PDFDir, err = filepath.Abs(c.PDFDir) if err != nil { - return fmt.Errorf("error finding absolute path for pdfs dir: %v", err) + return fmt.Errorf("error finding absolute path for pdfs directory: %v", err) + } + if err = os.MkdirAll(c.PDFDir, 0755); err != nil { + return fmt.Errorf("error creating pdfs directory: %v", err) } c.PicsDir, err = filepath.Abs(c.PicsDir) if err != nil { - return fmt.Errorf("error finding absolute path for pics dir: %v", err) + return fmt.Errorf("error finding absolute path for pics directory: %v", err) + } + if err = os.MkdirAll(c.PicsDir, 0755); err != nil { + return fmt.Errorf("error creating pics directory: %v", err) } c.Port = fmt.Sprint(":", port) @@ -126,7 +143,10 @@ func (c *Config) handleCliArgs() error { c.WebDir, err = filepath.Abs(c.WebDir) if err != nil { - return fmt.Errorf("error finding absolute path for web dir: %v", err) + return fmt.Errorf("error finding absolute path for web directory: %v", err) + } + if err = os.MkdirAll(c.WebDir, 0755); err != nil { + return fmt.Errorf("error creating web directory: %v", err) } return nil diff --git a/cmd/backend/rss.go b/cmd/backend/rss.go index 1807da0..7981fb2 100644 --- a/cmd/backend/rss.go +++ b/cmd/backend/rss.go @@ -39,7 +39,7 @@ func GetChannel(db *DB, title, link, description string) (*rss.Channel, error) { channel.Items = append(channel.Items, &rss.Item{ Title: article.Title, - Author: user.FirstName + user.LastName, + Author: fmt.Sprint(user.FirstName, " ", user.LastName), PubDate: article.Created.Format(time.RFC1123Z), Description: article.Description, Content: &rss.Content{Value: article.Content}, @@ -89,14 +89,22 @@ func GenerateRSS(c *Config, db *DB) (*string, error) { return nil, fmt.Errorf("error converting description to plain text for RSS feed: %v", err) } - channel.Items = append(channel.Items, &rss.Item{ - Author: fmt.Sprint(user.FirstName, user.LastName), + item := &rss.Item{ + Author: fmt.Sprint(user.FirstName, " ", user.LastName), Categories: tagNames, Description: articleDescription, - Link: fmt.Sprintf("http://%s/article/serve/%d", c.Domain, article.ID), PubDate: article.Created.Format(time.RFC1123Z), Title: articleTitle, - }) + } + fmt.Println(article.Link, ": ", len(article.Link)) + + if article.Link == "" { + item.Link = fmt.Sprint("http://", c.Domain, "/article/serve/", article.ID, ".html") + } else { + item.Link = article.Link + } + + channel.Items = append(channel.Items, item) } feed := rss.NewFeed() diff --git a/cmd/calls/articles.go b/cmd/calls/articles.go index 891f7a0..363dcbc 100644 --- a/cmd/calls/articles.go +++ b/cmd/calls/articles.go @@ -11,23 +11,25 @@ import ( func ServeArticle(c *b.Config, db *b.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - if tokenIsVerified(w, r, c) { - idString := r.PathValue("id") - id, err := strconv.ParseInt(idString, 10, 64) - if err != nil { - log.Println(err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - article, err := db.GetArticle(id) - if err != nil { - log.Println(err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - fmt.Fprint(w, article.Content) + if !tokenIsVerified(w, r, c) { + return } + + idString := r.PathValue("id") + id, err := strconv.ParseInt(idString, 10, 64) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + article, err := db.GetArticle(id) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + fmt.Fprint(w, article.Content) } } diff --git a/cmd/calls/images.go b/cmd/calls/images.go index 42a2d19..dd7a491 100644 --- a/cmd/calls/images.go +++ b/cmd/calls/images.go @@ -10,15 +10,17 @@ import ( func ServeImage(c *b.Config, s *b.CookieStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - if tokenIsVerified(w, r, c) { - absFilepath, err := filepath.Abs(c.PicsDir) - if err != nil { - log.Println(err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - http.ServeFile(w, r, absFilepath+"/"+r.PathValue("pic")) + if !tokenIsVerified(w, r, c) { + return } + + absFilepath, err := filepath.Abs(c.PicsDir) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + http.ServeFile(w, r, absFilepath+"/"+r.PathValue("pic")) } } diff --git a/cmd/calls/pdf.go b/cmd/calls/pdf.go index 8779634..c0213eb 100644 --- a/cmd/calls/pdf.go +++ b/cmd/calls/pdf.go @@ -12,33 +12,37 @@ import ( func ServePDFList(c *b.Config) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - if tokenIsVerified(w, r, c) { - files, err := os.ReadDir(c.PDFDir) - if err != nil { - log.Println(err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } + if !tokenIsVerified(w, r, c) { + return + } - fileNames := make([]string, 0) - for _, file := range files { - fileNames = append(fileNames, file.Name()) - } + files, err := os.ReadDir(c.PDFDir) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } - w.Header().Set("Content-Type", "application/json") - if err = json.NewEncoder(w).Encode(fileNames); err != nil { - log.Println(err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } + fileNames := make([]string, 0) + for _, file := range files { + fileNames = append(fileNames, file.Name()) + } + + w.Header().Set("Content-Type", "application/json") + if err = json.NewEncoder(w).Encode(fileNames); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return } } } func ServePDF(c *b.Config) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - if tokenIsVerified(w, r, c) { - http.ServeFile(w, r, fmt.Sprint(c.PDFDir, "/", r.PathValue("id"))) + if !tokenIsVerified(w, r, c) { + return } + + http.ServeFile(w, r, fmt.Sprint(c.PDFDir, "/", r.PathValue("id"))) } } diff --git a/cmd/calls/rss.go b/cmd/calls/rss.go index f8bc83a..d2757a7 100644 --- a/cmd/calls/rss.go +++ b/cmd/calls/rss.go @@ -8,8 +8,10 @@ import ( func ServeRSS(c *b.Config) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - if tokenIsVerified(w, r, c) { - http.ServeFile(w, r, c.RSSFile) + if !tokenIsVerified(w, r, c) { + return } + + http.ServeFile(w, r, c.RSSFile) } } diff --git a/cmd/frontend/articles.go b/cmd/frontend/articles.go index dac02e8..fa0c61e 100644 --- a/cmd/frontend/articles.go +++ b/cmd/frontend/articles.go @@ -5,7 +5,6 @@ import ( "fmt" "html/template" "io" - "io/fs" "log" "net/http" "os" @@ -347,6 +346,27 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } + article, err := db.GetArticle(id) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + content, err := b.ConvertToHTML(article.Content) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + err = os.WriteFile(fmt.Sprint(c.ArticleDir, "/", article.ID, ".html"), []byte(content), 0444) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if err = db.AddArticleToCurrentIssue(id); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -427,7 +447,7 @@ func ShowCurrentArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFu } } -func UploadImage(c *b.Config, s *b.CookieStore) http.HandlerFunc { +func UploadArticleImage(c *b.Config, s *b.CookieStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if _, err := getSession(w, r, c, s); err != nil { return @@ -436,7 +456,7 @@ func UploadImage(c *b.Config, s *b.CookieStore) http.HandlerFunc { file, header, err := r.FormFile("article-image") if err != nil { log.Println(err) - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, err.Error(), http.StatusBadRequest) return } defer file.Close() @@ -451,12 +471,6 @@ func UploadImage(c *b.Config, s *b.CookieStore) http.HandlerFunc { return } - if err = os.MkdirAll(fmt.Sprint(c.PicsDir, "/"), fs.FileMode(0755)); err != nil { - log.Println(err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - img, err := os.Create(absFilepath) if err != nil { log.Println(err) @@ -471,7 +485,7 @@ func UploadImage(c *b.Config, s *b.CookieStore) http.HandlerFunc { return } - url := fmt.Sprint(c.Domain, "/pics/", filename) + url := fmt.Sprint(c.Domain, "/image/serve/", filename) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(url) } diff --git a/cmd/frontend/issues.go b/cmd/frontend/issues.go index 5361e48..187352e 100644 --- a/cmd/frontend/issues.go +++ b/cmd/frontend/issues.go @@ -1,10 +1,17 @@ package frontend import ( + "fmt" "html/template" + "io" "log" "net/http" + "os" + "path/filepath" + "strings" + "time" + "github.com/google/uuid" b "streifling.com/jason/cpolis/cmd/backend" ) @@ -15,14 +22,141 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun return } + session.Values["article"] = nil + if err = session.Save(r, w); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if session.Values["issue-image"] == nil { + err := "error: Image required" + log.Println(err) + http.Error(w, err, http.StatusBadRequest) + return + } + + article := &b.Article{ + Title: "Autogenerated Issue Article", + Content: r.PostFormValue("issue-content"), + Link: session.Values["issue-image"].(string), + Published: true, + Rejected: false, + Created: time.Now(), + AuthorID: session.Values["id"].(int64), + } + fmt.Println(article.Link) + + content, err := b.ConvertToHTML(article.Content) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + err = os.WriteFile(fmt.Sprint(c.ArticleDir, "/", article.ID, ".html"), []byte(content), 0444) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + article.ID, err = db.AddArticle(article) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if err = db.AddArticleToCurrentIssue(article.ID); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if err := db.PublishLatestIssue(); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } + feed, err := b.GenerateRSS(c, db) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if err = b.SaveRSS(c.RSSFile, feed); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + session.Values["issue-image"] = nil + if err = session.Save(r, w); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html") tmpl = template.Must(tmpl, err) tmpl.ExecuteTemplate(w, "page-content", session.Values["role"]) } } + +func UploadIssueImage(c *b.Config, s *b.CookieStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + session, err := getSession(w, r, c, s) + if err != nil { + return + } + + if err := r.ParseMultipartForm(10 << 20); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + file, header, err := r.FormFile("issue-image") + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer file.Close() + + nameStrings := strings.Split(header.Filename, ".") + extension := "." + nameStrings[len(nameStrings)-1] + filename := fmt.Sprint(uuid.New(), extension) + absFilepath, err := filepath.Abs(fmt.Sprint(c.PicsDir, "/", filename)) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + img, err := os.Create(absFilepath) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer img.Close() + + if _, err = io.Copy(img, file); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + session.Values["issue-image"] = filename + if err = session.Save(r, w); err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + } +} diff --git a/cmd/frontend/sessions.go b/cmd/frontend/sessions.go index 514d9cf..3a53a28 100644 --- a/cmd/frontend/sessions.go +++ b/cmd/frontend/sessions.go @@ -60,7 +60,7 @@ func Login(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { id, ok := db.GetID(userName) if !ok { - http.Error(w, fmt.Sprintf("no such user: %v", userName), http.StatusInternalServerError) + http.Error(w, fmt.Sprintf("no such user: %v", userName), http.StatusBadRequest) return } @@ -96,7 +96,6 @@ func Logout(c *b.Config, s *b.CookieStore) http.HandlerFunc { } session.Options.MaxAge = -1 - if err = session.Save(r, w); err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/cmd/frontend/users.go b/cmd/frontend/users.go index 3eefa7f..848a033 100644 --- a/cmd/frontend/users.go +++ b/cmd/frontend/users.go @@ -198,10 +198,6 @@ func UpdateSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { func AddFirstUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - if _, err := getSession(w, r, c, s); err != nil { - return - } - var err error htmlData := UserData{ User: &b.User{ diff --git a/cmd/main.go b/cmd/main.go index 157fba7..79d7b32 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -62,7 +62,6 @@ func main() { mux.HandleFunc("GET /article/write", f.WriteArticle(config, db, store)) mux.HandleFunc("GET /hub", f.ShowHub(config, db, store)) mux.HandleFunc("GET /image/serve/{pic}", c.ServeImage(config, store)) - mux.HandleFunc("GET /issue/publish", f.PublishLatestIssue(config, db, store)) mux.HandleFunc("GET /issue/this", f.ShowCurrentArticles(config, db, store)) mux.HandleFunc("GET /logout", f.Logout(config, store)) mux.HandleFunc("GET /pdf/get-list", c.ServePDFList(config)) @@ -78,7 +77,9 @@ func main() { mux.HandleFunc("POST /article/resubmit/{id}", f.ResubmitArticle(config, db, store)) mux.HandleFunc("POST /article/submit", f.SubmitArticle(config, db, store)) - mux.HandleFunc("POST /image/upload", f.UploadImage(config, store)) + mux.HandleFunc("POST /article/upload-image", f.UploadArticleImage(config, store)) + mux.HandleFunc("POST /issue/publish", f.PublishLatestIssue(config, db, store)) + mux.HandleFunc("POST /issue/upload-image", f.UploadIssueImage(config, store)) mux.HandleFunc("POST /login", f.Login(config, db, store)) mux.HandleFunc("POST /tag/add", f.AddTag(config, db, store)) mux.HandleFunc("POST /user/add", f.AddUser(config, db, store)) diff --git a/create_db.sql b/create_db.sql index 1fa3d1a..436475b 100644 --- a/create_db.sql +++ b/create_db.sql @@ -25,7 +25,7 @@ CREATE TABLE articles ( title VARCHAR(255) NOT NULL, created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, description TEXT NOT NULL, - content TEXT NOT NULL, + link VARCHAR(255), published BOOL NOT NULL, rejected BOOL NOT NULL, author_id INT NOT NULL, diff --git a/web/templates/current-articles.html b/web/templates/current-articles.html index 991622e..fc58ba5 100644 --- a/web/templates/current-articles.html +++ b/web/templates/current-articles.html @@ -1,17 +1,68 @@ {{define "page-content"}} -
{{.Description}}
+