Compare commits
74 Commits
Author | SHA1 | Date | |
---|---|---|---|
d2b21e7405 | |||
4bd255a7c4 | |||
2743899b65 | |||
065ffcdc30 | |||
38ef7b80d5 | |||
e3c192359f | |||
c777a77824 | |||
46532e4c85 | |||
c183043dac | |||
c722135a56 | |||
391b3bf157 | |||
0aa479763d | |||
ff0e229f03 | |||
8ef6ff729e | |||
e4624b8705 | |||
887fa863bc | |||
4592bdf970 | |||
dadd610b2d | |||
74d71cfb6a | |||
4004bcb8f0 | |||
9e0182ed03 | |||
e554174c28 | |||
8597f1b849 | |||
b04e0e5e81 | |||
ca7e7cddd3 | |||
62921c8e2a | |||
94431a2aa9 | |||
c7761e2dc8 | |||
5b1f20c5bc | |||
013cddc157 | |||
523ff9d2db | |||
84960fdd44 | |||
d0c566f8df | |||
0a387c1087 | |||
38caf02b2c | |||
435157a42f | |||
4163afb0b7 | |||
5e586aa49a | |||
6cc0fb40d7 | |||
66b2743d3d | |||
18617f1dbc | |||
3723b2b5e6 | |||
79ee20a50e | |||
e05521591b | |||
0c9a79e24a | |||
ce788bfd50 | |||
8147d9bef6 | |||
230a6278cc | |||
da0d65d40b | |||
42d6e0c198 | |||
a2d219b2c0 | |||
e1af2979af | |||
b02a882ed7 | |||
f6dedc6f10 | |||
edb448413b | |||
cdf0a49550 | |||
222b791e90 | |||
c3c0650210 | |||
bc58b1be44 | |||
ab6b9b9a4f | |||
91ef195a56 | |||
d077f700d8 | |||
e4589f3b84 | |||
cd67fe6df3 | |||
ec752b1c66 | |||
4663cedec5 | |||
3f1b18c29f | |||
4a11e1a497 | |||
a33e7f9896 | |||
2b2ab0d428 | |||
c52e35bf0b | |||
cddd88d2f6 | |||
be467521d9 | |||
10d8fceb77 |
@ -4,13 +4,17 @@ tmp_dir = "tmp"
|
|||||||
|
|
||||||
[build]
|
[build]
|
||||||
args_bin = [
|
args_bin = [
|
||||||
|
"-articles tmp/articles",
|
||||||
|
"-config tmp/config.toml",
|
||||||
"-desc 'Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität'",
|
"-desc 'Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität'",
|
||||||
"-domain localhost",
|
"-domain localhost",
|
||||||
|
"-firebase tmp/firebase.json",
|
||||||
"-key tmp/key.gob",
|
"-key tmp/key.gob",
|
||||||
"-link https://distrikt-ni-st.de",
|
"-link https://distrikt-ni-st.de",
|
||||||
"-log tmp/cpolis.log",
|
"-log tmp/cpolis.log",
|
||||||
"-pdfs tmp/pdfs",
|
"-pdfs tmp/pdfs",
|
||||||
"-pics tmp/pics",
|
"-pics tmp/pics",
|
||||||
|
"-port 8080",
|
||||||
"-rss tmp/orientexpress_alle.rss",
|
"-rss tmp/orientexpress_alle.rss",
|
||||||
"-title 'Freimaurer Distrikt Niedersachsen und Sachsen-Anhalt'",
|
"-title 'Freimaurer Distrikt Niedersachsen und Sachsen-Anhalt'",
|
||||||
"-web web",
|
"-web web",
|
||||||
|
@ -9,15 +9,21 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Article struct {
|
type Article struct {
|
||||||
Title string
|
Created time.Time
|
||||||
Created time.Time
|
Title string
|
||||||
Description string
|
Description string
|
||||||
Content string
|
Link string
|
||||||
Published bool
|
EncURL string
|
||||||
Rejected bool
|
EncType string
|
||||||
ID int64
|
ID int64
|
||||||
AuthorID int64
|
AuthorID int64
|
||||||
IssueID int64
|
IssueID int64
|
||||||
|
EditedID int64
|
||||||
|
EncLength int
|
||||||
|
Published bool
|
||||||
|
Rejected bool
|
||||||
|
IsInIssue bool
|
||||||
|
AutoGenerated bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) AddArticle(a *Article) (int64, error) {
|
func (db *DB) AddArticle(a *Article) (int64, error) {
|
||||||
@ -26,8 +32,9 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
|
|||||||
selectQuery := "SELECT id FROM issues WHERE published = false"
|
selectQuery := "SELECT id FROM issues WHERE published = false"
|
||||||
insertQuery := `
|
insertQuery := `
|
||||||
INSERT INTO articles
|
INSERT INTO articles
|
||||||
(title, description, content, published, rejected, author_id, issue_id)
|
(title, description, link, enc_url, enc_length, enc_type, published,
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
rejected, author_id, issue_id, edited_id, is_in_issue, auto_generated)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`
|
`
|
||||||
|
|
||||||
for i := 0; i < TxMaxRetries; i++ {
|
for i := 0; i < TxMaxRetries; i++ {
|
||||||
@ -44,8 +51,9 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
|
|||||||
return 0, fmt.Errorf("error getting issue ID when adding article to DB: %v", err)
|
return 0, fmt.Errorf("error getting issue ID when adding article to DB: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := tx.Exec(insertQuery, a.Title, a.Description,
|
result, err := tx.Exec(insertQuery, a.Title, a.Description, a.Link,
|
||||||
a.Content, a.Published, a.Rejected, a.AuthorID, id)
|
a.EncURL, a.EncLength, a.EncType, a.Published, a.Rejected, a.AuthorID, id,
|
||||||
|
a.EditedID, a.IsInIssue, a.AutoGenerated)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if rollbackErr := tx.Rollback(); rollbackErr != nil {
|
if rollbackErr := tx.Rollback(); rollbackErr != nil {
|
||||||
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
|
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
|
||||||
@ -79,7 +87,9 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
|
|||||||
|
|
||||||
func (db *DB) GetArticle(id int64) (*Article, error) {
|
func (db *DB) GetArticle(id int64) (*Article, error) {
|
||||||
query := `
|
query := `
|
||||||
SELECT title, created, description, content, published, author_id
|
SELECT
|
||||||
|
title, created, description, link, enc_url, enc_length, enc_type,
|
||||||
|
published, author_id, issue_id, edited_id, is_in_issue, auto_generated
|
||||||
FROM articles
|
FROM articles
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`
|
`
|
||||||
@ -90,7 +100,9 @@ func (db *DB) GetArticle(id int64) (*Article, error) {
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
if err := row.Scan(&article.Title, &created, &article.Description,
|
if err := row.Scan(&article.Title, &created, &article.Description,
|
||||||
&article.Content, &article.Published, &article.AuthorID); err != nil {
|
&article.Link, &article.EncURL, &article.EncLength, &article.EncType,
|
||||||
|
&article.Published, &article.AuthorID, &article.IssueID, &article.EditedID,
|
||||||
|
&article.IsInIssue, &article.AutoGenerated); err != nil {
|
||||||
return nil, fmt.Errorf("error scanning article row: %v", err)
|
return nil, fmt.Errorf("error scanning article row: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,14 +115,15 @@ func (db *DB) GetArticle(id int64) (*Article, error) {
|
|||||||
return article, nil
|
return article, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) {
|
func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, error) {
|
||||||
query := `
|
query := fmt.Sprintf(`
|
||||||
SELECT id, title, created, description, content, author_id, issue_id
|
SELECT
|
||||||
|
id, title, created, description, link, enc_url, enc_length, enc_type,
|
||||||
|
author_id, issue_id, published, rejected, is_in_issue, auto_generated
|
||||||
FROM articles
|
FROM articles
|
||||||
WHERE published = ?
|
WHERE %s = ?
|
||||||
AND rejected = ?
|
`, attribute)
|
||||||
`
|
rows, err := db.Query(query, value)
|
||||||
rows, err := db.Query(query, published, rejected)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error querying articles: %v", err)
|
return nil, fmt.Errorf("error querying articles: %v", err)
|
||||||
}
|
}
|
||||||
@ -121,12 +134,12 @@ func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) {
|
|||||||
var created []byte
|
var created []byte
|
||||||
|
|
||||||
if err = rows.Scan(&article.ID, &article.Title, &created,
|
if err = rows.Scan(&article.ID, &article.Title, &created,
|
||||||
&article.Description, &article.Content, &article.AuthorID,
|
&article.Description, &article.Link, &article.EncURL, &article.EncLength,
|
||||||
&article.IssueID); err != nil {
|
&article.EncType, &article.AuthorID, &article.IssueID, &article.Published,
|
||||||
|
&article.Rejected, &article.IsInIssue, &article.AutoGenerated); err != nil {
|
||||||
return nil, fmt.Errorf("error scanning article row: %v", err)
|
return nil, fmt.Errorf("error scanning article row: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
article.Published = false
|
|
||||||
article.Created, err = time.Parse("2006-01-02 15:04:05", string(created))
|
article.Created, err = time.Parse("2006-01-02 15:04:05", string(created))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error parsing created: %v", err)
|
return nil, fmt.Errorf("error parsing created: %v", err)
|
||||||
@ -143,9 +156,11 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) {
|
|||||||
txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable}
|
txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable}
|
||||||
issueQuery := "SELECT id FROM issues WHERE published = false"
|
issueQuery := "SELECT id FROM issues WHERE published = false"
|
||||||
articlesQuery := `
|
articlesQuery := `
|
||||||
SELECT id, title, created, description, content, author_id
|
SELECT
|
||||||
|
id, title, created, description, link, enc_url, enc_length, enc_type,
|
||||||
|
author_id, auto_generated
|
||||||
FROM articles
|
FROM articles
|
||||||
WHERE issue_id = ? AND published = true
|
WHERE issue_id = ? AND published = true AND is_in_issue = true
|
||||||
`
|
`
|
||||||
|
|
||||||
for i := 0; i < TxMaxRetries; i++ {
|
for i := 0; i < TxMaxRetries; i++ {
|
||||||
@ -177,7 +192,8 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) {
|
|||||||
var created []byte
|
var created []byte
|
||||||
|
|
||||||
if err = rows.Scan(&article.ID, &article.Title, &created,
|
if err = rows.Scan(&article.ID, &article.Title, &created,
|
||||||
&article.Description, &article.Content, &article.AuthorID); err != nil {
|
&article.Description, &article.Link, &article.EncURL, &article.EncLength,
|
||||||
|
&article.EncType, &article.AuthorID, &article.AutoGenerated); err != nil {
|
||||||
if rollbackErr := tx.Rollback(); rollbackErr != nil {
|
if rollbackErr := tx.Rollback(); rollbackErr != nil {
|
||||||
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
|
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
|
||||||
}
|
}
|
||||||
@ -259,14 +275,13 @@ func (db *DB) AddArticleToCurrentIssue(id int64) error {
|
|||||||
|
|
||||||
func (db *DB) DeleteArticle(id int64) error {
|
func (db *DB) DeleteArticle(id int64) error {
|
||||||
articlesTagsQuery := "DELETE FROM articles_tags WHERE article_id = ?"
|
articlesTagsQuery := "DELETE FROM articles_tags WHERE article_id = ?"
|
||||||
|
articlesQuery := "DELETE FROM articles WHERE id = ?"
|
||||||
|
|
||||||
_, err := db.Exec(articlesTagsQuery, id)
|
_, err := db.Exec(articlesTagsQuery, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error deleting article %v from DB: %v", id, err)
|
return fmt.Errorf("error deleting article %v from DB: %v", id, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
articlesQuery := "DELETE FROM articles WHERE id = ?"
|
|
||||||
|
|
||||||
_, err = db.Exec(articlesQuery, id)
|
_, err = db.Exec(articlesQuery, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error deleting article %v from DB: %v", id, err)
|
return fmt.Errorf("error deleting article %v from DB: %v", id, err)
|
||||||
|
@ -3,6 +3,7 @@ package backend
|
|||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@ -11,6 +12,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
ArticleDir string
|
||||||
|
ConfigFile string
|
||||||
DBName string
|
DBName string
|
||||||
Description string
|
Description string
|
||||||
Domain string
|
Domain string
|
||||||
@ -28,55 +31,73 @@ type Config struct {
|
|||||||
|
|
||||||
func newConfig() *Config {
|
func newConfig() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
|
ArticleDir: "/var/www/cpolis/articles",
|
||||||
|
ConfigFile: "/etc/cpolis/config.toml",
|
||||||
DBName: "cpolis",
|
DBName: "cpolis",
|
||||||
FirebaseKey: "/var/www/cpolis/serviceAccountKey.json",
|
FirebaseKey: "/var/www/cpolis/serviceAccountKey.json",
|
||||||
KeyFile: "/var/www/cpolis/cpolis.key",
|
KeyFile: "/var/www/cpolis/cpolis.key",
|
||||||
LogFile: "/var/log/cpolis.log",
|
LogFile: "/var/log/cpolis.log",
|
||||||
PDFDir: "/var/www/cpolis/pdfs",
|
PDFDir: "/var/www/cpolis/pdfs",
|
||||||
PicsDir: "/var/www/cpolis/pics",
|
PicsDir: "/var/www/cpolis/pics",
|
||||||
|
Port: ":8080",
|
||||||
RSSFile: "/var/www/cpolis/cpolis.rss",
|
RSSFile: "/var/www/cpolis/cpolis.rss",
|
||||||
WebDir: "/var/www/cpolis/web",
|
WebDir: "/var/www/cpolis/web",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) readFile() error {
|
func mkDir(path string, perm fs.FileMode) (string, error) {
|
||||||
cfgFile, err := filepath.Abs(os.Getenv("HOME") + "/.config/cpolis/config.toml")
|
var err error
|
||||||
|
|
||||||
|
stringSlice := strings.Split(path, "/")
|
||||||
|
name := stringSlice[len(stringSlice)-1]
|
||||||
|
|
||||||
|
path, err = filepath.Abs(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error getting absolute path for config file: %v", err)
|
return "", fmt.Errorf("error finding absolute path for %v directory: %v", name, err)
|
||||||
|
}
|
||||||
|
if err = os.MkdirAll(path, perm); err != nil {
|
||||||
|
return "", fmt.Errorf("error creating %v directory: %v", name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = os.Stat(cfgFile)
|
return path, nil
|
||||||
if os.IsNotExist(err) {
|
}
|
||||||
fileStrings := strings.Split(cfgFile, "/")
|
|
||||||
|
|
||||||
dir := strings.Join(fileStrings[0:len(fileStrings)-1], "/")
|
func mkFile(path string, filePerm, dirPerm fs.FileMode) (string, error) {
|
||||||
if err = os.MkdirAll(dir, 0755); err != nil {
|
var err error
|
||||||
return fmt.Errorf("error creating config directory: %v", err)
|
|
||||||
|
path, err = filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error finding absolute path for %v: %v", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stringSlice := strings.Split(path, "/")
|
||||||
|
_, err = os.Stat(path)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
dir := strings.Join(stringSlice[:len(stringSlice)-1], "/")
|
||||||
|
if err = os.MkdirAll(dir, dirPerm); err != nil {
|
||||||
|
return "", fmt.Errorf("error creating %v: %v", dir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fileName := fileStrings[len(fileStrings)-1]
|
fileName := stringSlice[len(stringSlice)-1]
|
||||||
file, err := os.Create(dir + "/" + fileName)
|
file, err := os.Create(dir + "/" + fileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error creating config file: %v", err)
|
return "", fmt.Errorf("error creating %v: %v", fileName, err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
if err = file.Chmod(0644); err != nil {
|
if err = file.Chmod(filePerm); err != nil {
|
||||||
return fmt.Errorf("error setting permissions for config file: %v", err)
|
return "", fmt.Errorf("error setting permissions for %v: %v", fileName, err)
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_, err = toml.DecodeFile(cfgFile, c)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error reading config file: %v", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return path, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) handleCliArgs() error {
|
func (c *Config) handleCliArgs() error {
|
||||||
|
var port int
|
||||||
var err error
|
var err error
|
||||||
port := 8080
|
|
||||||
|
|
||||||
|
flag.StringVar(&c.ArticleDir, "articles", c.ArticleDir, "articles directory")
|
||||||
|
flag.StringVar(&c.ConfigFile, "config", c.ConfigFile, "config file")
|
||||||
flag.StringVar(&c.DBName, "db", c.DBName, "DB name")
|
flag.StringVar(&c.DBName, "db", c.DBName, "DB name")
|
||||||
flag.StringVar(&c.Description, "desc", c.Description, "channel description")
|
flag.StringVar(&c.Description, "desc", c.Description, "channel description")
|
||||||
flag.StringVar(&c.Domain, "domain", c.Domain, "domain name")
|
flag.StringVar(&c.Domain, "domain", c.Domain, "domain name")
|
||||||
@ -92,41 +113,117 @@ func (c *Config) handleCliArgs() error {
|
|||||||
flag.IntVar(&port, "port", port, "port")
|
flag.IntVar(&port, "port", port, "port")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
c.FirebaseKey, err = filepath.Abs(c.FirebaseKey)
|
if port != 0 {
|
||||||
if err != nil {
|
c.Port = fmt.Sprint(":", port)
|
||||||
return fmt.Errorf("error finding absolute path for Firebase service account key file: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.KeyFile, err = filepath.Abs(c.KeyFile)
|
c.ConfigFile, err = mkFile(c.ConfigFile, 0600, 0700)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error finding absolute path for key file: %v", err)
|
return fmt.Errorf("error setting up file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.LogFile, err = filepath.Abs(c.LogFile)
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) handleFile(configFile string) error {
|
||||||
|
_, err := toml.DecodeFile(configFile, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error finding absolute path for log file: %v", err)
|
return fmt.Errorf("error reading config file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.PDFDir, err = filepath.Abs(c.PDFDir)
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) setupConfig(cliConfig *Config) error {
|
||||||
|
var err error
|
||||||
|
defaultConfig := newConfig()
|
||||||
|
|
||||||
|
if cliConfig.ArticleDir != defaultConfig.ArticleDir {
|
||||||
|
c.ArticleDir = cliConfig.ArticleDir
|
||||||
|
}
|
||||||
|
c.ArticleDir, err = mkDir(c.ArticleDir, 0700)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error finding absolute path for pdfs dir: %v", err)
|
return fmt.Errorf("error setting up directory: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.PicsDir, err = filepath.Abs(c.PicsDir)
|
if cliConfig.DBName != defaultConfig.DBName {
|
||||||
if err != nil {
|
c.DBName = cliConfig.DBName
|
||||||
return fmt.Errorf("error finding absolute path for pics dir: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Port = fmt.Sprint(":", port)
|
if cliConfig.Description != defaultConfig.Description {
|
||||||
|
c.Description = cliConfig.Description
|
||||||
c.RSSFile, err = filepath.Abs(c.RSSFile)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error finding absolute path for RSS file: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.WebDir, err = filepath.Abs(c.WebDir)
|
if cliConfig.Domain != defaultConfig.Domain {
|
||||||
|
c.Domain = cliConfig.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
if cliConfig.FirebaseKey != defaultConfig.FirebaseKey {
|
||||||
|
c.FirebaseKey = cliConfig.FirebaseKey
|
||||||
|
}
|
||||||
|
c.FirebaseKey, err = mkFile(c.FirebaseKey, 0600, 0700)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error finding absolute path for web dir: %v", err)
|
return fmt.Errorf("error setting up file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cliConfig.KeyFile != defaultConfig.KeyFile {
|
||||||
|
c.KeyFile = cliConfig.KeyFile
|
||||||
|
}
|
||||||
|
c.KeyFile, err = mkFile(c.KeyFile, 0600, 0700)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error setting up file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cliConfig.Link != defaultConfig.Link {
|
||||||
|
c.Link = cliConfig.Link
|
||||||
|
}
|
||||||
|
|
||||||
|
if cliConfig.LogFile != defaultConfig.LogFile {
|
||||||
|
c.LogFile = cliConfig.LogFile
|
||||||
|
}
|
||||||
|
c.LogFile, err = mkFile(c.LogFile, 0600, 0700)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error setting up file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cliConfig.PDFDir != defaultConfig.PDFDir {
|
||||||
|
c.PDFDir = cliConfig.PDFDir
|
||||||
|
}
|
||||||
|
c.PDFDir, err = mkDir(c.PDFDir, 0700)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error setting up directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cliConfig.PicsDir != defaultConfig.PicsDir {
|
||||||
|
c.PicsDir = cliConfig.PicsDir
|
||||||
|
}
|
||||||
|
c.PicsDir, err = mkDir(c.PicsDir, 0700)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error setting up directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cliConfig.Port != defaultConfig.Port {
|
||||||
|
c.Port = cliConfig.Port
|
||||||
|
}
|
||||||
|
|
||||||
|
if cliConfig.RSSFile != defaultConfig.RSSFile {
|
||||||
|
c.RSSFile = cliConfig.RSSFile
|
||||||
|
}
|
||||||
|
c.RSSFile, err = mkFile(c.RSSFile, 0600, 0700)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error setting up file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cliConfig.Title != defaultConfig.Title {
|
||||||
|
c.Title = cliConfig.Title
|
||||||
|
}
|
||||||
|
|
||||||
|
if cliConfig.WebDir != defaultConfig.WebDir {
|
||||||
|
c.WebDir = cliConfig.WebDir
|
||||||
|
}
|
||||||
|
c.WebDir, err = mkDir(c.WebDir, 0700)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error setting up directory: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -134,13 +231,19 @@ func (c *Config) handleCliArgs() error {
|
|||||||
|
|
||||||
func HandleConfig() (*Config, error) {
|
func HandleConfig() (*Config, error) {
|
||||||
config := newConfig()
|
config := newConfig()
|
||||||
|
cliConfig := newConfig()
|
||||||
|
|
||||||
if err := config.readFile(); err != nil {
|
if err := cliConfig.handleCliArgs(); err != nil {
|
||||||
return nil, fmt.Errorf("error reading config file: %v", err)
|
return nil, fmt.Errorf("error handling cli arguments: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := config.handleCliArgs(); err != nil {
|
err := config.handleFile(cliConfig.ConfigFile)
|
||||||
return nil, fmt.Errorf("error handling cli arguments: %v", err)
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading configuration file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = config.setupConfig(cliConfig); err != nil {
|
||||||
|
return nil, fmt.Errorf("error setting up files: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
|
@ -8,9 +8,7 @@ import (
|
|||||||
"google.golang.org/api/option"
|
"google.golang.org/api/option"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct{ *auth.Client }
|
||||||
*auth.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClient(c *Config) (*Client, error) {
|
func NewClient(c *Config) (*Client, error) {
|
||||||
var err error
|
var err error
|
||||||
|
@ -9,56 +9,15 @@ import (
|
|||||||
"git.streifling.com/jason/rss"
|
"git.streifling.com/jason/rss"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetChannel(db *DB, title, link, description string) (*rss.Channel, error) {
|
func GenerateRSS(c *Config, db *DB) (*string, error) {
|
||||||
channel := &rss.Channel{
|
channel := &rss.Channel{
|
||||||
Title: title,
|
Title: c.Title,
|
||||||
Link: link,
|
Link: c.Link,
|
||||||
Description: description,
|
Description: c.Description,
|
||||||
Items: make([]*rss.Item, 0),
|
Items: make([]*rss.Item, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
articles, err := db.GetCertainArticles(true, false)
|
articles, err := db.GetCertainArticles("published", true)
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error fetching published articles: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, article := range articles {
|
|
||||||
tags, err := db.GetArticleTags(article.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error fetching tags for article %v: %v", article.Title, err)
|
|
||||||
}
|
|
||||||
tagNames := make([]string, 0)
|
|
||||||
for _, tag := range tags {
|
|
||||||
tagNames = append(tagNames, tag.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := db.GetUser(article.AuthorID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error finding user %v: %v", article.AuthorID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
channel.Items = append(channel.Items, &rss.Item{
|
|
||||||
Title: article.Title,
|
|
||||||
Author: user.FirstName + user.LastName,
|
|
||||||
PubDate: article.Created.Format(time.RFC1123Z),
|
|
||||||
Description: article.Description,
|
|
||||||
Content: &rss.Content{Value: article.Content},
|
|
||||||
Categories: tagNames,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return channel, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GenerateRSS(db *DB, title, link, desc string) (*string, error) {
|
|
||||||
channel := &rss.Channel{
|
|
||||||
Title: title,
|
|
||||||
Link: link,
|
|
||||||
Description: desc,
|
|
||||||
Items: make([]*rss.Item, 0),
|
|
||||||
}
|
|
||||||
|
|
||||||
articles, err := db.GetCertainArticles(true, false)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error getting published articles for RSS feed: %v", err)
|
return nil, fmt.Errorf("error getting published articles for RSS feed: %v", err)
|
||||||
}
|
}
|
||||||
@ -72,7 +31,13 @@ func GenerateRSS(db *DB, title, link, desc string) (*string, error) {
|
|||||||
for _, tag := range tags {
|
for _, tag := range tags {
|
||||||
tagNames = append(tagNames, tag.Name)
|
tagNames = append(tagNames, tag.Name)
|
||||||
}
|
}
|
||||||
tagNames = append(tagNames, fmt.Sprint("Orient Express ", article.IssueID))
|
|
||||||
|
if article.IsInIssue || article.AutoGenerated {
|
||||||
|
tagNames = append(tagNames, fmt.Sprint("Orient Express ", article.IssueID))
|
||||||
|
}
|
||||||
|
if article.AutoGenerated {
|
||||||
|
tagNames = append(tagNames, "autogenerated")
|
||||||
|
}
|
||||||
|
|
||||||
user, err := db.GetUser(article.AuthorID)
|
user, err := db.GetUser(article.AuthorID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -89,19 +54,25 @@ func GenerateRSS(db *DB, title, link, desc string) (*string, error) {
|
|||||||
return nil, fmt.Errorf("error converting description to plain text for RSS feed: %v", err)
|
return nil, fmt.Errorf("error converting description to plain text for RSS feed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
articleContent, err := ConvertToHTML(article.Content)
|
item := &rss.Item{
|
||||||
if err != nil {
|
Author: fmt.Sprint(user.FirstName, " ", user.LastName),
|
||||||
return nil, fmt.Errorf("error converting content to HTML for RSS feed: %v", err)
|
Categories: tagNames,
|
||||||
|
Description: articleDescription,
|
||||||
|
Guid: string(article.ID),
|
||||||
|
Link: article.Link,
|
||||||
|
PubDate: article.Created.Format(time.RFC1123Z),
|
||||||
|
Title: articleTitle,
|
||||||
}
|
}
|
||||||
|
|
||||||
channel.Items = append(channel.Items, &rss.Item{
|
if article.AutoGenerated {
|
||||||
Title: articleTitle,
|
item.Enclosure = &rss.Enclosure{
|
||||||
Author: user.FirstName + " " + user.LastName,
|
Url: article.EncURL,
|
||||||
PubDate: article.Created.Format(time.RFC1123Z),
|
Lenght: article.EncLength,
|
||||||
Description: articleDescription,
|
Type: article.EncType,
|
||||||
Content: &rss.Content{Value: articleContent},
|
}
|
||||||
Categories: tagNames,
|
}
|
||||||
})
|
|
||||||
|
channel.Items = append(channel.Items, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
feed := rss.NewFeed()
|
feed := rss.NewFeed()
|
||||||
|
@ -10,9 +10,7 @@ import (
|
|||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CookieStore struct {
|
type CookieStore struct{ sessions.CookieStore }
|
||||||
sessions.CookieStore
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewKey() ([]byte, error) {
|
func NewKey() ([]byte, error) {
|
||||||
key := make([]byte, 32)
|
key := make([]byte, 32)
|
||||||
|
55
cmd/calls/articles.go
Normal file
55
cmd/calls/articles.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package calls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
b "streifling.com/jason/cpolis/cmd/backend"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ServeArticle(c *b.Config, db *b.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if !article.Published {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
|
||||||
|
contentBytes, err := os.ReadFile(articleAbsName)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := b.ConvertToHTML(string(contentBytes))
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(w, content)
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package frontend
|
package calls
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
func ServeImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
|
func ServeImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if _, err := getSession(w, r, c, s); err != nil {
|
if !tokenIsVerified(w, r, c) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -12,33 +12,37 @@ import (
|
|||||||
|
|
||||||
func ServePDFList(c *b.Config) http.HandlerFunc {
|
func ServePDFList(c *b.Config) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if tokenIsVerified(w, r, c) {
|
if !tokenIsVerified(w, r, c) {
|
||||||
files, err := os.ReadDir(c.PDFDir)
|
return
|
||||||
if err != nil {
|
}
|
||||||
log.Println(err)
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fileNames := make([]string, 0)
|
files, err := os.ReadDir(c.PDFDir)
|
||||||
for _, file := range files {
|
if err != nil {
|
||||||
fileNames = append(fileNames, file.Name())
|
log.Println(err)
|
||||||
}
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
fileNames := make([]string, 0)
|
||||||
if err = json.NewEncoder(w).Encode(fileNames); err != nil {
|
for _, file := range files {
|
||||||
log.Println(err)
|
fileNames = append(fileNames, file.Name())
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ServePDF(c *b.Config) http.HandlerFunc {
|
func ServePDF(c *b.Config) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if tokenIsVerified(w, r, c) {
|
if !tokenIsVerified(w, r, c) {
|
||||||
http.ServeFile(w, r, fmt.Sprint(c.PDFDir, "/", r.PathValue("id")))
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
http.ServeFile(w, r, fmt.Sprint(c.PDFDir, "/", r.PathValue("id")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,10 @@ import (
|
|||||||
|
|
||||||
func ServeRSS(c *b.Config) http.HandlerFunc {
|
func ServeRSS(c *b.Config) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if tokenIsVerified(w, r, c) {
|
if !tokenIsVerified(w, r, c) {
|
||||||
http.ServeFile(w, r, c.RSSFile)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
http.ServeFile(w, r, c.RSSFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@ -23,6 +22,38 @@ const (
|
|||||||
PreviewMode
|
PreviewMode
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type EditorHTMLData struct {
|
||||||
|
Selected map[int64]bool
|
||||||
|
Content string
|
||||||
|
Action string
|
||||||
|
ActionTitle string
|
||||||
|
ActionButton string
|
||||||
|
HTMLContent template.HTML
|
||||||
|
Article *b.Article
|
||||||
|
Tags []*b.Tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFile(src, dst string) error {
|
||||||
|
srcFile, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error opening source file: %v", err)
|
||||||
|
}
|
||||||
|
defer srcFile.Close()
|
||||||
|
|
||||||
|
dstFile, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error opening destination file: %v", err)
|
||||||
|
}
|
||||||
|
defer dstFile.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(dstFile, srcFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error copying file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dstFile.Sync()
|
||||||
|
}
|
||||||
|
|
||||||
func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
|
func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
session, err := getSession(w, r, c, s)
|
session, err := getSession(w, r, c, s)
|
||||||
@ -30,22 +61,14 @@ func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type editorHTMLData struct {
|
data := new(EditorHTMLData)
|
||||||
Title string
|
|
||||||
Description string
|
|
||||||
Content string
|
|
||||||
HTMLContent template.HTML
|
|
||||||
Tags []*b.Tag
|
|
||||||
Mode int
|
|
||||||
}
|
|
||||||
|
|
||||||
var data editorHTMLData
|
|
||||||
if session.Values["article"] == nil {
|
if session.Values["article"] == nil {
|
||||||
data = editorHTMLData{}
|
data = &EditorHTMLData{Article: new(b.Article)}
|
||||||
} else {
|
} else {
|
||||||
data = session.Values["article"].(editorHTMLData)
|
data = session.Values["article"].(*EditorHTMLData)
|
||||||
}
|
}
|
||||||
data.Mode = EditMode
|
// data.Mode = EditMode
|
||||||
|
|
||||||
data.Tags, err = db.GetTagList()
|
data.Tags, err = db.GetTagList()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -54,6 +77,8 @@ func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data.Action = "submit"
|
||||||
|
|
||||||
tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html")
|
tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html")
|
||||||
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data)
|
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data)
|
||||||
}
|
}
|
||||||
@ -74,12 +99,13 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
article := &b.Article{
|
article := &b.Article{
|
||||||
Title: r.PostFormValue("article-title"),
|
Title: r.PostFormValue("article-title"),
|
||||||
Description: r.PostFormValue("article-description"),
|
Description: r.PostFormValue("article-description"),
|
||||||
Content: r.PostFormValue("article-content"),
|
Published: false,
|
||||||
Published: false,
|
Rejected: false,
|
||||||
Rejected: false,
|
AuthorID: session.Values["id"].(int64),
|
||||||
AuthorID: session.Values["id"].(int64),
|
IsInIssue: r.PostFormValue("issue") == "on",
|
||||||
|
AutoGenerated: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
article.ID, err = db.AddArticle(article)
|
article.ID, err = db.AddArticle(article)
|
||||||
@ -89,6 +115,20 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
|
||||||
|
if err = os.WriteFile(articleAbsName, []byte(r.PostFormValue("article-content")), 0644); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
article.Link = fmt.Sprint(c.Domain, "/article/serve/", article.ID)
|
||||||
|
if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "link", Value: article.Link}); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
r.ParseForm()
|
r.ParseForm()
|
||||||
tags := make([]int64, 0)
|
tags := make([]int64, 0)
|
||||||
for _, tag := range r.Form["tags"] {
|
for _, tag := range r.Form["tags"] {
|
||||||
@ -130,11 +170,18 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
|
|||||||
description := r.PostFormValue("article-description")
|
description := r.PostFormValue("article-description")
|
||||||
content := r.PostFormValue("article-content")
|
content := r.PostFormValue("article-content")
|
||||||
|
|
||||||
|
link := fmt.Sprint(c.ArticleDir, "/", id, ".md")
|
||||||
|
if err = os.WriteFile(link, []byte(content), 0644); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err = db.UpdateAttributes(
|
if err = db.UpdateAttributes(
|
||||||
&b.Attribute{Table: "articles", ID: id, AttName: "title", Value: title},
|
&b.Attribute{Table: "articles", ID: id, AttName: "title", Value: title},
|
||||||
&b.Attribute{Table: "articles", ID: id, AttName: "description", Value: description},
|
&b.Attribute{Table: "articles", ID: id, AttName: "description", Value: description},
|
||||||
&b.Attribute{Table: "articles", ID: id, AttName: "content", Value: content},
|
|
||||||
&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false},
|
&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false},
|
||||||
|
&b.Attribute{Table: "articles", ID: id, AttName: "is_in_issue", Value: r.PostFormValue("issue") == "on"},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
@ -164,22 +211,41 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ShowUnpublishedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
|
func ShowUnpublishedUnrejectedAndPublishedRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if _, err := getSession(w, r, c, s); err != nil {
|
if _, err := getSession(w, r, c, s); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
unpublishedArticles, err := db.GetCertainArticles(false, false)
|
rejectedArticles, err := db.GetCertainArticles("rejected", true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
articles := make([]*b.Article, 0)
|
||||||
|
for _, article := range rejectedArticles {
|
||||||
|
if article.Published {
|
||||||
|
articles = append(articles, article)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unpublishedArticles, err := db.GetCertainArticles("published", false)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, article := range unpublishedArticles {
|
||||||
|
if !article.Rejected {
|
||||||
|
articles = append(articles, article)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tmpl, err := template.ParseFiles(c.WebDir + "/templates/unpublished-articles.html")
|
tmpl, err := template.ParseFiles(c.WebDir + "/templates/unpublished-articles.html")
|
||||||
tmpl = template.Must(tmpl, err)
|
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", articles)
|
||||||
tmpl.ExecuteTemplate(w, "page-content", unpublishedArticles)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,13 +256,12 @@ func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerF
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type htmlData struct {
|
data := new(struct {
|
||||||
MyIDs map[int64]bool
|
MyIDs map[int64]bool
|
||||||
RejectedArticles []*b.Article
|
RejectedArticles []*b.Article
|
||||||
}
|
})
|
||||||
data := new(htmlData)
|
|
||||||
|
|
||||||
data.RejectedArticles, err = db.GetCertainArticles(false, true)
|
data.RejectedArticles, err = db.GetCertainArticles("rejected", true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
@ -216,85 +281,12 @@ func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerF
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReviewUnpublishedArticle(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
|
|
||||||
}
|
|
||||||
|
|
||||||
type htmlData struct {
|
|
||||||
Title string
|
|
||||||
Description string
|
|
||||||
Content template.HTML
|
|
||||||
Tags []*b.Tag
|
|
||||||
ID int64
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
data := new(htmlData)
|
|
||||||
|
|
||||||
data.ID, err = strconv.ParseInt(r.PathValue("id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
article, err := db.GetArticle(data.ID)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
data.Title, err = b.ConvertToPlain(article.Title)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
data.Description, err = b.ConvertToPlain(article.Description)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
data.Content = template.HTML(content)
|
|
||||||
|
|
||||||
data.Tags, err = db.GetArticleTags(data.ID)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpl, err := template.ParseFiles(c.WebDir + "/templates/to-be-published.html")
|
|
||||||
tmpl = template.Must(tmpl, err)
|
|
||||||
tmpl.ExecuteTemplate(w, "page-content", data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
|
func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if _, err := getSession(w, r, c, s); err != nil {
|
if _, err := getSession(w, r, c, s); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type htmlData struct {
|
|
||||||
Selected map[int64]bool
|
|
||||||
Article *b.Article
|
|
||||||
Tags []*b.Tag
|
|
||||||
}
|
|
||||||
data := new(htmlData)
|
|
||||||
|
|
||||||
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
@ -302,6 +294,7 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data := new(EditorHTMLData)
|
||||||
data.Article, err = db.GetArticle(id)
|
data.Article, err = db.GetArticle(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
@ -309,6 +302,15 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
articleAbsName := fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md")
|
||||||
|
content, err := os.ReadFile(articleAbsName)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.Content = string(content)
|
||||||
|
|
||||||
data.Tags, err = db.GetTagList()
|
data.Tags, err = db.GetTagList()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
@ -327,7 +329,9 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
|
|||||||
data.Selected[tag.ID] = true
|
data.Selected[tag.ID] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl, err := template.ParseFiles(c.WebDir + "/templates/rework-article.html")
|
data.Action = fmt.Sprint("resubmit/", data.Article.ID)
|
||||||
|
|
||||||
|
tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html")
|
||||||
tmpl = template.Must(tmpl, err)
|
tmpl = template.Must(tmpl, err)
|
||||||
tmpl.ExecuteTemplate(w, "page-content", data)
|
tmpl.ExecuteTemplate(w, "page-content", data)
|
||||||
}
|
}
|
||||||
@ -341,13 +345,20 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
article, err := db.GetArticle(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = db.AddArticleToCurrentIssue(id); err != nil {
|
if err = db.AddArticleToCurrentIssue(article.ID); err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@ -363,7 +374,37 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
feed, err := b.GenerateRSS(db, c.Title, c.Link, c.Description)
|
if article.EditedID != 0 {
|
||||||
|
oldArticle, err := db.GetArticle(article.EditedID)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = db.DeleteArticle(oldArticle.ID); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = os.Remove(fmt.Sprint(c.ArticleDir, "/", oldArticle.ID, ".md")); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = db.UpdateAttributes(
|
||||||
|
&b.Attribute{Table: "articles", ID: id, AttName: "link", Value: fmt.Sprint(c.Domain, "/article/serve/", article.ID)},
|
||||||
|
&b.Attribute{Table: "articles", ID: id, AttName: "edited_id", Value: 0},
|
||||||
|
); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
feed, err := b.GenerateRSS(c, db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
@ -395,9 +436,7 @@ func RejectArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = db.UpdateAttributes(
|
if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: true}); err != nil {
|
||||||
&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: true},
|
|
||||||
); err != nil {
|
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@ -427,7 +466,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) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if _, err := getSession(w, r, c, s); err != nil {
|
if _, err := getSession(w, r, c, s); err != nil {
|
||||||
return
|
return
|
||||||
@ -436,7 +475,7 @@ func UploadImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
|
|||||||
file, header, err := r.FormFile("article-image")
|
file, header, err := r.FormFile("article-image")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
@ -451,12 +490,6 @@ func UploadImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
|
|||||||
return
|
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)
|
img, err := os.Create(absFilepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
@ -471,94 +504,111 @@ func UploadImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
url := fmt.Sprint(c.Domain, "/pics/", filename)
|
url := fmt.Sprint(c.Domain, "/image/serve/", filename)
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(url)
|
json.NewEncoder(w).Encode(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ShowPublishedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
|
func ShowPublishedArticles(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if _, err := getSession(w, r, c, s); err != nil {
|
if _, err := getSession(w, r, c, s); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
publishedArticles, err := db.GetCertainArticles(true, false)
|
data := new(struct {
|
||||||
|
Action string
|
||||||
|
Articles []*b.Article
|
||||||
|
})
|
||||||
|
data.Action = action
|
||||||
|
|
||||||
|
publishedArticles, err := db.GetCertainArticles("published", true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, article := range publishedArticles {
|
||||||
|
if !article.AutoGenerated {
|
||||||
|
data.Articles = append(data.Articles, article)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tmpl, err := template.ParseFiles(c.WebDir + "/templates/published-articles.html")
|
tmpl, err := template.ParseFiles(c.WebDir + "/templates/published-articles.html")
|
||||||
tmpl = template.Must(tmpl, err)
|
tmpl = template.Must(tmpl, err)
|
||||||
tmpl.ExecuteTemplate(w, "page-content", publishedArticles)
|
tmpl.ExecuteTemplate(w, "page-content", data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReviewArticleForDeletion(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
|
func ReviewArticle(c *b.Config, db *b.DB, s *b.CookieStore, action, title, button string) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if _, err := getSession(w, r, c, s); err != nil {
|
if _, err := getSession(w, r, c, s); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type htmlData struct {
|
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
Title string
|
|
||||||
Description string
|
|
||||||
Content template.HTML
|
|
||||||
Tags []*b.Tag
|
|
||||||
ID int64
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
data := new(htmlData)
|
|
||||||
|
|
||||||
data.ID, err = strconv.ParseInt(r.PathValue("id"), 10, 64)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
article, err := db.GetArticle(data.ID)
|
article, err := db.GetArticle(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data.Title, err = b.ConvertToPlain(article.Title)
|
data := &EditorHTMLData{
|
||||||
|
Article: &b.Article{
|
||||||
|
ID: id,
|
||||||
|
IsInIssue: article.IsInIssue,
|
||||||
|
},
|
||||||
|
Action: action,
|
||||||
|
ActionTitle: title,
|
||||||
|
ActionButton: button,
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Article.Title, err = b.ConvertToPlain(article.Title)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data.Description, err = b.ConvertToPlain(article.Description)
|
data.Article.Description, err = b.ConvertToPlain(article.Description)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
content, err := b.ConvertToHTML(article.Content)
|
articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
|
||||||
|
content, err := os.ReadFile(articleAbsName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data.Content = template.HTML(content)
|
data.Content, err = b.ConvertToHTML(string(content))
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.HTMLContent = template.HTML(data.Content)
|
||||||
|
|
||||||
data.Tags, err = db.GetArticleTags(data.ID)
|
data.Tags, err = db.GetArticleTags(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl, err := template.ParseFiles(c.WebDir + "/templates/to-be-deleted.html")
|
tmpl, err := template.ParseFiles(c.WebDir + "/templates/review-article.html")
|
||||||
tmpl = template.Must(tmpl, err)
|
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data)
|
||||||
tmpl.ExecuteTemplate(w, "page-content", data)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -582,7 +632,13 @@ func DeleteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
feed, err := b.GenerateRSS(db, c.Title, c.Link, c.Description)
|
if err = os.Remove(fmt.Sprint(c.ArticleDir, "/", id, ".md")); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
feed, err := b.GenerateRSS(c, db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
@ -599,3 +655,201 @@ func DeleteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
|
|||||||
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int))
|
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AllowEditArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
session, err := getSession(w, r, c, s)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oldID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(oldID)
|
||||||
|
|
||||||
|
oldArticle, err := db.GetArticle(oldID)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newArticle := oldArticle
|
||||||
|
newArticle.Published = false
|
||||||
|
newArticle.Rejected = true
|
||||||
|
newArticle.EditedID = oldArticle.ID
|
||||||
|
|
||||||
|
newID, err := db.AddArticle(newArticle)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: oldID, AttName: "edited_id", Value: newID}); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = copyFile(fmt.Sprint(c.ArticleDir, "/", oldID, ".md"), fmt.Sprint(c.ArticleDir, "/", newID, ".md")); 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"].(int))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func EditArticle(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
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := new(EditorHTMLData)
|
||||||
|
|
||||||
|
data.Article, err = db.GetArticle(id)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := os.ReadFile(fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md"))
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.Content = string(content)
|
||||||
|
|
||||||
|
data.Tags, err = db.GetArticleTags(data.Article.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedTags, err := db.GetArticleTags(id)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.Selected = make(map[int64]bool)
|
||||||
|
for _, tag := range selectedTags {
|
||||||
|
data.Selected[tag.ID] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Action = fmt.Sprint("save/", data.Article.ID)
|
||||||
|
|
||||||
|
tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html")
|
||||||
|
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
session, err := getSession(w, r, c, s)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session.Values["article"] = nil
|
||||||
|
if err = session.Save(r, w); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
article := &b.Article{
|
||||||
|
Title: r.PostFormValue("article-title"),
|
||||||
|
Description: r.PostFormValue("article-description"),
|
||||||
|
Published: false,
|
||||||
|
Rejected: false,
|
||||||
|
AuthorID: session.Values["id"].(int64),
|
||||||
|
IsInIssue: r.PostFormValue("issue") == "on",
|
||||||
|
AutoGenerated: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
oldID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(oldID)
|
||||||
|
if oldID != 0 {
|
||||||
|
if err = db.DeleteArticle(oldID); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = os.Remove(fmt.Sprint(c.ArticleDir, "/", oldID, ".md")); 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
|
||||||
|
}
|
||||||
|
|
||||||
|
articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
|
||||||
|
if err = os.WriteFile(articleAbsName, []byte(r.PostFormValue("article-content")), 0644); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
article.Link = fmt.Sprint(c.Domain, "/article/serve/", article.ID)
|
||||||
|
if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "link", Value: article.Link}); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.ParseForm()
|
||||||
|
tags := make([]int64, 0)
|
||||||
|
for _, tag := range r.Form["tags"] {
|
||||||
|
tagID, err := strconv.ParseInt(tag, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tags = append(tags, tagID)
|
||||||
|
}
|
||||||
|
if err = db.WriteArticleTags(article.ID, tags); 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"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,10 +1,18 @@
|
|||||||
package frontend
|
package frontend
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
b "streifling.com/jason/cpolis/cmd/backend"
|
b "streifling.com/jason/cpolis/cmd/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -15,14 +23,174 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
session.Values["article"] = nil
|
||||||
|
if err = session.Save(r, w); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
title := r.PostFormValue("issue-title")
|
||||||
|
if len(title) == 0 {
|
||||||
|
err = fmt.Errorf("error: no title for issue specified")
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
imgFileName := session.Values["issue-image"].(string)
|
||||||
|
imgAbsName := fmt.Sprint(c.PicsDir, "/", imgFileName)
|
||||||
|
|
||||||
|
imgFile, err := os.Open(imgAbsName)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer imgFile.Close()
|
||||||
|
|
||||||
|
imgInfo, err := imgFile.Stat()
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
article := &b.Article{
|
||||||
|
Title: title,
|
||||||
|
EncURL: fmt.Sprint(c.Domain, "/image/serve/", imgFileName),
|
||||||
|
EncLength: int(imgInfo.Size()),
|
||||||
|
EncType: mime.TypeByExtension(filepath.Ext(imgAbsName)),
|
||||||
|
Published: true,
|
||||||
|
Rejected: false,
|
||||||
|
Created: time.Now(),
|
||||||
|
AuthorID: session.Values["id"].(int64),
|
||||||
|
AutoGenerated: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
article.ID, err = db.AddArticle(article)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
|
||||||
|
if err = os.WriteFile(articleAbsName, []byte(r.PostFormValue("article-content")), 0644); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
article.Link = fmt.Sprint(c.Domain, "/article/serve/", article.ID)
|
||||||
|
if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "link", Value: article.Link}); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "link", Value: article.Link}); 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 {
|
if err := db.PublishLatestIssue(); err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
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, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
|
||||||
tmpl = template.Must(tmpl, err)
|
tmpl = template.Must(tmpl, err)
|
||||||
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
59
cmd/frontend/pdf.go
Normal file
59
cmd/frontend/pdf.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package frontend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
b "streifling.com/jason/cpolis/cmd/backend"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UploadPDF(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
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.ParseMultipartForm(10 << 20); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file, _, err := r.FormFile("pdf-upload")
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
filename := fmt.Sprint(uuid.New(), ".pdf")
|
||||||
|
absFilepath, err := filepath.Abs(fmt.Sprint(c.PDFDir, "/", filename))
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pdf, err := os.Create(absFilepath)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer pdf.Close()
|
||||||
|
|
||||||
|
if _, err = io.Copy(pdf, file); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
@ -60,7 +60,7 @@ func Login(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
|
|||||||
|
|
||||||
id, ok := db.GetID(userName)
|
id, ok := db.GetID(userName)
|
||||||
if !ok {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,7 +96,6 @@ func Logout(c *b.Config, s *b.CookieStore) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
session.Options.MaxAge = -1
|
session.Options.MaxAge = -1
|
||||||
|
|
||||||
if err = session.Save(r, w); err != nil {
|
if err = session.Save(r, w); err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
@ -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 {
|
func AddFirstUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if _, err := getSession(w, r, c, s); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
htmlData := UserData{
|
htmlData := UserData{
|
||||||
User: &b.User{
|
User: &b.User{
|
||||||
@ -281,12 +277,12 @@ func ShowAllUsers(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.H
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type htmlData struct {
|
data := new(struct {
|
||||||
Users map[int64]*b.User
|
Users map[int64]*b.User
|
||||||
Action string
|
Action string
|
||||||
}
|
})
|
||||||
|
|
||||||
data := &htmlData{Action: action}
|
data.Action = action
|
||||||
data.Users, err = db.GetAllUsers()
|
data.Users, err = db.GetAllUsers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
|
68
cmd/main.go
68
cmd/main.go
@ -49,41 +49,49 @@ func main() {
|
|||||||
http.FileServer(http.Dir(config.WebDir+"/static/"))))
|
http.FileServer(http.Dir(config.WebDir+"/static/"))))
|
||||||
mux.HandleFunc("/", f.HomePage(config, db, store))
|
mux.HandleFunc("/", f.HomePage(config, db, store))
|
||||||
|
|
||||||
mux.HandleFunc("GET /create-tag", f.CreateTag(config, store))
|
mux.HandleFunc("GET /article/allow-edit/{id}", f.AllowEditArticle(config, db, store))
|
||||||
mux.HandleFunc("GET /create-user", f.CreateUser(config, store))
|
mux.HandleFunc("GET /article/all-published/review-edit", f.ShowPublishedArticles(config, db, store, "review-edit"))
|
||||||
mux.HandleFunc("GET /delete-article/{id}", f.DeleteArticle(config, db, store))
|
mux.HandleFunc("GET /article/all-published/delete", f.ShowPublishedArticles(config, db, store, "review-delete"))
|
||||||
mux.HandleFunc("GET /delete-user/{id}", f.DeleteUser(config, db, store))
|
mux.HandleFunc("GET /article/all-rejected", f.ShowRejectedArticles(config, db, store))
|
||||||
mux.HandleFunc("GET /edit-self", f.EditSelf(config, db, store))
|
mux.HandleFunc("GET /article/all-unpublished-unrejected-and-published-rejected", f.ShowUnpublishedUnrejectedAndPublishedRejectedArticles(config, db, store))
|
||||||
mux.HandleFunc("GET /edit-user/{id}", f.EditUser(config, db, store))
|
mux.HandleFunc("GET /article/delete/{id}", f.DeleteArticle(config, db, store))
|
||||||
|
mux.HandleFunc("GET /article/edit/{id}", f.EditArticle(config, db, store))
|
||||||
|
mux.HandleFunc("GET /article/publish/{id}", f.PublishArticle(config, db, store))
|
||||||
|
mux.HandleFunc("GET /article/reject/{id}", f.RejectArticle(config, db, store))
|
||||||
|
mux.HandleFunc("GET /article/review-delete/{id}", f.ReviewArticle(config, db, store, "delete", "Artikel löschen", "Löschen"))
|
||||||
|
mux.HandleFunc("GET /article/review-edit/{id}", f.ReviewArticle(config, db, store, "allow-edit", "Artikel bearbeiten", "Bearbeiten erlauben"))
|
||||||
|
mux.HandleFunc("GET /article/review-rejected/{id}", f.ReviewRejectedArticle(config, db, store))
|
||||||
|
mux.HandleFunc("GET /article/review-unpublished/{id}", f.ReviewArticle(config, db, store, "publish", "Artikel veröffentlichen", "Veröffentlichen"))
|
||||||
|
mux.HandleFunc("GET /article/save/{id}", f.SaveArticle(config, db, store))
|
||||||
|
mux.HandleFunc("GET /article/serve/{id}", c.ServeArticle(config, db))
|
||||||
|
mux.HandleFunc("GET /article/write", f.WriteArticle(config, db, store))
|
||||||
mux.HandleFunc("GET /hub", f.ShowHub(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/this", f.ShowCurrentArticles(config, db, store))
|
||||||
mux.HandleFunc("GET /logout", f.Logout(config, store))
|
mux.HandleFunc("GET /logout", f.Logout(config, store))
|
||||||
mux.HandleFunc("GET /pdf/get-list", c.ServePDFList(config))
|
mux.HandleFunc("GET /pdf/get-list", c.ServePDFList(config))
|
||||||
mux.HandleFunc("GET /pdf/{id}", c.ServePDF(config))
|
mux.HandleFunc("GET /pdf/serve/{id}", c.ServePDF(config))
|
||||||
mux.HandleFunc("GET /pics/{pic}", f.ServeImage(config, store))
|
mux.HandleFunc("GET /rss/serve", c.ServeRSS(config))
|
||||||
mux.HandleFunc("GET /publish-article/{id}", f.PublishArticle(config, db, store))
|
mux.HandleFunc("GET /tag/create", f.CreateTag(config, store))
|
||||||
mux.HandleFunc("GET /publish-issue", f.PublishLatestIssue(config, db, store))
|
mux.HandleFunc("GET /user/create", f.CreateUser(config, store))
|
||||||
mux.HandleFunc("GET /published-articles", f.ShowPublishedArticles(config, db, store))
|
mux.HandleFunc("GET /user/delete/{id}", f.DeleteUser(config, db, store))
|
||||||
mux.HandleFunc("GET /reject-article/{id}", f.RejectArticle(config, db, store))
|
mux.HandleFunc("GET /user/edit/{id}", f.EditUser(config, db, store))
|
||||||
mux.HandleFunc("GET /rejected-articles", f.ShowRejectedArticles(config, db, store))
|
mux.HandleFunc("GET /user/edit/self", f.EditSelf(config, db, store))
|
||||||
mux.HandleFunc("GET /review-article-for-deletion/{id}", f.ReviewArticleForDeletion(config, db, store))
|
mux.HandleFunc("GET /user/show-all/delete", f.ShowAllUsers(config, db, store, "delete"))
|
||||||
mux.HandleFunc("GET /review-rejected-article/{id}", f.ReviewRejectedArticle(config, db, store))
|
mux.HandleFunc("GET /user/show-all/edit", f.ShowAllUsers(config, db, store, "edit"))
|
||||||
mux.HandleFunc("GET /review-unpublished-article/{id}", f.ReviewUnpublishedArticle(config, db, store))
|
|
||||||
mux.HandleFunc("GET /rss", c.ServeRSS(config))
|
|
||||||
mux.HandleFunc("GET /show-all-users-edit", f.ShowAllUsers(config, db, store, "edit-user"))
|
|
||||||
mux.HandleFunc("GET /show-all-users-delete", f.ShowAllUsers(config, db, store, "delete-user"))
|
|
||||||
mux.HandleFunc("GET /this-issue", f.ShowCurrentArticles(config, db, store))
|
|
||||||
mux.HandleFunc("GET /unpublished-articles", f.ShowUnpublishedArticles(config, db, store))
|
|
||||||
mux.HandleFunc("GET /write-article", f.WriteArticle(config, db, store))
|
|
||||||
|
|
||||||
mux.HandleFunc("POST /add-first-user", f.AddFirstUser(config, db, store))
|
mux.HandleFunc("POST /article/resubmit/{id}", f.ResubmitArticle(config, db, store))
|
||||||
mux.HandleFunc("POST /add-tag", f.AddTag(config, db, store))
|
mux.HandleFunc("POST /article/submit", f.SubmitArticle(config, db, store))
|
||||||
mux.HandleFunc("POST /add-user", f.AddUser(config, db, 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 /login", f.Login(config, db, store))
|
||||||
mux.HandleFunc("POST /resubmit-article/{id}", f.ResubmitArticle(config, db, store))
|
mux.HandleFunc("POST /pdf/upload", f.UploadPDF(config, store))
|
||||||
mux.HandleFunc("POST /submit-article", f.SubmitArticle(config, db, store))
|
mux.HandleFunc("POST /tag/add", f.AddTag(config, db, store))
|
||||||
mux.HandleFunc("POST /update-self", f.UpdateSelf(config, db, store))
|
mux.HandleFunc("POST /user/add", f.AddUser(config, db, store))
|
||||||
mux.HandleFunc("POST /update-user/{id}", f.UpdateUser(config, db, store))
|
mux.HandleFunc("POST /user/add-first", f.AddFirstUser(config, db, store))
|
||||||
mux.HandleFunc("POST /upload-image", f.UploadImage(config, store))
|
mux.HandleFunc("POST /user/update/{id}", f.UpdateUser(config, db, store))
|
||||||
|
mux.HandleFunc("POST /user/update/self", f.UpdateSelf(config, db, store))
|
||||||
|
|
||||||
log.Fatalln(http.ListenAndServe(config.Port, mux))
|
log.Fatalln(http.ListenAndServe(config.Port, mux))
|
||||||
}
|
}
|
||||||
|
@ -11,40 +11,46 @@ CREATE TABLE users (
|
|||||||
first_name VARCHAR(50) NOT NULL,
|
first_name VARCHAR(50) NOT NULL,
|
||||||
last_name VARCHAR(50) NOT NULL,
|
last_name VARCHAR(50) NOT NULL,
|
||||||
role INT NOT NULL,
|
role INT NOT NULL,
|
||||||
PRIMARY KEY(id)
|
PRIMARY KEY (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE issues (
|
CREATE TABLE issues (
|
||||||
id INT AUTO_INCREMENT,
|
id INT AUTO_INCREMENT,
|
||||||
published BOOL NOT NULL,
|
published BOOL NOT NULL,
|
||||||
PRIMARY KEY(id)
|
PRIMARY KEY (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE articles (
|
CREATE TABLE articles (
|
||||||
id INT AUTO_INCREMENT,
|
id INT AUTO_INCREMENT,
|
||||||
title VARCHAR(255) NOT NULL,
|
title VARCHAR(255) NOT NULL,
|
||||||
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
description TEXT NOT NULL,
|
description TEXT NOT NULL,
|
||||||
content TEXT NOT NULL,
|
link VARCHAR(255),
|
||||||
published BOOL NOT NULL,
|
enc_url VARCHAR(255),
|
||||||
rejected BOOL NOT NULL,
|
enc_length INT,
|
||||||
author_id INT NOT NULL,
|
enc_type VARCHAR(255),
|
||||||
issue_id INT NOT NULL,
|
published BOOL NOT NULL,
|
||||||
PRIMARY KEY(id),
|
rejected BOOL NOT NULL,
|
||||||
FOREIGN KEY(author_id) REFERENCES users(id),
|
author_id INT NOT NULL,
|
||||||
FOREIGN KEY(issue_id) REFERENCES issues(id)
|
issue_id INT NOT NULL,
|
||||||
|
edited_id INT,
|
||||||
|
is_in_issue BOOL NOT NULL,
|
||||||
|
auto_generated BOOL NOT NULL,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
FOREIGN KEY (author_id) REFERENCES users (id),
|
||||||
|
FOREIGN KEY (issue_id) REFERENCES issues (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE tags (
|
CREATE TABLE tags (
|
||||||
id INT AUTO_INCREMENT,
|
id INT AUTO_INCREMENT,
|
||||||
name VARCHAR(50) NOT NULL UNIQUE,
|
name VARCHAR(50) NOT NULL UNIQUE,
|
||||||
PRIMARY KEY(id)
|
PRIMARY KEY (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE articles_tags (
|
CREATE TABLE articles_tags (
|
||||||
article_id INT,
|
article_id INT,
|
||||||
tag_id INT,
|
tag_id INT,
|
||||||
PRIMARY KEY(article_id, tag_id),
|
PRIMARY KEY (article_id, tag_id),
|
||||||
FOREIGN KEY(article_id) REFERENCES articles(id),
|
FOREIGN KEY (article_id) REFERENCES articles (id),
|
||||||
FOREIGN KEY(tag_id) REFERENCES tags(id)
|
FOREIGN KEY (tag_id) REFERENCES tags (id)
|
||||||
);
|
);
|
||||||
|
2
go.mod
2
go.mod
@ -4,7 +4,7 @@ go 1.22.0
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
firebase.google.com/go/v4 v4.14.1
|
firebase.google.com/go/v4 v4.14.1
|
||||||
git.streifling.com/jason/rss v0.1.2
|
git.streifling.com/jason/rss v0.1.3
|
||||||
github.com/BurntSushi/toml v1.3.2
|
github.com/BurntSushi/toml v1.3.2
|
||||||
github.com/go-sql-driver/mysql v1.7.1
|
github.com/go-sql-driver/mysql v1.7.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
|
4
go.sum
4
go.sum
@ -15,8 +15,8 @@ cloud.google.com/go/storage v1.40.0 h1:VEpDQV5CJxFmJ6ueWNsKxcr1QAYOXEgxDa+sBbJah
|
|||||||
cloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g=
|
cloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g=
|
||||||
firebase.google.com/go/v4 v4.14.1 h1:4qiUETaFRWoFGE1XP5VbcEdtPX93Qs+8B/7KvP2825g=
|
firebase.google.com/go/v4 v4.14.1 h1:4qiUETaFRWoFGE1XP5VbcEdtPX93Qs+8B/7KvP2825g=
|
||||||
firebase.google.com/go/v4 v4.14.1/go.mod h1:fgk2XshgNDEKaioKco+AouiegSI9oTWVqRaBdTTGBoM=
|
firebase.google.com/go/v4 v4.14.1/go.mod h1:fgk2XshgNDEKaioKco+AouiegSI9oTWVqRaBdTTGBoM=
|
||||||
git.streifling.com/jason/rss v0.1.2 h1:UB3UHJXMt5WDDh9y8n0Z6nS1XortbPXjEr7QZTdovY4=
|
git.streifling.com/jason/rss v0.1.3 h1:fd3j4ZtcLehapcmmroo3AP3X34gRHC4xzpfV6bDV1ZU=
|
||||||
git.streifling.com/jason/rss v0.1.2/go.mod h1:gpZF0nZbQSstMpyHD9DTAvlQEG7v4pjO5c7aIMWM4Jg=
|
git.streifling.com/jason/rss v0.1.3/go.mod h1:gpZF0nZbQSstMpyHD9DTAvlQEG7v4pjO5c7aIMWM4Jg=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
|
@ -7,4 +7,5 @@ module.exports = {
|
|||||||
plugins: [
|
plugins: [
|
||||||
require('@tailwindcss/typography')
|
require('@tailwindcss/typography')
|
||||||
],
|
],
|
||||||
|
darkMode: 'selector',
|
||||||
}
|
}
|
||||||
|
@ -43,13 +43,15 @@ chmod +x $CPOLIS_DIR/tailwindcss
|
|||||||
$CPOLIS_DIR/tailwindcss -i $CPOLIS_DIR/web/static/css/input.css -o $CPOLIS_DIR/web/static/css/style.css
|
$CPOLIS_DIR/tailwindcss -i $CPOLIS_DIR/web/static/css/input.css -o $CPOLIS_DIR/web/static/css/style.css
|
||||||
|
|
||||||
echo '\nBuilding cpolis...' >&2
|
echo '\nBuilding cpolis...' >&2
|
||||||
go build -o $TMP_DIR/cpolis $CPOLIS_DIR/cmd/main.go
|
cd $CPOLIS_DIR
|
||||||
|
go build -o $BIN_DIR/cpolis cmd/main.go
|
||||||
|
cd
|
||||||
|
|
||||||
echo '\nSetting system files up...' >&2
|
echo '\nSetting up system files...' >&2
|
||||||
sudo mv $TMP_DIR/cpolis $BIN_DIR/cpolis
|
|
||||||
sudo chown root:root $BIN_DIR/cpolis
|
sudo chown root:root $BIN_DIR/cpolis
|
||||||
chmod +x $BIN_DIR/cpolis
|
chmod +x $BIN_DIR/cpolis
|
||||||
|
|
||||||
|
echo '\nSetting up service...' >&2
|
||||||
sudo mv $CPOLIS_DIR/cpolis.service $SYSTEMD_DIR
|
sudo mv $CPOLIS_DIR/cpolis.service $SYSTEMD_DIR
|
||||||
sudo chown root:root $SYSTEMD_DIR/cpolis.service
|
sudo chown root:root $SYSTEMD_DIR/cpolis.service
|
||||||
sudo systemctl daemon-reload
|
sudo systemctl daemon-reload
|
||||||
|
@ -2,40 +2,71 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
body {
|
|
||||||
width: 800px;
|
|
||||||
@apply mx-auto text-slate-900;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
@apply font-bold mb-2 text-2xl;
|
@apply font-bold mb-2 text-2xl;
|
||||||
}
|
}
|
||||||
|
|
||||||
form {
|
h3 {
|
||||||
@apply flex flex-col gap-y-3;
|
@apply font-bold mb-2 text-xl;
|
||||||
}
|
|
||||||
|
|
||||||
input[type="file"] {
|
|
||||||
@apply border rounded-md w-full;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="password"],
|
input[type="password"],
|
||||||
input[type="text"] {
|
input[type="text"] {
|
||||||
@apply border h-8 rounded-md;
|
@apply bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 h-8 rounded-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
@apply border h-32 rounded-md;
|
@apply bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 h-32 rounded-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-area-1 {
|
||||||
|
@apply grid grid-cols-1 gap-x-4 gap-y-1 mt-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-area {
|
.btn-area {
|
||||||
@apply flex gap-4 mt-4;
|
@apply grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-1 mt-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-area-3 {
|
||||||
|
@apply grid grid-cols-1 md:grid-cols-3 gap-x-4 gap-y-1 mt-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
@apply bg-slate-200 border my-2 px-3 py-2 rounded-md w-full hover:bg-slate-100;
|
@apply bg-slate-200 dark:bg-slate-800 hover:bg-slate-100 dark:hover:bg-slate-900 border border-slate-200 dark:border-slate-800 my-2 px-3 py-2 rounded-md w-full;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-btn {
|
.action-btn {
|
||||||
@apply bg-slate-800 border my-2 px-3 py-2 rounded-md text-slate-50 w-full hover:bg-slate-700;
|
@apply bg-slate-800 dark:bg-slate-200 hover:bg-slate-700 dark:hover:bg-slate-300 my-2 px-3 py-2 rounded-md text-slate-50 dark:text-slate-950 w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.EasyMDEContainer .CodeMirror {
|
||||||
|
@apply bg-slate-50 dark:bg-slate-950 border-slate-200 dark:border-slate-800 text-slate-900 dark:text-slate-100
|
||||||
|
}
|
||||||
|
|
||||||
|
.EasyMDEContainer .cm-s-easymde .CodeMirror-cursor {
|
||||||
|
@apply border-slate-900 dark:border-slate-100
|
||||||
|
}
|
||||||
|
|
||||||
|
.EasyMDEContainer .editor-toolbar > * {
|
||||||
|
@apply text-slate-900 dark:text-slate-100
|
||||||
|
}
|
||||||
|
|
||||||
|
.EasyMDEContainer .editor-toolbar > .active, .editor-toolbar > button:hover, .editor-preview pre, .cm-s-easymde .cm-comment {
|
||||||
|
@apply bg-slate-100 dark:bg-slate-900
|
||||||
|
}
|
||||||
|
|
||||||
|
.EasyMDEContainer .CodeMirror-fullscreen {
|
||||||
|
@apply bg-slate-50 dark:bg-slate-950
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar {
|
||||||
|
@apply border border-slate-200 dark:border-slate-800
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar.fullscreen {
|
||||||
|
@apply bg-slate-50 dark:bg-slate-950
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-preview {
|
||||||
|
@apply bg-slate-50 dark:bg-slate-950
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
<h2>Neuer Tag</h2>
|
<h2>Neuer Tag</h2>
|
||||||
|
|
||||||
<form>
|
<form>
|
||||||
<input required name="tag" placeholder="Tag eingeben" type="text" />
|
<input class="w-full" name="tag" placeholder="Tag eingeben" required type="text" />
|
||||||
<div class="btn-area">
|
<div class="btn-area">
|
||||||
<input class="action-btn" type="submit" value="Anlegen" hx-post="/add-tag" hx-target="#page-content" />
|
<input class="action-btn" type="submit" value="Anlegen" hx-post="/tag/add" hx-target="#page-content" />
|
||||||
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
|
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<h2>Neuer Benutzer</h2>
|
<h2>Neuer Benutzer</h2>
|
||||||
|
|
||||||
<form>
|
<form>
|
||||||
<div class="grid grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="username">Benutzername</label>
|
<label for="username">Benutzername</label>
|
||||||
<input class="w-full" required name="username" type="text" value="{{.UserName}}" />
|
<input class="w-full" required name="username" type="text" value="{{.UserName}}" />
|
||||||
@ -25,7 +25,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-4">
|
<div class="flex flex-wrap gap-4">
|
||||||
<div>
|
<div>
|
||||||
<input required id="author" name="role" type="radio" value="3" {{if eq .Role 3 }}checked{{end}} />
|
<input required id="author" name="role" type="radio" value="3" {{if eq .Role 3 }}checked{{end}} />
|
||||||
<label for="author">Autor</label>
|
<label for="author">Autor</label>
|
||||||
@ -45,7 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn-area">
|
<div class="btn-area">
|
||||||
<input class="action-btn" type="submit" value="Anlegen" hx-post="/add-user" hx-target="#page-content" />
|
<input class="action-btn" type="submit" value="Anlegen" hx-post="/user/add" hx-target="#page-content" />
|
||||||
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
|
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,17 +1,73 @@
|
|||||||
{{define "page-content"}}
|
{{define "page-content"}}
|
||||||
<h2>Aktuelle Artikel</h2>
|
<h2>Diese Ausgabe</h2>
|
||||||
|
|
||||||
<div class="flex flex-col gap-4">
|
<form hx-encoding="multipart/form-data">
|
||||||
{{range .}}
|
<div class="flex flex-col gap-4">
|
||||||
<div class="border px-2 py-1 rounded-md">
|
<div>
|
||||||
<h1 class="font-bold text-2xl">{{.Title}}</h1>
|
<h3>Aktuelle Artikel</h3>
|
||||||
<p>{{.Description}}</p>
|
<div class="flex flex-col gap-4">
|
||||||
|
{{range .}}
|
||||||
|
<div class="border border-slate-200 dark:border-slate-800 px-2 py-1 rounded-md">
|
||||||
|
<h1 class="font-bold text-2xl">{{.Title}}</h1>
|
||||||
|
<p>{{.Description}}</p>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3>Titelseite</h3>
|
||||||
|
<div class="grid grid-cols-2 gap-4 items-center">
|
||||||
|
<input class="h-full" name="issue-title" placeholder="Titel" required type="text" />
|
||||||
|
<label class="btn text-center" for="image-upload">Bild hochladen</label>
|
||||||
|
<input class="hidden" id="image-upload" name="issue-image" type="file" required
|
||||||
|
hx-post="/issue/upload-image" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3>Über diese Ausgabe</h3>
|
||||||
|
<div>
|
||||||
|
<textarea id="easyMDE" placeholder="Beschreibung dieser Ausgabe"></textarea>
|
||||||
|
<input id="issue-content" name="issue-content" type="hidden" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="btn-area">
|
<div class="btn-area">
|
||||||
<button class="action-btn" hx-get="/publish-issue" hx-target="#page-content">Ausgabe publizieren</button>
|
<button class="action-btn" hx-post="/issue/publish" hx-target="#page-content">Ausgabe publizieren</button>
|
||||||
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
|
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var easyMDE = new EasyMDE({
|
||||||
|
element: document.getElementById('easyMDE'),
|
||||||
|
hideIcons: ['image'],
|
||||||
|
imageTexts: {sbInit: ''},
|
||||||
|
showIcons: ["code", "table", "upload-image"],
|
||||||
|
uploadImage: true,
|
||||||
|
|
||||||
|
imageUploadFunction: function (file, onSuccess, onError) {
|
||||||
|
var formData = new FormData();
|
||||||
|
formData.append('article-image', file);
|
||||||
|
|
||||||
|
fetch('/article/upload-image', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
onSuccess(data);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
onError(error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
easyMDE.codemirror.on("change", () => {
|
||||||
|
document.getElementById('issue-content').value = easyMDE.value();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<h2>Profil bearbeiten</h2>
|
<h2>Profil bearbeiten</h2>
|
||||||
|
|
||||||
<form>
|
<form>
|
||||||
<div class="grid grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="username">Benutzername</label>
|
<label for="username">Benutzername</label>
|
||||||
<input class="w-full" name="username" type="text" value="{{.UserName}}" />
|
<input class="w-full" name="username" type="text" value="{{.UserName}}" />
|
||||||
@ -35,7 +35,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn-area">
|
<div class="btn-area">
|
||||||
<input class="action-btn" type="submit" value="Aktualisieren" hx-post="/update-self"
|
<input class="action-btn" type="submit" value="Aktualisieren" hx-post="/user/update/self"
|
||||||
hx-target="#page-content" />
|
hx-target="#page-content" />
|
||||||
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
|
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<h2>Profil von {{.FirstName}} {{.LastName}} bearbeiten</h2>
|
<h2>Profil von {{.FirstName}} {{.LastName}} bearbeiten</h2>
|
||||||
|
|
||||||
<form>
|
<form>
|
||||||
<div class="grid grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="username">Benutzername</label>
|
<label for="username">Benutzername</label>
|
||||||
<input class="w-full" name="username" type="text" value="{{.UserName}}" />
|
<input class="w-full" name="username" type="text" value="{{.UserName}}" />
|
||||||
@ -29,7 +29,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-4">
|
<div class="flex flex-wrap gap-4">
|
||||||
<div>
|
<div>
|
||||||
<input required id="author" name="role" type="radio" value="3" {{if eq .Role 3 }}checked{{end}} />
|
<input required id="author" name="role" type="radio" value="3" {{if eq .Role 3 }}checked{{end}} />
|
||||||
<label for="author">Autor</label>
|
<label for="author">Autor</label>
|
||||||
@ -49,7 +49,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn-area">
|
<div class="btn-area">
|
||||||
<input class="action-btn" type="submit" value="Aktualisieren" hx-post="/update-user/{{.ID}}"
|
<input class="action-btn" type="submit" value="Aktualisieren" hx-post="/user/update/{{.ID}}"
|
||||||
hx-target="#page-content" />
|
hx-target="#page-content" />
|
||||||
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
|
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,32 +4,41 @@
|
|||||||
<form id="edit-area">
|
<form id="edit-area">
|
||||||
<div class="flex flex-col gap-y-1">
|
<div class="flex flex-col gap-y-1">
|
||||||
<label for="article-title">Titel</label>
|
<label for="article-title">Titel</label>
|
||||||
<input name="article-title" type="text" value="{{.Title}}" />
|
<input name="article-title" type="text" value="{{.Article.Title}}" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
|
||||||
|
<div class="flex flex-col gap-y-1">
|
||||||
<label for="article-description">Beschreibung</label>
|
<label for="article-description">Beschreibung</label>
|
||||||
<textarea name="article-description">{{.Description}}</textarea>
|
<textarea name="article-description">{{.Article.Description}}</textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
|
||||||
<label for="article-content">Artikel</label>
|
<div class="flex flex-col gap-y-1">
|
||||||
|
<label for="easyMDE">Artikel</label>
|
||||||
<textarea id="easyMDE">{{.Content}}</textarea>
|
<textarea id="easyMDE">{{.Content}}</textarea>
|
||||||
|
<input id="article-content" name="article-content" type="hidden" value="{{.Content}}" />
|
||||||
</div>
|
</div>
|
||||||
<input id="article-content" name="article-content" type="hidden" />
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<span>Tags</span>
|
<span>Tags</span>
|
||||||
<div class="flex flex-wrap gap-x-4">
|
<div class="flex flex-wrap gap-x-4">
|
||||||
|
<div>
|
||||||
|
<input id="issue" name="issue" type="checkbox" {{if .Article.IsInIssue}}checked{{end}} />
|
||||||
|
<label for="issue">Orient Express</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{range .Tags}}
|
{{range .Tags}}
|
||||||
<div>
|
<div>
|
||||||
<input id="{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" />
|
<input id="tag-{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" {{if index $.Selected
|
||||||
<label for="{{.Name}}">{{.Name}}</label>
|
.ID}}checked{{end}} />
|
||||||
|
<label for="tag-{{.Name}}">{{.Name}}</label>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn-area">
|
<div class="btn-area">
|
||||||
<input class="action-btn" type="submit" value="Senden" hx-post="/submit-article" hx-target="#page-content" />
|
<input class="action-btn" type="submit" value="Senden" hx-post="/article/{{.Action}}"
|
||||||
|
hx-target="#page-content" />
|
||||||
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
|
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@ -46,7 +55,7 @@
|
|||||||
var formData = new FormData();
|
var formData = new FormData();
|
||||||
formData.append('article-image', file);
|
formData.append('article-image', file);
|
||||||
|
|
||||||
fetch('/upload-image', {
|
fetch('/article/upload-image', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
})
|
})
|
||||||
|
@ -25,8 +25,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn-area">
|
<div class="btn-area-1">
|
||||||
<input class="action-btn" type="submit" value="Anlegen" hx-post="/add-first-user" hx-target="#page-content" />
|
<input class="action-btn" type="submit" value="Anlegen" hx-post="/user/add-first" hx-target="#page-content" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
@ -4,45 +4,46 @@
|
|||||||
|
|
||||||
{{if lt . 4}}
|
{{if lt . 4}}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<h2>Autor</h2>
|
<h2>Artikel</h2>
|
||||||
<div class="grid grid-cols-2 gap-x-4 gap-y-2">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-2">
|
||||||
<button class="btn" hx-get="/write-article" hx-target="#page-content">Artikel schreiben</button>
|
<button class="btn" hx-get="/article/write" hx-target="#page-content">Artikel schreiben</button>
|
||||||
<button class="btn" hx-get="/rejected-articles" hx-target="#page-content">Abgelehnte Artikel</button>
|
<button class="btn" hx-get="/article/all-rejected" hx-target="#page-content">Artikel bearbeiten</button>
|
||||||
<a class="btn text-center" href="/rss">RSS Feed</a>
|
{{if lt . 3}}<button class="btn" hx-get="/article/all-unpublished-unrejected-and-published-rejected"
|
||||||
<button class="btn" hx-get="/edit-self" hx-target="#page-content">Profil bearbeiten</button>
|
hx-target="#page-content">Artikel veröffentlichen</button>{{end}}
|
||||||
</div>
|
{{if lt . 2}}<button class="btn" hx-get="/article/all-published/delete" hx-target="#page-content">Artikel
|
||||||
</div>
|
löschen</button>{{end}}
|
||||||
{{end}}
|
{{if lt . 2}}<button class="btn" hx-get="/article/all-published/review-edit"
|
||||||
|
hx-target="#page-content">Artikel bearbeiten lassen</button>{{end}}
|
||||||
{{if lt . 3}}
|
{{if lt . 3}}<button class="btn" hx-get="/tag/create" hx-target="#page-content">Neuer Tag</button>{{end}}
|
||||||
<div class="mb-3">
|
|
||||||
<h2>Redakteur</h2>
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
<button class="btn" hx-get="/unpublished-articles" hx-target="#page-content">
|
|
||||||
Unveröffentlichte Artikel
|
|
||||||
</button>
|
|
||||||
<button class="btn" hx-get="/create-tag" hx-target="#page-content">Neuer Tag</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if lt . 2}}
|
{{if lt . 2}}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<h2>Herausgeber</h2>
|
<h2>Ausgabe</h2>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-2">
|
||||||
<button class="btn" hx-get="/this-issue" hx-target="#page-content">Diese Ausgabe</button>
|
<button class="btn" hx-get="/issue/this" hx-target="#page-content">Diese Ausgabe</button>
|
||||||
<button class="btn" hx-get="/published-articles" hx-target="#page-content">Artikel löschen</button>
|
<form class="flex" hx-encoding="multipart/form-data">
|
||||||
|
<label class="btn text-center" for="pdf-upload">PDF hochladen</label>
|
||||||
|
<input accept=".pdf" class="hidden" id="pdf-upload" name="pdf-upload" type="file"
|
||||||
|
hx-post="/pdf/upload" />
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{if eq . 0}}
|
{{if lt . 4}}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<h2>Administrator</h2>
|
<h2>Benutzer</h2>
|
||||||
<div class="grid grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-2">
|
||||||
<button class="btn" hx-get="/create-user" hx-target="#page-content">Benutzer hinzufügen</button>
|
<button class="btn" hx-get="/user/edit/self" hx-target="#page-content">Mein Profil bearbeiten</button>
|
||||||
<button class="btn" hx-get="/show-all-users-edit" hx-target="#page-content">Benutzer bearbeiten</button>
|
{{if eq . 0}}<button class="btn" hx-get="/user/create" hx-target="#page-content">Benutzer
|
||||||
<button class="btn" hx-get="/show-all-users-delete" hx-target="#page-content">Benutzer löschen</button>
|
hinzufügen</button>{{end}}
|
||||||
|
{{if eq . 0}}<button class="btn" hx-get="/user/show-all/edit" hx-target="#page-content">Benutzer
|
||||||
|
bearbeiten</button>{{end}}
|
||||||
|
{{if eq . 0}}<button class="btn" hx-get="/user/show-all/delete" hx-target="#page-content">Benutzer
|
||||||
|
löschen</button>{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -5,13 +5,26 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Orient Editor</title>
|
<title>Orient Editor</title>
|
||||||
|
|
||||||
<link href="/web/static/css/style.css" rel="stylesheet">
|
<link href="/web/static/css/style.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="https://unpkg.com/easymde/dist/easymde.min.css">
|
<link rel="stylesheet" href="https://unpkg.com/easymde/dist/easymde.min.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="flex flex-col justify-between min-h-screen bg-slate-50">
|
<body
|
||||||
|
class="bg-slate-50 dark:bg-slate-950 container flex flex-col justify-between max-w-screen-lg min-h-screen mx-auto text-slate-900 dark:text-slate-100">
|
||||||
<header class="my-8">
|
<header class="my-8">
|
||||||
<h1 class="font-bold text-4xl text-center">Orient Editor</h1>
|
<h1 class="font-bold text-4xl text-center">Orient Editor</h1>
|
||||||
|
|
||||||
|
<div class="ml-4">
|
||||||
|
<label class="cursor-pointer flex items-center relative" for="theme-toggle">
|
||||||
|
<div class="bg-slate-200 dark:bg-slate-800 block h-6 w-12 rounded-full"></div>
|
||||||
|
<div
|
||||||
|
class="absolute bg-slate-800 dark:bg-slate-50 dot left-1 top-1 h-4 w-4 rounded-full transform transition">
|
||||||
|
</div>
|
||||||
|
<span class="ml-2">Dunkel</span>
|
||||||
|
</label>
|
||||||
|
<input type="checkbox" id="theme-toggle" class="sr-only">
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="mx-4">
|
<main class="mx-4">
|
||||||
@ -20,14 +33,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="my-8">
|
<footer class="text-center text-gray-500 my-8">
|
||||||
<p class="text-center text-gray-500 dark:text-gray-400">
|
<p>© 2024 Jason Streifling. Alle Rechte vorbehalten.</p>
|
||||||
© 2024 Jason Streifling. Alle Rechte vorbehalten.
|
<p>v0.11.0 - <strong>Alpha: Drastische Änderungen und Fehler vorbehalten.</strong></p>
|
||||||
</p>
|
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="https://unpkg.com/htmx.org@2.0.1"></script>
|
<script src="https://unpkg.com/htmx.org@2.0.2"></script>
|
||||||
<script src="https://unpkg.com/easymde/dist/easymde.min.js"></script>
|
<script src="https://unpkg.com/easymde/dist/easymde.min.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const toggleSwitch = document.getElementById('theme-toggle');
|
||||||
|
const dot = document.querySelector('.dot');
|
||||||
|
|
||||||
|
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||||
|
document.documentElement.classList.add('dark')
|
||||||
|
toggleSwitch.checked = true;
|
||||||
|
dot.classList.add('translate-x-6');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark')
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSwitch.addEventListener('change', () => {
|
||||||
|
if (toggleSwitch.checked) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
localStorage.theme = 'dark';
|
||||||
|
dot.classList.add('translate-x-6');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
localStorage.theme = 'light';
|
||||||
|
dot.classList.remove('translate-x-6');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
@ -2,11 +2,13 @@
|
|||||||
<h2>Anmeldung</h2>
|
<h2>Anmeldung</h2>
|
||||||
|
|
||||||
<form>
|
<form>
|
||||||
<div class="btn-area">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 my-1">
|
||||||
<input class="w-full" name="username" placeholder="Benutzername" type="text" />
|
<input class="w-full" name="username" placeholder="Benutzername" type="text" />
|
||||||
<input class="w-full" name="password" placeholder="Passwort" type="password" />
|
<input class="w-full" name="password" placeholder="Passwort" type="password" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input class="action-btn" type="submit" value="Anmelden" hx-post="/login" hx-target="#page-content" />
|
<div class="mt-2">
|
||||||
|
<input class="action-btn" type="submit" value="Anmelden" hx-post="/login" hx-target="#page-content" />
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -2,12 +2,13 @@
|
|||||||
<h2>Artikel löschen</h2>
|
<h2>Artikel löschen</h2>
|
||||||
|
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
{{range .}}
|
{{range .Articles}}
|
||||||
<button class="btn" hx-get="/review-article-for-deletion/{{.ID}}" hx-target="#page-content">
|
<button class="btn" hx-get="/article/{{$.Action}}/{{.ID}}" hx-target="#page-content">
|
||||||
<h1 class="font-bold text-2xl">{{.Title}}</h1>
|
<h1 class="font-bold text-2xl">{{.Title}}</h1>
|
||||||
<p>{{.Description}}</p>
|
<p>{{.Description}}</p>
|
||||||
</button>
|
</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<button class="action-btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
|
<button class="action-btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
{{define "page-content"}}
|
{{define "page-content"}}
|
||||||
<h2>Abgelehnte Artikel</h2>
|
<h2>Artikel bearbeiten</h2>
|
||||||
|
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
{{range .RejectedArticles}}
|
{{range .RejectedArticles}}
|
||||||
{{if index $.MyIDs .ID}}
|
{{if index $.MyIDs .ID}}
|
||||||
<button class="btn" hx-get="/review-rejected-article/{{.ID}}" hx-target="#page-content">
|
<button class="btn" hx-get="/article/review-rejected/{{.ID}}" hx-target="#page-content">
|
||||||
<h1 class="font-bold text-2xl">{{.Title}}</h1>
|
<h1 class="font-bold text-2xl">{{.Title}}</h1>
|
||||||
<p>{{.Description}}</p>
|
<p>{{.Description}}</p>
|
||||||
</button>
|
</button>
|
||||||
|
50
web/templates/review-article.html
Normal file
50
web/templates/review-article.html
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
{{define "page-content"}}
|
||||||
|
<h2>{{.ActionTitle}}</h2>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span>Titel</span>
|
||||||
|
<div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full">
|
||||||
|
{{.Article.Title}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span>Beschreibung</span>
|
||||||
|
<div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full">
|
||||||
|
{{.Article.Description}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span>Artikel</span>
|
||||||
|
<div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full">
|
||||||
|
<div class="prose text-slate-900 dark:text-slate-100">
|
||||||
|
{{.HTMLContent}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span>Tags</span>
|
||||||
|
<div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full">
|
||||||
|
{{if .Article.IsInIssue}}
|
||||||
|
<span>Orient Express</span>
|
||||||
|
<br>
|
||||||
|
{{end}}
|
||||||
|
{{range .Tags}}
|
||||||
|
<span>{{.Name}}</span>
|
||||||
|
<br>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{if eq .Action "publish"}}
|
||||||
|
<div class="btn-area-3">
|
||||||
|
<input class="action-btn" type="submit" value="{{.ActionButton}}" hx-get="/article/{{.Action}}/{{.Article.ID}}"
|
||||||
|
hx-target="#page-content" />
|
||||||
|
<input class="btn" type="submit" value="Ablehnen" hx-get="/article/reject/{{.Article.ID}}"
|
||||||
|
hx-target="#page-content" />
|
||||||
|
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="btn-area">
|
||||||
|
<input class="action-btn" type="submit" value="{{.ActionButton}}" hx-get="/article/{{.Action}}/{{.Article.ID}}"
|
||||||
|
hx-target="#page-content" />
|
||||||
|
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
@ -1,69 +0,0 @@
|
|||||||
{{define "page-content"}}
|
|
||||||
<h2>Editor</h2>
|
|
||||||
|
|
||||||
<form>
|
|
||||||
<div class="flex flex-col gap-y-1">
|
|
||||||
<label for="article-title">Titel</label>
|
|
||||||
<input name="article-title" type="text" value="{{.Article.Title}}" />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<label for="article-description">Beschreibung</label>
|
|
||||||
<textarea name="article-description">{{.Article.Description}}</textarea>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<label for="article-content">Artikel</label>
|
|
||||||
<textarea name="article-content" placeholder="Artikel">{{.Article.Content}}</textarea>
|
|
||||||
</div>
|
|
||||||
<input id="article-content" name="article-content" type="hidden" />
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<span>Tags</span>
|
|
||||||
<div class="flex flex-wrap gap-x-4">
|
|
||||||
{{range .Tags}}
|
|
||||||
<div>
|
|
||||||
<input id="tag-{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" {{if index $.Selected
|
|
||||||
.ID}}checked{{end}} />
|
|
||||||
<label for="tag-{{.Name}}">{{.Name}}</label>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="btn-area">
|
|
||||||
<input class="action-btn" type="submit" value="Senden" hx-post="/resubmit-article/{{.Article.ID}}"
|
|
||||||
hx-target="#page-content" />
|
|
||||||
<button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
var easyMDE = new EasyMDE({
|
|
||||||
element: document.getElementById('easyMDE'),
|
|
||||||
hideIcons: ['image'],
|
|
||||||
imageTexts: {sbInit: ''},
|
|
||||||
showIcons: ["code", "table", "upload-image"],
|
|
||||||
uploadImage: true,
|
|
||||||
|
|
||||||
imageUploadFunction: function (file, onSuccess, onError) {
|
|
||||||
var formData = new FormData();
|
|
||||||
formData.append('article-image', file);
|
|
||||||
|
|
||||||
fetch('/upload-image', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
onSuccess(data);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
onError(error);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
easyMDE.codemirror.on("change", () => {
|
|
||||||
document.getElementById('article-content').value = easyMDE.value();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{{end}}
|
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
{{range .Users}}
|
{{range .Users}}
|
||||||
<button class="btn" hx-get="/{{$.Action}}/{{.ID}}" hx-target="#page-content">
|
<button class="btn" hx-get="/user/{{$.Action}}/{{.ID}}" hx-target="#page-content">
|
||||||
<h1 class="font-bold text-2xl">
|
<h1 class="font-bold text-2xl">
|
||||||
{{.UserName}}
|
{{.UserName}}
|
||||||
({{if eq .Role 0}}
|
({{if eq .Role 0}}
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
{{define "page-content"}}
|
|
||||||
<h2>Artikel löschen</h2>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<span>Titel</span>
|
|
||||||
<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
|
|
||||||
{{.Title}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span>Beschreibung</span>
|
|
||||||
<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
|
|
||||||
{{.Description}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span>Artikel</span>
|
|
||||||
<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
|
|
||||||
<div class="prose">
|
|
||||||
{{.Content}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span>Tags</span>
|
|
||||||
<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
|
|
||||||
{{range .Tags}}
|
|
||||||
{{.Name}}
|
|
||||||
<br>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="btn-area">
|
|
||||||
<input class="action-btn" type="submit" value="Löschen" hx-get="/delete-article/{{.ID}}"
|
|
||||||
hx-target="#page-content" />
|
|
||||||
<button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
@ -1,37 +0,0 @@
|
|||||||
{{define "page-content"}}
|
|
||||||
<h2>Artikel veröffentlichen</h2>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<span>Titel</span>
|
|
||||||
<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
|
|
||||||
{{.Title}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span>Beschreibung</span>
|
|
||||||
<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
|
|
||||||
{{.Description}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span>Artikel</span>
|
|
||||||
<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
|
|
||||||
<div class="prose">
|
|
||||||
{{.Content}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span>Tags</span>
|
|
||||||
<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
|
|
||||||
{{range .Tags}}
|
|
||||||
{{.Name}}
|
|
||||||
<br>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="btn-area">
|
|
||||||
<input class="action-btn" type="submit" value="Veröffentlichen" hx-get="/publish-article/{{.ID}}"
|
|
||||||
hx-target="#page-content" />
|
|
||||||
<input class="btn" type="submit" value="Ablehnen" hx-get="/reject-article/{{.ID}}" hx-target="#page-content" />
|
|
||||||
<button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
@ -1,9 +1,9 @@
|
|||||||
{{define "page-content"}}
|
{{define "page-content"}}
|
||||||
<h2>Unveröffentlichte Artikel</h2>
|
<h2>Artikel veröffentlichen</h2>
|
||||||
|
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
{{range .}}
|
{{range .}}
|
||||||
<button class="btn" hx-get="/review-unpublished-article/{{.ID}}" hx-target="#page-content">
|
<button class="btn" hx-get="/article/review-unpublished/{{.ID}}" hx-target="#page-content">
|
||||||
<h1 class="font-bold text-2xl">{{.Title}}</h1>
|
<h1 class="font-bold text-2xl">{{.Title}}</h1>
|
||||||
<p>{{.Description}}</p>
|
<p>{{.Description}}</p>
|
||||||
</button>
|
</button>
|
||||||
|
Reference in New Issue
Block a user