Compare commits

..

7 Commits

46 changed files with 660 additions and 1967 deletions

View File

@ -4,20 +4,15 @@ 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",
"-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"
] ]
bin = "./tmp/main" bin = "./tmp/main"
cmd = "go build -o ./tmp/main ./cmd/main.go" cmd = "go build -o ./tmp/main ./cmd/main.go"

View File

@ -1,18 +1,3 @@
# cpolis # cpolis
cpolis is an application written in Go to serve as the backend of the Orient cpolis is an application written in Go to serve as the backend of the Orient Express magazine.
Express magazine.
## Installation
You should have the following packages installed:
- Go >= 1.22
- MariaDB
Enable and start the MariaDB service.
sudo systemctl enable --now mariadb.service
Set up a dedicated MariaDB user for cpolis.

View File

@ -9,21 +9,15 @@ import (
) )
type Article struct { type Article struct {
Created time.Time
Title string Title string
Created time.Time
Description string Description string
Link string Content string
EncURL string Published bool
EncType string Rejected bool
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) {
@ -32,9 +26,8 @@ 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, link, enc_url, enc_length, enc_type, published, (title, description, content, published, rejected, author_id, issue_id)
rejected, author_id, issue_id, edited_id, is_in_issue, auto_generated) VALUES (?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
` `
for i := 0; i < TxMaxRetries; i++ { for i := 0; i < TxMaxRetries; i++ {
@ -51,9 +44,8 @@ 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, a.Link, result, err := tx.Exec(insertQuery, a.Title, a.Description,
a.EncURL, a.EncLength, a.EncType, a.Published, a.Rejected, a.AuthorID, id, a.Content, 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)
@ -87,9 +79,7 @@ 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 SELECT title, created, description, content, published, author_id
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 = ?
` `
@ -100,9 +90,7 @@ 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.Link, &article.EncURL, &article.EncLength, &article.EncType, &article.Content, &article.Published, &article.AuthorID); err != nil {
&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)
} }
@ -115,15 +103,14 @@ func (db *DB) GetArticle(id int64) (*Article, error) {
return article, nil return article, nil
} }
func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, error) { func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) {
query := fmt.Sprintf(` query := `
SELECT SELECT id, title, created, description, content, author_id, issue_id
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 %s = ? WHERE published = ?
`, attribute) AND rejected = ?
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)
} }
@ -134,12 +121,12 @@ func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, erro
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.Link, &article.EncURL, &article.EncLength, &article.Description, &article.Content, &article.AuthorID,
&article.EncType, &article.AuthorID, &article.IssueID, &article.Published, &article.IssueID); err != nil {
&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)
@ -156,11 +143,9 @@ 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 SELECT id, title, created, description, content, author_id
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 AND is_in_issue = true WHERE issue_id = ? AND published = true
` `
for i := 0; i < TxMaxRetries; i++ { for i := 0; i < TxMaxRetries; i++ {
@ -192,8 +177,7 @@ 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.Link, &article.EncURL, &article.EncLength, &article.Description, &article.Content, &article.AuthorID); err != nil {
&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)
} }
@ -272,20 +256,3 @@ func (db *DB) AddArticleToCurrentIssue(id int64) error {
return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries) return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries)
} }
func (db *DB) DeleteArticle(id int64) error {
articlesTagsQuery := "DELETE FROM articles_tags WHERE article_id = ?"
articlesQuery := "DELETE FROM articles WHERE id = ?"
_, err := db.Exec(articlesTagsQuery, id)
if err != nil {
return fmt.Errorf("error deleting article %v from DB: %v", id, err)
}
_, err = db.Exec(articlesQuery, id)
if err != nil {
return fmt.Errorf("error deleting article %v from DB: %v", id, err)
}
return nil
}

View File

@ -3,7 +3,6 @@ package backend
import ( import (
"flag" "flag"
"fmt" "fmt"
"io/fs"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -12,232 +11,106 @@ import (
) )
type Config struct { type Config struct {
ArticleDir string
ConfigFile string
DBName string DBName string
Description string Description string
Domain string Domain string
FirebaseKey string
KeyFile string KeyFile string
Link string Link string
LogFile string LogFile string
PDFDir string
PicsDir string PicsDir string
Port string Port string
RSSFile string RSSFile string
Title string Title string
WebDir string WebDir string
MaxImgHeight int
MaxImgWidth int
} }
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",
KeyFile: "/var/www/cpolis/cpolis.key", KeyFile: "/var/www/cpolis/cpolis.key",
LogFile: "/var/log/cpolis.log", LogFile: "/var/log/cpolis.log",
MaxImgHeight: 1080,
MaxImgWidth: 1920,
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 mkDir(path string, perm fs.FileMode) (string, error) { func (c *Config) readFile() error {
var err error cfgFile, err := filepath.Abs(os.Getenv("HOME") + "/.config/cpolis/config.toml")
stringSlice := strings.Split(path, "/")
name := stringSlice[len(stringSlice)-1]
path, err = filepath.Abs(path)
if err != nil { if err != nil {
return "", fmt.Errorf("error finding absolute path for %v directory: %v", name, err) return fmt.Errorf("error getting absolute path for config file: %v", err)
}
if err = os.MkdirAll(path, perm); err != nil {
return "", fmt.Errorf("error creating %v directory: %v", name, err)
} }
return path, nil _, err = os.Stat(cfgFile)
}
func mkFile(path string, filePerm, dirPerm fs.FileMode) (string, error) {
var err error
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) { if os.IsNotExist(err) {
dir := strings.Join(stringSlice[:len(stringSlice)-1], "/") fileStrings := strings.Split(cfgFile, "/")
if err = os.MkdirAll(dir, dirPerm); err != nil {
return "", fmt.Errorf("error creating %v: %v", dir, err) dir := strings.Join(fileStrings[0:len(fileStrings)-1], "/")
if err = os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("error creating config directory: %v", err)
} }
fileName := stringSlice[len(stringSlice)-1] fileName := fileStrings[len(fileStrings)-1]
file, err := os.Create(dir + "/" + fileName) file, err := os.Create(dir + "/" + fileName)
if err != nil { if err != nil {
return "", fmt.Errorf("error creating %v: %v", fileName, err) return fmt.Errorf("error creating config file: %v", err)
} }
defer file.Close() defer file.Close()
if err = file.Chmod(filePerm); err != nil { if err = file.Chmod(0644); err != nil {
return "", fmt.Errorf("error setting permissions for %v: %v", fileName, err) return fmt.Errorf("error setting permissions for config file: %v", err)
} }
} } else {
_, err = toml.DecodeFile(cfgFile, c)
return path, nil
}
func (c *Config) handleCliArgs() error {
var port int
var err error
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.Description, "desc", c.Description, "channel description")
flag.StringVar(&c.Domain, "domain", c.Domain, "domain name")
flag.StringVar(&c.FirebaseKey, "firebase", c.FirebaseKey, "Firebase service account key file")
flag.StringVar(&c.KeyFile, "key", c.KeyFile, "key file")
flag.StringVar(&c.Link, "link", c.Link, "channel Link")
flag.StringVar(&c.LogFile, "log", c.LogFile, "log file")
flag.StringVar(&c.PDFDir, "pdfs", c.PDFDir, "pdf directory")
flag.StringVar(&c.PicsDir, "pics", c.PicsDir, "pictures directory")
flag.StringVar(&c.RSSFile, "rss", c.RSSFile, "RSS file")
flag.StringVar(&c.Title, "title", c.Title, "channel title")
flag.StringVar(&c.WebDir, "web", c.WebDir, "web directory")
flag.IntVar(&c.MaxImgHeight, "height", c.MaxImgHeight, "maximum image height")
flag.IntVar(&c.MaxImgWidth, "width", c.MaxImgWidth, "maximum image width")
flag.IntVar(&port, "port", port, "port")
flag.Parse()
if port != 0 {
c.Port = fmt.Sprint(":", port)
}
c.ConfigFile, err = mkFile(c.ConfigFile, 0600, 0700)
if err != nil {
return fmt.Errorf("error setting up file: %v", err)
}
return nil
}
func (c *Config) handleFile(configFile string) error {
_, err := toml.DecodeFile(configFile, c)
if err != nil { if err != nil {
return fmt.Errorf("error reading config file: %v", err) return fmt.Errorf("error reading config file: %v", err)
} }
}
return nil return nil
} }
func (c *Config) setupConfig(cliConfig *Config) error { func (c *Config) handleCliArgs() error {
var err error var err error
defaultConfig := newConfig() port := 8080
if cliConfig.ArticleDir != defaultConfig.ArticleDir { flag.StringVar(&c.DBName, "db", c.DBName, "DB name")
c.ArticleDir = cliConfig.ArticleDir flag.StringVar(&c.Description, "desc", c.Description, "Channel description")
} flag.StringVar(&c.Domain, "domain", c.Domain, "domain name")
c.ArticleDir, err = mkDir(c.ArticleDir, 0700) flag.StringVar(&c.KeyFile, "key", c.KeyFile, "key file")
flag.StringVar(&c.Link, "link", c.Link, "Channel Link")
flag.StringVar(&c.LogFile, "log", c.LogFile, "log file")
flag.StringVar(&c.PicsDir, "pics", c.PicsDir, "pictures directory")
flag.StringVar(&c.RSSFile, "rss", c.RSSFile, "RSS file")
flag.StringVar(&c.Title, "title", c.Title, "Channel title")
flag.StringVar(&c.WebDir, "web", c.WebDir, "web directory")
flag.IntVar(&port, "port", port, "port")
flag.Parse()
c.KeyFile, err = filepath.Abs(c.KeyFile)
if err != nil { if err != nil {
return fmt.Errorf("error setting up directory: %v", err) return fmt.Errorf("error finding absolute path for key file: %v", err)
} }
if cliConfig.DBName != defaultConfig.DBName { c.LogFile, err = filepath.Abs(c.LogFile)
c.DBName = cliConfig.DBName
}
if cliConfig.Description != defaultConfig.Description {
c.Description = cliConfig.Description
}
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 setting up file: %v", err) return fmt.Errorf("error finding absolute path for log file: %v", err)
} }
if cliConfig.KeyFile != defaultConfig.KeyFile { c.PicsDir, err = filepath.Abs(c.PicsDir)
c.KeyFile = cliConfig.KeyFile
}
c.KeyFile, err = mkFile(c.KeyFile, 0600, 0700)
if err != nil { if err != nil {
return fmt.Errorf("error setting up file: %v", err) return fmt.Errorf("error finding absolute path for pics dir: %v", err)
} }
if cliConfig.Link != defaultConfig.Link { c.Port = fmt.Sprint(":", port)
c.Link = cliConfig.Link
}
if cliConfig.LogFile != defaultConfig.LogFile { c.RSSFile, err = filepath.Abs(c.RSSFile)
c.LogFile = cliConfig.LogFile
}
c.LogFile, err = mkFile(c.LogFile, 0600, 0700)
if err != nil { if err != nil {
return fmt.Errorf("error setting up file: %v", err) return fmt.Errorf("error finding absolute path for RSS file: %v", err)
} }
if cliConfig.MaxImgHeight != defaultConfig.MaxImgHeight { c.WebDir, err = filepath.Abs(c.WebDir)
c.MaxImgHeight = cliConfig.MaxImgHeight
}
if cliConfig.MaxImgWidth != defaultConfig.MaxImgWidth {
c.MaxImgWidth = cliConfig.MaxImgWidth
}
if cliConfig.PDFDir != defaultConfig.PDFDir {
c.PDFDir = cliConfig.PDFDir
}
c.PDFDir, err = mkDir(c.PDFDir, 0700)
if err != nil { if err != nil {
return fmt.Errorf("error setting up directory: %v", err) return fmt.Errorf("error finding absolute path for web dir: %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
@ -245,20 +118,14 @@ func (c *Config) setupConfig(cliConfig *Config) error {
func HandleConfig() (*Config, error) { func HandleConfig() (*Config, error) {
config := newConfig() config := newConfig()
cliConfig := newConfig()
if err := cliConfig.handleCliArgs(); err != nil { if err := config.readFile(); err != nil {
return nil, fmt.Errorf("error reading config file: %v", err)
}
if err := config.handleCliArgs(); err != nil {
return nil, fmt.Errorf("error handling cli arguments: %v", err) return nil, fmt.Errorf("error handling cli arguments: %v", err)
} }
err := config.handleFile(cliConfig.ConfigFile)
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
} }

View File

@ -1,28 +0,0 @@
package backend
import (
"fmt"
"io"
"os"
)
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.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
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()
}

View File

@ -1,42 +0,0 @@
package backend
import (
"context"
firebase "firebase.google.com/go/v4"
"firebase.google.com/go/v4/auth"
"google.golang.org/api/option"
)
type Client struct{ *auth.Client }
func NewClient(c *Config) (*Client, error) {
var err error
client := new(Client)
ctx := context.Background()
opt := option.WithCredentialsFile(c.FirebaseKey)
app, err := firebase.NewApp(ctx, nil, opt)
if err != nil {
return nil, err
}
client.Client, err = app.Auth(ctx)
if err != nil {
return nil, err
}
return client, nil
}
func (c *Client) Verify(idToken string) (*auth.Token, error) {
ctx := context.Background()
token, err := c.VerifyIDTokenAndCheckRevoked(ctx, idToken)
if err != nil {
return nil, err
}
return token, nil
}

View File

@ -1,38 +0,0 @@
package backend
import (
"fmt"
"io"
"os"
"github.com/chai2010/webp"
"github.com/disintegration/imaging"
"github.com/google/uuid"
)
func SaveImage(c *Config, src io.Reader) (string, error) {
img, err := imaging.Decode(src, imaging.AutoOrientation(true))
if err != nil {
return "", fmt.Errorf("error decoding image: %v", err)
}
if img.Bounds().Dy() > c.MaxImgHeight {
img = imaging.Resize(img, 0, c.MaxImgHeight, imaging.Lanczos)
}
if img.Bounds().Dx() > c.MaxImgWidth {
img = imaging.Resize(img, c.MaxImgWidth, 0, imaging.Lanczos)
}
filename := fmt.Sprint(c.PicsDir, "/", uuid.New(), ".webp")
file, err := os.Create(filename)
if err != nil {
return "", fmt.Errorf("error creating new image file: %v", err)
}
defer file.Close()
if err = webp.Encode(file, img, &webp.Options{Lossless: true}); err != nil {
return "", fmt.Errorf("error encoding image as webp: %v", err)
}
return filename, nil
}

View File

@ -9,15 +9,56 @@ import (
"git.streifling.com/jason/rss" "git.streifling.com/jason/rss"
) )
func GenerateRSS(c *Config, db *DB) (*string, error) { func GetChannel(db *DB, title, link, description string) (*rss.Channel, error) {
channel := &rss.Channel{ channel := &rss.Channel{
Title: c.Title, Title: title,
Link: c.Link, Link: link,
Description: c.Description, Description: description,
Items: make([]*rss.Item, 0), Items: make([]*rss.Item, 0),
} }
articles, err := db.GetCertainArticles("published", true) articles, err := db.GetCertainArticles(true, false)
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)
} }
@ -31,13 +72,7 @@ func GenerateRSS(c *Config, db *DB) (*string, error) {
for _, tag := range tags { for _, tag := range tags {
tagNames = append(tagNames, tag.Name) tagNames = append(tagNames, tag.Name)
} }
if article.IsInIssue || article.AutoGenerated {
tagNames = append(tagNames, fmt.Sprint("Orient Express ", article.IssueID)) 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 {
@ -54,25 +89,19 @@ func GenerateRSS(c *Config, db *DB) (*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)
} }
item := &rss.Item{ articleContent, err := ConvertToHTML(article.Content)
Author: fmt.Sprint(user.FirstName, " ", user.LastName), if err != nil {
Categories: tagNames, return nil, fmt.Errorf("error converting content to HTML for RSS feed: %v", err)
Description: articleDescription, }
Guid: string(article.ID),
Link: article.Link, channel.Items = append(channel.Items, &rss.Item{
PubDate: article.Created.Format(time.RFC1123Z),
Title: articleTitle, Title: articleTitle,
} Author: user.FirstName + " " + user.LastName,
PubDate: article.Created.Format(time.RFC1123Z),
if article.AutoGenerated { Description: articleDescription,
item.Enclosure = &rss.Enclosure{ Content: &rss.Content{Value: articleContent},
Url: article.EncURL, Categories: tagNames,
Lenght: article.EncLength, })
Type: article.EncType,
}
}
channel.Items = append(channel.Items, item)
} }
feed := rss.NewFeed() feed := rss.NewFeed()

View File

@ -10,10 +10,9 @@ import (
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
) )
type ( type CookieStore struct {
CookieStore struct{ sessions.CookieStore } sessions.CookieStore
Session struct{ sessions.Session } }
)
func NewKey() ([]byte, error) { func NewKey() ([]byte, error) {
key := make([]byte, 32) key := make([]byte, 32)

View File

@ -1,55 +0,0 @@
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)
}
}

View File

@ -1,48 +0,0 @@
package calls
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
b "streifling.com/jason/cpolis/cmd/backend"
)
func ServePDFList(c *b.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !tokenIsVerified(w, r, c) {
return
}
files, err := os.ReadDir(c.PDFDir)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fileNames := make([]string, 0)
for _, file := range files {
fileNames = append(fileNames, file.Name())
}
w.Header().Set("Content-Type", "application/json")
if err = json.NewEncoder(w).Encode(fileNames); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
func ServePDF(c *b.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !tokenIsVerified(w, r, c) {
return
}
http.ServeFile(w, r, fmt.Sprint(c.PDFDir, "/", r.PathValue("id")))
}
}

View File

@ -1,17 +0,0 @@
package calls
import (
"net/http"
b "streifling.com/jason/cpolis/cmd/backend"
)
func ServeRSS(c *b.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !tokenIsVerified(w, r, c) {
return
}
http.ServeFile(w, r, c.RSSFile)
}
}

View File

@ -1,34 +0,0 @@
package calls
import (
"log"
"net/http"
b "streifling.com/jason/cpolis/cmd/backend"
)
// tokenIsVerified verifies that a request is authorized. It returns a bool.
func tokenIsVerified(w http.ResponseWriter, r *http.Request, c *b.Config) bool {
idToken := r.Header.Get("Authorization")
if idToken == "" {
log.Println("Authorization header missing")
http.Error(w, "Authorization header missing", http.StatusUnauthorized)
return false
}
client, err := b.NewClient(c)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return false
}
_, err = client.Verify(idToken)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusUnauthorized)
return false
}
return true
}

View File

@ -4,12 +4,17 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"html/template" "html/template"
"io"
"io/fs"
"log" "log"
"net/http" "net/http"
"os" "os"
"path/filepath"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/google/uuid"
b "streifling.com/jason/cpolis/cmd/backend" b "streifling.com/jason/cpolis/cmd/backend"
) )
@ -18,31 +23,52 @@ const (
PreviewMode PreviewMode
) )
type EditorHTMLData struct { func ShowHub(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
Selected map[int64]bool return func(w http.ResponseWriter, r *http.Request) {
Content string session, err := s.Get(r, "cookie")
Action string if err != nil {
ActionTitle string tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
ActionButton string msg := "Session nicht mehr gültig. Bitte erneut anmelden."
HTMLContent template.HTML template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
Article *b.Article }
Tags []*b.Tag
session.Values["article"] = 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")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", session.Values["role"].(int))
}
} }
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) type editorHTMLData struct {
Title string
Description string
Content string
HTMLContent template.HTML
Tags []*b.Tag
Mode int
}
session, err := s.Get(r, "cookie")
if err != nil { if err != nil {
return tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
} }
data := &EditorHTMLData{Action: "submit"} var data editorHTMLData
if session.Values["article"] == nil { if session.Values["article"] == nil {
data = &EditorHTMLData{Article: new(b.Article)} data = editorHTMLData{}
} else { } else {
data = session.Values["article"].(*EditorHTMLData) data = session.Values["article"].(editorHTMLData)
} }
data.Mode = EditMode
data.Tags, err = db.GetTagList() data.Tags, err = db.GetTagList()
if err != nil { if err != nil {
@ -58,9 +84,11 @@ func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { func SubmitArticle(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 := s.Get(r, "cookie")
if err != nil { if err != nil {
return tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
} }
session.Values["article"] = nil session.Values["article"] = nil
@ -73,11 +101,10 @@ 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)
@ -87,20 +114,6 @@ 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"] {
@ -126,11 +139,6 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { func ResubmitArticle(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)
if err != nil {
return
}
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)
@ -142,18 +150,11 @@ 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)
@ -177,63 +178,50 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return return
} }
session, err := s.Get(r, "cookie")
if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
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 ShowUnpublishedUnrejectedAndPublishedRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { func ShowUnpublishedArticles(c *b.Config, db *b.DB) 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 { unpublishedArticles, err := db.GetCertainArticles(false, false)
return
}
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")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", articles) tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", unpublishedArticles)
} }
} }
func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { func ShowRejectedArticles(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) type htmlData struct {
MyIDs map[int64]bool
RejectedArticles []*b.Article
}
data := new(htmlData)
session, err := s.Get(r, "cookie")
if err != nil { if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
data := new(struct { data.RejectedArticles, err = db.GetCertainArticles(false, true)
MyIDs map[int64]bool
RejectedArticles []*b.Article
})
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)
@ -253,12 +241,77 @@ func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerF
} }
} }
func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { func ReviewUnpublishedArticle(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 { 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 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 {
return func(w http.ResponseWriter, r *http.Request) {
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)
@ -266,7 +319,6 @@ 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)
@ -274,15 +326,6 @@ 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)
@ -301,9 +344,7 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
data.Selected[tag.ID] = true data.Selected[tag.ID] = true
} }
data.Action = fmt.Sprint("resubmit/", data.Article.ID) tmpl, err := template.ParseFiles(c.WebDir + "/templates/rework-article.html")
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)
} }
@ -311,26 +352,21 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { func PublishArticle(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)
if err != nil {
return
}
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(article.ID); err != nil { session, err := s.Get(r, "cookie")
if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
if err = db.AddArticleToCurrentIssue(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
@ -346,37 +382,7 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return return
} }
if article.EditedID != 0 { feed, err := b.GenerateRSS(db, c.Title, c.Link, c.Description)
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)
@ -396,11 +402,6 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func RejectArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { func RejectArticle(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)
if err != nil {
return
}
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)
@ -408,7 +409,16 @@ func RejectArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return return
} }
if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: true}); err != nil { session, err := s.Get(r, "cookie")
if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
if err = db.UpdateAttributes(
&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
@ -420,12 +430,8 @@ func RejectArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
} }
} }
func ShowCurrentArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { func ShowCurrentArticles(c *b.Config, db *b.DB) 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
}
articles, err := db.GetCurrentIssueArticles() articles, err := db.GetCurrentIssueArticles()
if err != nil { if err != nil {
log.Println(err) log.Println(err)
@ -438,282 +444,48 @@ func ShowCurrentArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFu
} }
} }
func UploadArticleImage(c *b.Config, s *b.CookieStore) http.HandlerFunc { func UploadImage(c *b.Config) 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 { file, header, err := r.FormFile("article-image")
return
}
file, _, err := r.FormFile("article-image")
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
defer file.Close() defer file.Close()
filename, err := b.SaveImage(c, file) 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 { 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
} }
url := fmt.Sprint(c.Domain, "/image/serve/", filename) if err = os.MkdirAll(fmt.Sprint(c.PicsDir, "/"), fs.FileMode(0755)); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
img, err := os.Create(absFilepath)
if err != nil {
log.Println(err)
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
}
url := fmt.Sprint(c.Domain, "/pics/", 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, action string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil {
return
}
data := new(struct {
Action string
Articles []*b.Article
})
data.Action = action
publishedArticles, err := db.GetCertainArticles("published", true)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
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 = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", data)
}
}
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) {
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
}
article, err := db.GetArticle(id)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Article.Description, err = b.ConvertToPlain(article.Description)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
content, err := os.ReadFile(articleAbsName)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
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(id)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/review-article.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data)
}
}
func DeleteArticle(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
}
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = db.DeleteArticle(id); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
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 {
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
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
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 = b.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)
}
}

View File

@ -7,12 +7,8 @@ import (
b "streifling.com/jason/cpolis/cmd/backend" b "streifling.com/jason/cpolis/cmd/backend"
) )
func CreateTag(c *b.Config, s *b.CookieStore) http.HandlerFunc { func CreateTag(c *b.Config) 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
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-tag.html") tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-tag.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil) template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil)
} }
@ -20,13 +16,15 @@ func CreateTag(c *b.Config, s *b.CookieStore) http.HandlerFunc {
func AddTag(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { func AddTag(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)
if err != nil {
return
}
db.AddTag(r.PostFormValue("tag")) db.AddTag(r.PostFormValue("tag"))
session, err := s.Get(r, "cookie")
if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
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"])

View File

@ -1,4 +1,4 @@
package calls package frontend
import ( import (
"log" "log"
@ -10,10 +10,6 @@ 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 !tokenIsVerified(w, r, c) {
return
}
absFilepath, err := filepath.Abs(c.PicsDir) absFilepath, err := filepath.Abs(c.PicsDir)
if err != nil { if err != nil {
log.Println(err) log.Println(err)

View File

@ -1,133 +1,26 @@
package frontend package frontend
import ( import (
"fmt"
"html/template" "html/template"
"log" "log"
"mime"
"net/http" "net/http"
"os"
"path/filepath"
"time"
b "streifling.com/jason/cpolis/cmd/backend" b "streifling.com/jason/cpolis/cmd/backend"
) )
func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { func PublishLatestIssue(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)
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
}
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) session, err := s.Get(r, "cookie")
if err != nil { if err != nil {
log.Println(err) tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
http.Error(w, err.Error(), http.StatusInternalServerError) msg := "Session nicht mehr gültig. Bitte erneut anmelden."
return template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
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")
@ -135,42 +28,3 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
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, _, err := r.FormFile("issue-image")
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer file.Close()
filename, err := b.SaveImage(c, file)
if 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)
}
}

View File

@ -1,59 +0,0 @@
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)
}
}

View File

@ -1,7 +1,6 @@
package frontend package frontend
import ( import (
"errors"
"fmt" "fmt"
"html/template" "html/template"
"log" "log"
@ -27,26 +26,6 @@ func saveSession(w http.ResponseWriter, r *http.Request, s *b.CookieStore, u *b.
return nil return nil
} }
// getSession is used for verifying that the user is logged in and returns their session and an error.
func getSession(w http.ResponseWriter, r *http.Request, c *b.Config, s *b.CookieStore) (*b.Session, error) {
msg := "Keine gültige Session. Bitte erneut anmelden."
tmpl, tmplErr := template.ParseFiles(c.WebDir+"/templates/index.html", c.WebDir+"/templates/login.html")
tmpSession, err := s.Get(r, "cookie")
if err != nil {
template.Must(tmpl, tmplErr).ExecuteTemplate(w, "page-content", msg)
return nil, err
}
session := &b.Session{Session: *tmpSession}
if session.IsNew {
template.Must(tmpl, tmplErr).ExecuteTemplate(w, "page-content", msg)
return session, errors.New("error: no existing session")
}
return session, nil
}
func HomePage(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { func HomePage(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) {
numRows, err := db.CountEntries("users") numRows, err := db.CountEntries("users")
@ -81,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.StatusBadRequest) http.Error(w, fmt.Sprintf("no such user: %v", userName), http.StatusInternalServerError)
return return
} }
@ -111,12 +90,15 @@ func Login(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func Logout(c *b.Config, s *b.CookieStore) http.HandlerFunc { func Logout(c *b.Config, 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 := s.Get(r, "cookie")
if err != nil { if err != nil {
return tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
} }
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)
@ -127,22 +109,3 @@ func Logout(c *b.Config, s *b.CookieStore) http.HandlerFunc {
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil) template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil)
} }
} }
func ShowHub(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
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", session.Values["role"].(int))
}
}

View File

@ -30,12 +30,8 @@ func checkUserStrings(user *b.User) (string, int, bool) {
} }
} }
func CreateUser(c *b.Config, s *b.CookieStore) http.HandlerFunc { func CreateUser(c *b.Config) 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
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html") tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil) template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil)
} }
@ -43,11 +39,6 @@ func CreateUser(c *b.Config, s *b.CookieStore) http.HandlerFunc {
func AddUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { func AddUser(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)
if err != nil {
return
}
role, err := strconv.Atoi(r.PostFormValue("role")) role, err := strconv.Atoi(r.PostFormValue("role"))
if err != nil { if err != nil {
log.Println(err) log.Println(err)
@ -103,6 +94,13 @@ func AddUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return return
} }
session, err := s.Get(r, "cookie")
if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
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"].(int)) tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int))
@ -111,9 +109,11 @@ func AddUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func EditSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { func EditSelf(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 := s.Get(r, "cookie")
if err != nil { if err != nil {
return tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
} }
user, err := db.GetUser(session.Values["id"].(int64)) user, err := db.GetUser(session.Values["id"].(int64))
@ -130,9 +130,11 @@ func EditSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func UpdateSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { func UpdateSelf(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 := s.Get(r, "cookie")
if err != nil { if err != nil {
return tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
} }
userData := UserData{ userData := UserData{
@ -199,6 +201,7 @@ 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) {
var err error var err error
htmlData := UserData{ htmlData := UserData{
User: &b.User{ User: &b.User{
UserName: r.PostFormValue("username"), UserName: r.PostFormValue("username"),
@ -272,17 +275,13 @@ func AddFirstUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func ShowAllUsers(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.HandlerFunc { func ShowAllUsers(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) {
session, err := getSession(w, r, c, s) var err error
if err != nil { type htmlData struct {
return
}
data := new(struct {
Users map[int64]*b.User Users map[int64]*b.User
Action string Action string
}) }
data.Action = action data := &htmlData{Action: action}
data.Users, err = db.GetAllUsers() data.Users, err = db.GetAllUsers()
if err != nil { if err != nil {
log.Println(err) log.Println(err)
@ -290,18 +289,21 @@ func ShowAllUsers(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.H
return return
} }
session, err := s.Get(r, "cookie")
if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
delete(data.Users, session.Values["id"].(int64)) delete(data.Users, session.Values["id"].(int64))
tmpl, err := template.ParseFiles(c.WebDir + "/templates/show-all-users.html") tmpl, err := template.ParseFiles(c.WebDir + "/templates/show-all-users.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data) template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data)
} }
} }
func EditUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { func EditUser(c *b.Config, db *b.DB) 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
}
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)
@ -323,11 +325,6 @@ func EditUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func UpdateUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { func UpdateUser(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)
if err != nil {
return
}
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)
@ -397,6 +394,13 @@ func UpdateUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", userData) template.Must(tmpl, err).ExecuteTemplate(w, "page-content", userData)
} }
session, err := s.Get(r, "cookie")
if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
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"].(int)) tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int))
@ -405,11 +409,6 @@ func UpdateUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func DeleteUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { func DeleteUser(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)
if err != nil {
return
}
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)
@ -423,6 +422,13 @@ func DeleteUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return return
} }
session, err := s.Get(r, "cookie")
if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
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"].(int)) tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int))

View File

@ -7,7 +7,6 @@ import (
"os" "os"
b "streifling.com/jason/cpolis/cmd/backend" b "streifling.com/jason/cpolis/cmd/backend"
c "streifling.com/jason/cpolis/cmd/calls"
f "streifling.com/jason/cpolis/cmd/frontend" f "streifling.com/jason/cpolis/cmd/frontend"
) )
@ -49,48 +48,36 @@ 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 /article/allow-edit/{id}", f.AllowEditArticle(config, db, store)) mux.HandleFunc("GET /create-tag", f.CreateTag(config))
mux.HandleFunc("GET /article/all-published/review-edit", f.ShowPublishedArticles(config, db, store, "review-edit")) mux.HandleFunc("GET /create-user", f.CreateUser(config))
mux.HandleFunc("GET /article/all-published/delete", f.ShowPublishedArticles(config, db, store, "review-delete")) mux.HandleFunc("GET /edit-self", f.EditSelf(config, db, store))
mux.HandleFunc("GET /article/all-rejected", f.ShowRejectedArticles(config, db, store)) mux.HandleFunc("GET /edit-user/{id}", f.EditUser(config, db))
mux.HandleFunc("GET /article/all-unpublished-unrejected-and-published-rejected", f.ShowUnpublishedUnrejectedAndPublishedRejectedArticles(config, db, store)) mux.HandleFunc("GET /delete-user/{id}", f.DeleteUser(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/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 /pics/{pic}", f.ServeImage(config, store))
mux.HandleFunc("GET /pdf/serve/{id}", c.ServePDF(config)) mux.HandleFunc("GET /publish-article/{id}", f.PublishArticle(config, db, store))
mux.HandleFunc("GET /rss/serve", c.ServeRSS(config)) mux.HandleFunc("GET /publish-issue", f.PublishLatestIssue(config, db, store))
mux.HandleFunc("GET /tag/create", f.CreateTag(config, store)) mux.HandleFunc("GET /reject-article/{id}", f.RejectArticle(config, db, store))
mux.HandleFunc("GET /user/create", f.CreateUser(config, store)) mux.HandleFunc("GET /rejected-articles", f.ShowRejectedArticles(config, db, store))
mux.HandleFunc("GET /user/delete/{id}", f.DeleteUser(config, db, store)) mux.HandleFunc("GET /review-rejected-article/{id}", f.ReviewRejectedArticle(config, db, store))
mux.HandleFunc("GET /user/edit/{id}", f.EditUser(config, db, store)) mux.HandleFunc("GET /review-unpublished-article/{id}", f.ReviewUnpublishedArticle(config, db, store))
mux.HandleFunc("GET /user/edit/self", f.EditSelf(config, db, store)) mux.HandleFunc("GET /rss", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, config.RSSFile) })
mux.HandleFunc("GET /user/show-all/delete", f.ShowAllUsers(config, db, store, "delete")) mux.HandleFunc("GET /show-all-users-edit", f.ShowAllUsers(config, db, store, "edit-user"))
mux.HandleFunc("GET /user/show-all/edit", f.ShowAllUsers(config, db, store, "edit")) mux.HandleFunc("GET /show-all-users-delete", f.ShowAllUsers(config, db, store, "delete-user"))
mux.HandleFunc("GET /this-issue", f.ShowCurrentArticles(config, db))
mux.HandleFunc("GET /unpublished-articles", f.ShowUnpublishedArticles(config, db))
mux.HandleFunc("GET /write-article", f.WriteArticle(config, db, store))
mux.HandleFunc("POST /article/resubmit/{id}", f.ResubmitArticle(config, db, store)) mux.HandleFunc("POST /add-first-user", f.AddFirstUser(config, db, store))
mux.HandleFunc("POST /article/submit", f.SubmitArticle(config, db, store)) mux.HandleFunc("POST /add-tag", f.AddTag(config, db, store))
mux.HandleFunc("POST /article/upload-image", f.UploadArticleImage(config, store)) mux.HandleFunc("POST /add-user", f.AddUser(config, db, 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 /pdf/upload", f.UploadPDF(config, store)) mux.HandleFunc("POST /resubmit-article/{id}", f.ResubmitArticle(config, db, store))
mux.HandleFunc("POST /tag/add", f.AddTag(config, db, store)) mux.HandleFunc("POST /submit-article", f.SubmitArticle(config, db, store))
mux.HandleFunc("POST /user/add", f.AddUser(config, db, store)) mux.HandleFunc("POST /update-self", f.UpdateSelf(config, db, store))
mux.HandleFunc("POST /user/add-first", f.AddFirstUser(config, db, store)) mux.HandleFunc("POST /update-user/{id}", f.UpdateUser(config, db, store))
mux.HandleFunc("POST /user/update/{id}", f.UpdateUser(config, db, store)) mux.HandleFunc("POST /upload-image", f.UploadImage(config))
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))
} }

View File

@ -1,9 +0,0 @@
[Unit]
Description=cpolis
[Service]
ExecStart=/usr/local/bin/cpolis
Restart=on-failure
[Install]
WantedBy=default.target

View File

@ -11,13 +11,13 @@ 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 (
@ -25,32 +25,26 @@ CREATE TABLE articles (
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,
link VARCHAR(255), content TEXT NOT NULL,
enc_url VARCHAR(255),
enc_length INT,
enc_type VARCHAR(255),
published BOOL NOT NULL, published BOOL NOT NULL,
rejected BOOL NOT NULL, rejected BOOL NOT NULL,
author_id INT NOT NULL, author_id INT NOT NULL,
issue_id INT NOT NULL, issue_id INT NOT NULL,
edited_id INT, PRIMARY KEY(id),
is_in_issue BOOL NOT NULL, FOREIGN KEY(author_id) REFERENCES users(id),
auto_generated BOOL NOT NULL, FOREIGN KEY(issue_id) REFERENCES issues(id)
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)
); );

57
go.mod
View File

@ -1,66 +1,23 @@
module streifling.com/jason/cpolis module streifling.com/jason/cpolis
go 1.22.6 go 1.22.0
toolchain go1.23.1
require ( require (
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/anthonynsimon/bild v0.14.0
github.com/chai2010/webp v1.1.1
github.com/disintegration/imaging v1.6.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
github.com/gorilla/sessions v1.2.2 github.com/gorilla/sessions v1.2.2
github.com/microcosm-cc/bluemonday v1.0.26 github.com/microcosm-cc/bluemonday v1.0.26
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd github.com/yuin/goldmark v1.7.0
github.com/yuin/goldmark v1.7.4 golang.org/x/crypto v0.14.0
golang.org/x/crypto v0.27.0 golang.org/x/term v0.17.0
golang.org/x/term v0.24.0
google.golang.org/api v0.191.0
) )
require ( require (
cloud.google.com/go v0.115.0 // indirect
cloud.google.com/go/auth v0.8.1 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
cloud.google.com/go/compute/metadata v0.5.0 // indirect
cloud.google.com/go/firestore v1.16.0 // indirect
cloud.google.com/go/iam v1.1.13 // indirect
cloud.google.com/go/longrunning v0.5.11 // indirect
cloud.google.com/go/storage v1.43.0 // indirect
github.com/MicahParks/keyfunc v1.9.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/s2a-go v0.1.8 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
github.com/gorilla/css v1.0.0 // indirect github.com/gorilla/css v1.0.0 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect
go.opencensus.io v0.24.0 // indirect golang.org/x/net v0.17.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect golang.org/x/sys v0.17.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect
go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
golang.org/x/image v0.18.0 // indirect
golang.org/x/net v0.29.0 // indirect
golang.org/x/oauth2 v0.22.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/text v0.18.0 // indirect
golang.org/x/time v0.6.0 // indirect
google.golang.org/appengine/v2 v2.0.2 // indirect
google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240812133136-8ffd90a71988 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240812133136-8ffd90a71988 // indirect
google.golang.org/grpc v1.65.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
) )

216
go.sum
View File

@ -1,97 +1,15 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= git.streifling.com/jason/rss v0.1.2 h1:UB3UHJXMt5WDDh9y8n0Z6nS1XortbPXjEr7QZTdovY4=
cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14= git.streifling.com/jason/rss v0.1.2/go.mod h1:gpZF0nZbQSstMpyHD9DTAvlQEG7v4pjO5c7aIMWM4Jg=
cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU=
cloud.google.com/go/auth v0.8.1 h1:QZW9FjC5lZzN864p13YxvAtGUlQ+KgRL+8Sg45Z6vxo=
cloud.google.com/go/auth v0.8.1/go.mod h1:qGVp/Y3kDRSDZ5gFD/XPUfYQ9xW1iI7q8RIRoCyBbJc=
cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
cloud.google.com/go/firestore v1.16.0 h1:YwmDHcyrxVRErWcgxunzEaZxtNbc8QoFYA/JOEwDPgc=
cloud.google.com/go/firestore v1.16.0/go.mod h1:+22v/7p+WNBSQwdSwP57vz47aZiY+HrDkrOsJNhk7rg=
cloud.google.com/go/iam v1.1.13 h1:7zWBXG9ERbMLrzQBRhFliAV+kjcRToDTgQT3CTwYyv4=
cloud.google.com/go/iam v1.1.13/go.mod h1:K8mY0uSXwEXS30KrnVb+j54LB/ntfZu1dr+4zFMNbus=
cloud.google.com/go/longrunning v0.5.11 h1:Havn1kGjz3whCfoD8dxMLP73Ph5w+ODyZB9RUsDxtGk=
cloud.google.com/go/longrunning v0.5.11/go.mod h1:rDn7//lmlfWV1Dx6IB4RatCPenTwwmqXuiP0/RgoEO4=
cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs=
cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0=
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=
git.streifling.com/jason/rss v0.1.3 h1:fd3j4ZtcLehapcmmroo3AP3X34gRHC4xzpfV6bDV1ZU=
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 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=
github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
github.com/anthonynsimon/bild v0.14.0 h1:IFRkmKdNdqmexXHfEU7rPlAmdUZ8BDZEGtGHDnGWync=
github.com/anthonynsimon/bild v0.14.0/go.mod h1:hcvEAyBjTW69qkKJTfpcDQ83sSZHxwOunsseDfeQhUs=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chai2010/webp v1.1.1 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk=
github.com/chai2010/webp v1.1.1/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=
github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
@ -100,123 +18,13 @@ github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTj
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.191.0 h1:cJcF09Z+4HAB2t5qTQM1ZtfL/PemsLFkcFG67qq2afk=
google.golang.org/api v0.191.0/go.mod h1:tD5dsFGxFza0hnQveGfVk9QQYKcfp+VzgRqyXFxE0+E=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine/v2 v2.0.2 h1:MSqyWy2shDLwG7chbwBJ5uMyw6SNqJzhJHNDwYB0Akk=
google.golang.org/appengine/v2 v2.0.2/go.mod h1:PkgRUWz4o1XOvbqtWTkBtCitEJ5Tp4HoVEdMMYQR/8E=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988 h1:CT2Thj5AuPV9phrYMtzX11k+XkzMGfRAet42PmoTATM=
google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988/go.mod h1:7uvplUBj4RjHAxIZ//98LzOvrQ04JBkaixRmCMI29hc=
google.golang.org/genproto/googleapis/api v0.0.0-20240812133136-8ffd90a71988 h1:+/tmTy5zAieooKIXfzDm9KiA3Bv6JBwriRN9LY+yayk=
google.golang.org/genproto/googleapis/api v0.0.0-20240812133136-8ffd90a71988/go.mod h1:4+X6GvPs+25wZKbQq9qyAXrwIRExv7w0Ea6MgZLZiDM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240812133136-8ffd90a71988 h1:V71AcdLZr2p8dC9dbOIMCpqi4EmRl8wUwnJzXXLmbmc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240812133136-8ffd90a71988/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -7,5 +7,4 @@ module.exports = {
plugins: [ plugins: [
require('@tailwindcss/typography') require('@tailwindcss/typography')
], ],
darkMode: 'selector',
} }

View File

@ -1,58 +0,0 @@
#! /bin/sh -
CPOLIS_REPO_URL="https://git.streifling.com/api/v1/repos/jason/cpolis/releases"
EXTRACTION_DIR=$HOME
CPOLIS_DIR=$EXTRACTION_DIR/cpolis
TAILWINDCSS_REPO_URL=https://api.github.com/repos/tailwindlabs/tailwindcss/releases/latest
TMP_DIR=/tmp
BIN_DIR=/usr/local/bin
SYSTEMD_DIR=/etc/systemd/system
check_dependency() {
if ! which $1 >/dev/null 2>&1; then
echo "$1 needs to be installed" >&2
exit 1
fi
}
if ! groups | grep -E 'root|wheel|sudo' >/dev/null; then
echo "You need administrative privileges for this script" >&2
exit 1
fi
check_dependency curl
check_dependency go
check_dependency jq
check_dependency tar
check_dependency xargs
echo '\nDownloading cpolis...' >&2
rm -fr $CPOLIS_DIR/*
latest_release=$(curl -s $CPOLIS_REPO_URL | jq -r '.[0].tag_name')
curl -Lo $TMP_DIR/cpolis.tar.gz https://git.streifling.com/jason/cpolis/archive/$latest_release.tar.gz
tar -xzf $TMP_DIR/cpolis.tar.gz -C $EXTRACTION_DIR
rm $TMP_DIR/cpolis.tar.gz
echo '\nDownloading TailwindCSS...' >&2
curl -s $TAILWINDCSS_REPO_URL |
grep -F browser_download_url |
grep -F linux-x64 |
cut -d'"' -f4 |
xargs -r curl -Lo $CPOLIS_DIR/tailwindcss
chmod +x $CPOLIS_DIR/tailwindcss
$CPOLIS_DIR/tailwindcss -i $CPOLIS_DIR/web/static/css/input.css -o $CPOLIS_DIR/web/static/css/style.css
echo '\nBuilding cpolis...' >&2
cd $CPOLIS_DIR
go build -o $BIN_DIR/cpolis cmd/main.go
cd
echo '\nSetting up system files...' >&2
sudo chown root:root $BIN_DIR/cpolis
chmod +x $BIN_DIR/cpolis
echo '\nSetting up service...' >&2
sudo mv $CPOLIS_DIR/cpolis.service $SYSTEMD_DIR
sudo chown root:root $SYSTEMD_DIR/cpolis.service
sudo systemctl daemon-reload
sudo systemctl is-active --quiet cpolis.service && sudo systemctl restart cpolis.service

View File

@ -2,71 +2,40 @@
@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;
} }
h3 { form {
@apply font-bold mb-2 text-xl; @apply flex flex-col gap-y-3;
}
input[type="file"] {
@apply border rounded-md w-full;
} }
input[type="password"], input[type="password"],
input[type="text"] { input[type="text"] {
@apply bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 h-8 rounded-md; @apply border h-8 rounded-md;
} }
textarea { textarea {
@apply bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 h-32 rounded-md; @apply border 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 grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-1 mt-4; @apply flex gap-4 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 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; @apply bg-slate-200 border my-2 px-3 py-2 rounded-md w-full hover:bg-slate-100;
} }
.action-btn { .action-btn {
@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; @apply bg-slate-800 border my-2 px-3 py-2 rounded-md text-slate-50 w-full hover:bg-slate-700;
}
.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
} }

1
web/static/js/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -2,9 +2,9 @@
<h2>Neuer Tag</h2> <h2>Neuer Tag</h2>
<form> <form>
<input class="w-full" name="tag" placeholder="Tag eingeben" required type="text" /> <input required name="tag" placeholder="Tag eingeben" type="text" />
<div class="btn-area"> <div class="btn-area">
<input class="action-btn" type="submit" value="Anlegen" hx-post="/tag/add" hx-target="#page-content" /> <input class="action-btn" type="submit" value="Anlegen" hx-post="/add-tag" 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>

View File

@ -2,7 +2,7 @@
<h2>Neuer Benutzer</h2> <h2>Neuer Benutzer</h2>
<form> <form>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"> <div class="grid 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 flex-wrap gap-4"> <div class="flex 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="/user/add" hx-target="#page-content" /> <input class="action-btn" type="submit" value="Anlegen" hx-post="/add-user" 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>

View File

@ -1,73 +1,17 @@
{{define "page-content"}} {{define "page-content"}}
<h2>Diese Ausgabe</h2> <h2>Aktuelle Artikel</h2>
<form hx-encoding="multipart/form-data"> <div class="flex flex-col gap-4">
<div class="flex flex-col gap-4">
<div>
<h3>Aktuelle Artikel</h3>
<div class="flex flex-col gap-4">
{{range .}} {{range .}}
<div class="border border-slate-200 dark:border-slate-800 px-2 py-1 rounded-md"> <div class="border px-2 py-1 rounded-md">
<h1 class="font-bold text-2xl">{{.Title}}</h1> <h1 class="font-bold text-2xl">{{.Title}}</h1>
<p>{{.Description}}</p> <p>{{.Description}}</p>
</div> </div>
{{end}} {{end}}
</div> </div>
</div>
<div> <div class="btn-area">
<h3>Titelseite</h3> <button class="action-btn" hx-get="/publish-issue" hx-target="#page-content">Ausgabe publizieren</button>
<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 class="btn-area">
<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}}

View File

@ -2,7 +2,7 @@
<h2>Profil bearbeiten</h2> <h2>Profil bearbeiten</h2>
<form> <form>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"> <div class="grid 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="/user/update/self" <input class="action-btn" type="submit" value="Aktualisieren" hx-post="/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>

View File

@ -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-1 sm:grid-cols-2 md:grid-cols-3 gap-4"> <div class="grid 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 flex-wrap gap-4"> <div class="flex 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="/user/update/{{.ID}}" <input class="action-btn" type="submit" value="Aktualisieren" hx-post="/update-user/{{.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>

View File

@ -4,41 +4,32 @@
<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="{{.Article.Title}}" /> <input name="article-title" type="text" value="{{.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">{{.Article.Description}}</textarea> <textarea name="article-description">{{.Description}}</textarea>
</div> </div>
<div class="flex flex-col">
<div class="flex flex-col gap-y-1"> <label for="article-content">Artikel</label>
<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="tag-{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" {{if index $.Selected <input id="{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" />
.ID}}checked{{end}} /> <label for="{{.Name}}">{{.Name}}</label>
<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="/article/{{.Action}}" <input class="action-btn" type="submit" value="Senden" hx-post="/submit-article" 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>
</form> </form>
@ -55,7 +46,7 @@
var formData = new FormData(); var formData = new FormData();
formData.append('article-image', file); formData.append('article-image', file);
fetch('/article/upload-image', { fetch('/upload-image', {
method: 'POST', method: 'POST',
body: formData body: formData
}) })

View File

@ -25,8 +25,8 @@
</div> </div>
</div> </div>
<div class="btn-area-1"> <div class="btn-area">
<input class="action-btn" type="submit" value="Anlegen" hx-post="/user/add-first" hx-target="#page-content" /> <input class="action-btn" type="submit" value="Anlegen" hx-post="/add-first-user" hx-target="#page-content" />
</div> </div>
</form> </form>

View File

@ -4,46 +4,44 @@
{{if lt . 4}} {{if lt . 4}}
<div class="mb-3"> <div class="mb-3">
<h2>Artikel</h2> <h2>Autor</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-2"> <div class="grid grid-cols-2 gap-x-4 gap-y-2">
<button class="btn" hx-get="/article/write" hx-target="#page-content">Artikel schreiben</button> <button class="btn" hx-get="/write-article" hx-target="#page-content">Artikel schreiben</button>
<button class="btn" hx-get="/article/all-rejected" hx-target="#page-content">Artikel bearbeiten</button> <button class="btn" hx-get="/rejected-articles" hx-target="#page-content">Abgelehnte Artikel</button>
{{if lt . 3}}<button class="btn" hx-get="/article/all-unpublished-unrejected-and-published-rejected" <a class="btn text-center" href="/rss">RSS Feed</a>
hx-target="#page-content">Artikel veröffentlichen</button>{{end}} <button class="btn" hx-get="/edit-self" hx-target="#page-content">Profil bearbeiten</button>
{{if lt . 2}}<button class="btn" hx-get="/article/all-published/delete" hx-target="#page-content">Artikel </div>
löschen</button>{{end}} </div>
{{if lt . 2}}<button class="btn" hx-get="/article/all-published/review-edit" {{end}}
hx-target="#page-content">Artikel bearbeiten lassen</button>{{end}}
{{if lt . 3}}<button class="btn" hx-get="/tag/create" hx-target="#page-content">Neuer Tag</button>{{end}} {{if lt . 3}}
<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>Ausgabe</h2> <h2>Herausgeber</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-2"> <div class="grid grid-cols-2 gap-4">
<button class="btn" hx-get="/issue/this" hx-target="#page-content">Diese Ausgabe</button> <button class="btn" hx-get="/this-issue" hx-target="#page-content">Diese Ausgabe</button>
<form class="flex" hx-encoding="multipart/form-data"> </div>
<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}} {{end}}
</div>
{{if lt . 4}} {{if eq . 0}}
<div class="mb-3"> <div class="mb-3">
<h2>Benutzer</h2> <h2>Administrator</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-2"> <div class="grid grid-cols-3 gap-4">
<button class="btn" hx-get="/user/edit/self" hx-target="#page-content">Mein Profil bearbeiten</button> <button class="btn" hx-get="/create-user" hx-target="#page-content">Benutzer hinzufügen</button>
{{if eq . 0}}<button class="btn" hx-get="/user/create" hx-target="#page-content">Benutzer <button class="btn" hx-get="/show-all-users-edit" hx-target="#page-content">Benutzer bearbeiten</button>
hinzufügen</button>{{end}} <button class="btn" hx-get="/show-all-users-delete" hx-target="#page-content">Benutzer löschen</button>
{{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}}

View File

@ -5,26 +5,13 @@
<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 <body class="flex flex-col justify-between min-h-screen bg-slate-50">
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">
@ -33,39 +20,14 @@
</div> </div>
</main> </main>
<footer class="text-center text-gray-500 my-8"> <footer class="my-8">
<p>&copy; 2024 Jason Streifling. Alle Rechte vorbehalten.</p> <p class="text-center text-gray-500 dark:text-gray-400">
<p>v0.11.1 - <strong>Alpha: Drastische Änderungen und Fehler vorbehalten.</strong></p> &copy; 2024 Jason Streifling. Alle Rechte vorbehalten.
</p>
</footer> </footer>
<script src="https://unpkg.com/htmx.org@2.0.2"></script> <script src="https://unpkg.com/htmx.org@2.0.1"></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>

View File

@ -2,13 +2,11 @@
<h2>Anmeldung</h2> <h2>Anmeldung</h2>
<form> <form>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 my-1"> <div class="btn-area">
<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>
<div class="mt-2">
<input class="action-btn" type="submit" value="Anmelden" hx-post="/login" hx-target="#page-content" /> <input class="action-btn" type="submit" value="Anmelden" hx-post="/login" hx-target="#page-content" />
</div>
</form> </form>
{{end}} {{end}}

View File

@ -1,14 +0,0 @@
{{define "page-content"}}
<h2>Artikel löschen</h2>
<div class="flex flex-col gap-4">
{{range .Articles}}
<button class="btn" hx-get="/article/{{$.Action}}/{{.ID}}" hx-target="#page-content">
<h1 class="font-bold text-2xl">{{.Title}}</h1>
<p>{{.Description}}</p>
</button>
{{end}}
<button class="action-btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
</div>
{{end}}

View File

@ -1,10 +1,10 @@
{{define "page-content"}} {{define "page-content"}}
<h2>Artikel bearbeiten</h2> <h2>Abgelehnte Artikel</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="/article/review-rejected/{{.ID}}" hx-target="#page-content"> <button class="btn" hx-get="/review-rejected-article/{{.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>

View File

@ -1,50 +0,0 @@
{{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}}

View File

@ -0,0 +1,69 @@
{{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}}

View File

@ -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="/user/{{$.Action}}/{{.ID}}" hx-target="#page-content"> <button class="btn" hx-get="/{{$.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}}

View File

@ -0,0 +1,37 @@
{{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}}

View File

@ -1,9 +1,9 @@
{{define "page-content"}} {{define "page-content"}}
<h2>Artikel veröffentlichen</h2> <h2>Unveröffentlichte Artikel</h2>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
{{range .}} {{range .}}
<button class="btn" hx-get="/article/review-unpublished/{{.ID}}" hx-target="#page-content"> <button class="btn" hx-get="/review-unpublished-article/{{.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>