Compare commits

..

20 Commits

Author SHA1 Message Date
42d6e0c198 Merge branch 'devel' 2024-08-31 12:17:41 +02:00
e1af2979af Merge branch 'devel' 2024-08-31 11:27:15 +02:00
f6dedc6f10 Merge branch 'devel' 2024-08-31 01:43:53 +02:00
cdf0a49550 Merge branch 'devel' 2024-08-31 01:38:28 +02:00
c3c0650210 Merge branch 'devel' 2024-08-31 01:00:55 +02:00
d077f700d8 Merge branch 'devel' 2024-08-31 00:36:48 +02:00
ec752b1c66 Merge branch 'devel' 2024-08-30 23:43:12 +02:00
46aef4f12f Merge branch 'devel' 2024-08-25 10:51:35 +02:00
1b29e328cf Merge branch 'devel' 2024-08-25 06:38:55 +02:00
e50cb819f3 Merge branch 'devel' 2024-08-23 21:45:30 +02:00
c32e38ca10 Merge branch 'devel' 2024-08-23 20:57:11 +02:00
d7c8c7a43a Merge branch 'devel' 2024-08-18 17:31:00 +02:00
1cd3edc90c Merge branch 'devel' 2024-08-18 12:06:29 +02:00
0e768c9f61 Merge branch 'devel' 2024-08-08 21:27:07 +02:00
1fcd775cc5 Merge branch 'devel' 2024-08-08 21:14:24 +02:00
203a1ed147 Implemented EasyMDE 2024-08-08 21:13:25 +02:00
ef1914ee5c Implemented article preview 2024-08-08 21:13:25 +02:00
084b101e31 Register f.ArticlePreviewHtmlData in init() 2024-08-08 21:13:25 +02:00
b2db128aa9 Shorten lines by referencing frontend as f and backend as b 2024-08-08 21:13:25 +02:00
081e880fb6 Change structure of code tor frontend and backend one 2024-08-08 21:13:25 +02:00
42 changed files with 982 additions and 1465 deletions

View File

@ -5,16 +5,13 @@ tmp_dir = "tmp"
[build]
args_bin = [
"-articles tmp/articles",
"-config tmp/config.toml",
"-desc 'Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität'",
"-domain localhost",
"-firebase tmp/firebase.json",
"-key tmp/key.gob",
"-link https://distrikt-ni-st.de",
"-log tmp/cpolis.log",
"-pdfs tmp/pdfs",
"-pics tmp/pics",
"-port 8080",
"-rss tmp/orientexpress_alle.rss",
"-title 'Freimaurer Distrikt Niedersachsen und Sachsen-Anhalt'",
"-web web",

View File

@ -9,21 +9,18 @@ import (
)
type Article struct {
Created time.Time
Title string
Description string
Link string
EncURL string
EncType string
ID int64
AuthorID int64
IssueID int64
EditedID int64
EncLength int
Published bool
Rejected bool
IsInIssue bool
AutoGenerated bool
Title string
Created time.Time
Description string
Link string
EncURL string
EncLength int
EncType string
Published bool
Rejected bool
ID int64
AuthorID int64
IssueID int64
}
func (db *DB) AddArticle(a *Article) (int64, error) {
@ -32,9 +29,8 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
selectQuery := "SELECT id FROM issues WHERE published = false"
insertQuery := `
INSERT INTO articles
(title, description, link, enc_url, enc_length, enc_type, published,
rejected, author_id, issue_id, edited_id, is_in_issue, auto_generated)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
(title, description, link, enc_url, enc_length, enc_type, published, rejected, author_id, issue_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`
for i := 0; i < TxMaxRetries; i++ {
@ -52,8 +48,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
}
result, err := tx.Exec(insertQuery, a.Title, a.Description, a.Link,
a.EncURL, a.EncLength, a.EncType, a.Published, a.Rejected, a.AuthorID, id,
a.EditedID, a.IsInIssue, a.AutoGenerated)
a.EncURL, a.EncLength, a.EncType, a.Published, a.Rejected, a.AuthorID, id)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
@ -87,9 +82,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
func (db *DB) GetArticle(id int64) (*Article, error) {
query := `
SELECT
title, created, description, link, enc_url, enc_length, enc_type,
published, author_id, issue_id, edited_id, is_in_issue, auto_generated
SELECT title, created, description, link, enc_url, enc_length, enc_type, published, author_id
FROM articles
WHERE id = ?
`
@ -101,8 +94,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) {
if err := row.Scan(&article.Title, &created, &article.Description,
&article.Link, &article.EncURL, &article.EncLength, &article.EncType,
&article.Published, &article.AuthorID, &article.IssueID, &article.EditedID,
&article.IsInIssue, &article.AutoGenerated); err != nil {
&article.Published, &article.AuthorID); err != nil {
return nil, fmt.Errorf("error scanning article row: %v", err)
}
@ -115,15 +107,14 @@ func (db *DB) GetArticle(id int64) (*Article, error) {
return article, nil
}
func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, error) {
query := fmt.Sprintf(`
SELECT
id, title, created, description, link, enc_url, enc_length, enc_type,
author_id, issue_id, published, rejected, is_in_issue, auto_generated
func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) {
query := `
SELECT id, title, created, description, link, enc_url, enc_length, enc_type, author_id, issue_id
FROM articles
WHERE %s = ?
`, attribute)
rows, err := db.Query(query, value)
WHERE published = ?
AND rejected = ?
`
rows, err := db.Query(query, published, rejected)
if err != nil {
return nil, fmt.Errorf("error querying articles: %v", err)
}
@ -135,11 +126,11 @@ func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, erro
if err = rows.Scan(&article.ID, &article.Title, &created,
&article.Description, &article.Link, &article.EncURL, &article.EncLength,
&article.EncType, &article.AuthorID, &article.IssueID, &article.Published,
&article.Rejected, &article.IsInIssue, &article.AutoGenerated); err != nil {
&article.EncType, &article.AuthorID, &article.IssueID); err != nil {
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))
if err != nil {
return nil, fmt.Errorf("error parsing created: %v", err)
@ -156,11 +147,9 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) {
txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable}
issueQuery := "SELECT id FROM issues WHERE published = false"
articlesQuery := `
SELECT
id, title, created, description, link, enc_url, enc_length, enc_type,
author_id, auto_generated
SELECT id, title, created, description, link, enc_url, enc_length, enc_type, author_id
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++ {
@ -193,7 +182,7 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) {
if err = rows.Scan(&article.ID, &article.Title, &created,
&article.Description, &article.Link, &article.EncURL, &article.EncLength,
&article.EncType, &article.AuthorID, &article.AutoGenerated); err != nil {
&article.EncType, &article.AuthorID); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
@ -275,13 +264,14 @@ func (db *DB) AddArticleToCurrentIssue(id int64) error {
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)
}
articlesQuery := "DELETE FROM articles WHERE id = ?"
_, err = db.Exec(articlesQuery, id)
if err != nil {
return fmt.Errorf("error deleting article %v from DB: %v", id, err)

View File

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

View File

@ -1,49 +0,0 @@
package backend
import (
"fmt"
"io"
"log"
"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()
if _, err = io.Copy(dstFile, srcFile); err != nil {
return fmt.Errorf("error copying file: %v", err)
}
return dstFile.Sync()
}
func WriteFile(path string, src io.Reader) error {
file, err := os.Create(path)
if err != nil {
log.Println(err)
return fmt.Errorf("error creating file: %v", err)
}
defer file.Close()
if _, err = io.Copy(file, src); err != nil {
log.Println(err)
return fmt.Errorf("error copying file: %v", err)
}
if err = file.Sync(); err != nil {
log.Println(err)
return fmt.Errorf("error syncing file: %v", err)
}
return nil
}

View File

@ -8,7 +8,9 @@ import (
"google.golang.org/api/option"
)
type Client struct{ *auth.Client }
type Client struct {
*auth.Client
}
func NewClient(c *Config) (*Client, error) {
var err error

View File

@ -1,44 +0,0 @@
package backend
import (
"fmt"
"image"
"io"
"os"
"github.com/chai2010/webp"
"github.com/disintegration/imaging"
"github.com/google/uuid"
)
var ErrUnsupportedFormat error = image.ErrFormat // used internally by imaging
func SaveImage(c *Config, src io.Reader) (string, error) {
img, err := imaging.Decode(src, imaging.AutoOrientation(true))
if err != nil {
if err == ErrUnsupportedFormat {
return "", ErrUnsupportedFormat
}
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(uuid.New(), ".webp")
file, err := os.Create(c.PicsDir + "/" + 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

@ -17,12 +17,16 @@ func GenerateRSS(c *Config, db *DB) (*string, error) {
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 getting published articles for RSS feed: %v", err)
}
for _, article := range articles {
if !article.Published {
continue
}
tags, err := db.GetArticleTags(article.ID)
if err != nil {
return nil, fmt.Errorf("error getting tags for articles for RSS feed: %v", err)
@ -31,13 +35,7 @@ func GenerateRSS(c *Config, db *DB) (*string, error) {
for _, tag := range tags {
tagNames = append(tagNames, tag.Name)
}
if article.IsInIssue || article.AutoGenerated {
tagNames = append(tagNames, fmt.Sprint("Orient Express ", article.IssueID))
}
if article.AutoGenerated {
tagNames = append(tagNames, "autogenerated")
}
tagNames = append(tagNames, fmt.Sprint("Orient Express ", article.IssueID))
user, err := db.GetUser(article.AuthorID)
if err != nil {
@ -55,7 +53,7 @@ func GenerateRSS(c *Config, db *DB) (*string, error) {
}
item := &rss.Item{
Author: user.FirstName + " " + user.LastName,
Author: fmt.Sprint(user.FirstName, " ", user.LastName),
Categories: tagNames,
Description: articleDescription,
Guid: string(article.ID),
@ -63,8 +61,9 @@ func GenerateRSS(c *Config, db *DB) (*string, error) {
PubDate: article.Created.Format(time.RFC1123Z),
Title: articleTitle,
}
fmt.Println(article.Link, ": ", len(article.Link))
if article.AutoGenerated {
if article.Title == "Autogenerated cpolis Issue Article" {
item.Enclosure = &rss.Enclosure{
Url: article.EncURL,
Lenght: article.EncLength,

View File

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

View File

@ -47,7 +47,7 @@ func (db *DB) AddUser(u *User, pass string) (int64, error) {
return id, nil
}
func (db *DB) GetID(userName string) int64 {
func (db *DB) GetID(userName string) (int64, bool) {
var id int64
query := `
@ -56,11 +56,11 @@ func (db *DB) GetID(userName string) int64 {
WHERE username = ?
`
row := db.QueryRow(query, userName)
if err := row.Scan(&id); err != nil { // seems like the only possible error is ErrNoRows
return 0
if err := row.Scan(&id); err != nil {
return 0, false
}
return id
return id, true
}
func (db *DB) CheckPassword(id int64, pass string) error {
@ -146,7 +146,7 @@ func (db *DB) GetUser(id int64) (*User, error) {
return user, nil
}
func (db *DB) UpdateOwnUserAttributes(id int64, user, first, last, oldPass, newPass, newPass2 string) error {
func (db *DB) UpdateOwnAttributes(id int64, user, first, last, oldPass, newPass, newPass2 string) error {
passwordEmpty := true
if len(newPass) > 0 || len(newPass2) > 0 {
if newPass != newPass2 {
@ -228,7 +228,7 @@ func (db *DB) AddFirstUser(u *User, pass string) (int64, error) {
if err = tx.Commit(); err != nil {
return 0, fmt.Errorf("error committing transaction: %v", err)
}
return -1, nil
return 2, nil
}
hashedPass, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)

View File

@ -2,6 +2,7 @@ package calls
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
@ -42,6 +43,6 @@ func ServePDF(c *b.Config) http.HandlerFunc {
return
}
http.ServeFile(w, r, c.PDFDir+"/"+r.PathValue("id"))
http.ServeFile(w, r, fmt.Sprint(c.PDFDir, "/", r.PathValue("id")))
}
}

View File

@ -4,12 +4,16 @@ import (
"encoding/json"
"fmt"
"html/template"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/google/uuid"
b "streifling.com/jason/cpolis/cmd/backend"
)
@ -18,33 +22,30 @@ const (
PreviewMode
)
type EditorHTMLData struct {
Selected map[int64]bool
Content string
Action string
ActionTitle string
ActionButton string
HTMLContent template.HTML
Article *b.Article
Tags []*b.Tag
}
func WriteArticle(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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var data *EditorHTMLData
if session.Values["article"] == nil {
data = &EditorHTMLData{Action: "submit", Article: new(b.Article)}
} else {
data = session.Values["article"].(*EditorHTMLData)
type editorHTMLData struct {
Title string
Description string
Content string
HTMLContent template.HTML
Tags []*b.Tag
Mode int
}
var data editorHTMLData
if session.Values["article"] == nil {
data = editorHTMLData{}
} else {
data = session.Values["article"].(editorHTMLData)
}
data.Mode = EditMode
data.Tags, err = db.GetTagList()
if err != nil {
log.Println(err)
@ -53,11 +54,7 @@ func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html")
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data)
}
}
@ -65,8 +62,6 @@ func SubmitArticle(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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@ -78,22 +73,11 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
article := &b.Article{
Title: r.PostFormValue("article-title"),
Description: r.PostFormValue("article-description"),
Published: false,
Rejected: false,
AuthorID: session.Values["id"].(int64),
IsInIssue: r.PostFormValue("issue") == "on",
AutoGenerated: false,
}
if len(article.Title) == 0 {
http.Error(w, "Bitte den Titel eingeben.", http.StatusBadRequest)
return
}
if len(article.Description) == 0 {
http.Error(w, "Bitte die Beschreibung eingeben.", http.StatusBadRequest)
return
Title: r.PostFormValue("article-title"),
Description: r.PostFormValue("article-description"),
Published: false,
Rejected: false,
AuthorID: session.Values["id"].(int64),
}
article.ID, err = db.AddArticle(article)
@ -103,14 +87,8 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return
}
content := []byte(r.PostFormValue("article-content"))
if len(content) == 0 {
http.Error(w, "Bitte den Artikel eingeben.", http.StatusBadRequest)
return
}
articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
if err = os.WriteFile(articleAbsName, content, 0644); err != nil {
if err = os.WriteFile(articleAbsName, []byte(r.PostFormValue("article-content")), 0644); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -142,11 +120,7 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", session.Values["role"]); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
}
}
@ -154,8 +128,6 @@ func ResubmitArticle(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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@ -167,22 +139,8 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
title := r.PostFormValue("article-title")
if len(title) == 0 {
http.Error(w, "Bitte den Titel eingeben.", http.StatusBadRequest)
return
}
description := r.PostFormValue("article-description")
if len(description) == 0 {
http.Error(w, "Bitte die Beschreibung eingeben.", http.StatusBadRequest)
return
}
content := r.PostFormValue("article-content")
if len(content) == 0 {
http.Error(w, "Bitte den Artikel eingeben.", http.StatusBadRequest)
return
}
link := fmt.Sprint(c.ArticleDir, "/", id, ".md")
if err = os.WriteFile(link, []byte(content), 0644); err != nil {
@ -195,7 +153,6 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
&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: "rejected", Value: false},
&b.Attribute{Table: "articles", ID: id, AttName: "is_in_issue", Value: r.PostFormValue("issue") == "on"},
); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -221,55 +178,26 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", session.Values["role"]); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
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, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
rejectedArticles, err := db.GetCertainArticles("rejected", true)
unpublishedArticles, err := db.GetCertainArticles(false, false)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
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")
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", articles); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", unpublishedArticles)
}
}
@ -277,17 +205,16 @@ func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerF
return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data := new(struct {
type htmlData struct {
MyIDs map[int64]bool
RejectedArticles []*b.Article
})
}
data := new(htmlData)
data.RejectedArticles, err = db.GetCertainArticles("rejected", true)
data.RejectedArticles, err = db.GetCertainArticles(false, true)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -303,22 +230,98 @@ func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerF
tmpl, err := template.ParseFiles(c.WebDir + "/templates/rejected-articles.html")
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
tmpl.ExecuteTemplate(w, "page-content", data)
}
}
func ReviewUnpublishedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil {
return
}
type htmlData struct {
Title string
Description string
Content template.HTML
Tags []*b.Tag
ID int64
}
var err error
data := new(htmlData)
data.ID, err = strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
article, err := db.GetArticle(data.ID)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Title, err = b.ConvertToPlain(article.Title)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Description, err = b.ConvertToPlain(article.Description)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
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
}
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) {
if _, err := getSession(w, r, c, s); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
type htmlData struct {
Selected map[int64]bool
Article *b.Article
Content string
Tags []*b.Tag
}
data := new(htmlData)
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
log.Println(err)
@ -326,7 +329,6 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
return
}
data := new(EditorHTMLData)
data.Article, err = db.GetArticle(id)
if err != nil {
log.Println(err)
@ -335,13 +337,14 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
}
articleAbsName := fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md")
content, err := os.ReadFile(articleAbsName)
contentBytes, err := os.ReadFile(articleAbsName)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Content = string(content)
data.Content = string(contentBytes)
data.Tags, err = db.GetTagList()
if err != nil {
@ -361,15 +364,9 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
data.Selected[tag.ID] = true
}
data.Action = fmt.Sprint("resubmit/", data.Article.ID)
tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html")
tmpl, err := template.ParseFiles(c.WebDir + "/templates/rework-article.html")
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl.ExecuteTemplate(w, "page-content", data)
}
}
@ -377,26 +374,17 @@ func PublishArticle(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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = db.AddArticleToCurrentIssue(article.ID); err != nil {
if err = db.AddArticleToCurrentIssue(id); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -412,36 +400,6 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return
}
if article.EditedID != 0 {
oldArticle, err := db.GetArticle(article.EditedID)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = db.DeleteArticle(oldArticle.ID); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = os.Remove(fmt.Sprint(c.ArticleDir, "/", oldArticle.ID, ".md")); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = db.UpdateAttributes(
&b.Attribute{Table: "articles", ID: id, AttName: "link", Value: fmt.Sprint(c.Domain, "/article/serve/", article.ID)},
&b.Attribute{Table: "articles", ID: id, AttName: "edited_id", Value: 0},
); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
feed, err := b.GenerateRSS(c, db)
if err != nil {
log.Println(err)
@ -456,11 +414,7 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", session.Values["role"]); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
}
}
@ -468,8 +422,6 @@ func RejectArticle(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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@ -480,7 +432,9 @@ func RejectArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return
}
if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: true}); err != nil {
if err = db.UpdateAttributes(
&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: true},
); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -488,19 +442,13 @@ func RejectArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", session.Values["role"]); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
}
}
func ShowCurrentArticles(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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@ -512,23 +460,17 @@ func ShowCurrentArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFu
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/current-articles.html")
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", articles); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", articles)
}
}
func UploadArticleImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
file, _, err := r.FormFile("article-image")
file, header, err := r.FormFile("article-image")
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusBadRequest)
@ -536,100 +478,101 @@ func UploadArticleImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
}
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 == b.ErrUnsupportedFormat {
http.Error(w, "Das Dateiformat wird nicht unterstützt.", http.StatusBadRequest)
return
}
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
url := c.Domain + "/image/serve/" + filename
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, "/image/serve/", filename)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(url)
}
}
func ShowPublishedArticles(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.HandlerFunc {
func ShowPublishedArticles(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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data := new(struct {
Action string
Articles []*b.Article
})
data.Action = action
publishedArticles, err := db.GetCertainArticles("published", true)
publishedArticles, err := db.GetCertainArticles(true, false)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
filteredArticles := make([]*b.Article, 0)
for _, article := range publishedArticles {
if !article.AutoGenerated {
data.Articles = append(data.Articles, article)
if article.Title != "Autogenerated cpolis Issue Article" {
filteredArticles = append(filteredArticles, article)
}
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/published-articles.html")
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl.ExecuteTemplate(w, "page-content", filteredArticles)
}
}
func ReviewArticle(c *b.Config, db *b.DB, s *b.CookieStore, action, title, button string) http.HandlerFunc {
func ReviewArticleForDeletion(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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
type htmlData struct {
Title string
Description string
Content template.HTML
Tags []*b.Tag
ID int64
}
var err error
data := new(htmlData)
data.ID, err = strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
article, err := db.GetArticle(id)
article, err := db.GetArticle(data.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)
data.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)
data.Description, err = b.ConvertToPlain(article.Description)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -637,33 +580,31 @@ func ReviewArticle(c *b.Config, db *b.DB, s *b.CookieStore, action, title, butto
}
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)
contentBytes, err := os.ReadFile(articleAbsName)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/review-article.html")
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil {
content, err := b.ConvertToHTML(string(contentBytes))
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-deleted.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", data)
}
}
@ -671,8 +612,6 @@ 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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@ -709,129 +648,6 @@ func DeleteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int)); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
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)
if err = tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int)); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
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")
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int))
}
}

34
cmd/frontend/editor.go Normal file
View File

@ -0,0 +1,34 @@
package frontend
import (
"html/template"
"net/http"
b "streifling.com/jason/cpolis/cmd/backend"
)
func CreateTag(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
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-tag.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil)
}
}
func AddTag(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
}
db.AddTag(r.PostFormValue("tag"))
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
}
}

View File

@ -3,13 +3,16 @@ package frontend
import (
"fmt"
"html/template"
"io"
"log"
"mime"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/google/uuid"
b "streifling.com/jason/cpolis/cmd/backend"
)
@ -17,8 +20,6 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@ -29,20 +30,15 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
return
}
title := r.PostFormValue("issue-title")
if len(title) == 0 {
http.Error(w, "Bitte den Titel eingeben.", http.StatusBadRequest)
return
}
if session.Values["issue-image"] == nil {
http.Error(w, "Bitte ein Bild einfügen.", http.StatusBadRequest)
err := "error: Image required"
log.Println(err)
http.Error(w, err, http.StatusBadRequest)
return
}
imgFileName := session.Values["issue-image"].(string)
fmt.Println(imgFileName)
imgAbsName := c.PicsDir + "/" + imgFileName
imgAbsName := fmt.Sprint(c.PicsDir, "/", imgFileName)
imgFile, err := os.Open(imgAbsName)
if err != nil {
@ -59,17 +55,20 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
return
}
imgSize := imgInfo.Size()
mimeType := mime.TypeByExtension(filepath.Ext(imgAbsName))
article := &b.Article{
Title: title,
EncURL: 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,
Title: "Autogenerated cpolis Issue Article",
EncURL: fmt.Sprint(c.Domain, "/image/serve/", imgFileName),
EncLength: int(imgSize),
EncType: mimeType,
Published: true,
Rejected: false,
Created: time.Now(),
AuthorID: session.Values["id"].(int64),
}
fmt.Println(article.Link)
article.ID, err = db.AddArticle(article)
if err != nil {
@ -78,14 +77,8 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
return
}
content := []byte(r.PostFormValue("issue-content"))
if len(content) == 0 {
http.Error(w, "Bitte eine Beschreibung eingeben.", http.StatusBadRequest)
return
}
articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
if err = os.WriteFile(articleAbsName, content, 0644); err != nil {
if err = os.WriteFile(articleAbsName, []byte(r.PostFormValue("article-content")), 0644); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -137,11 +130,7 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", session.Values["role"]); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
}
}
@ -149,8 +138,6 @@ 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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@ -160,7 +147,7 @@ func UploadIssueImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
return
}
file, _, err := r.FormFile("issue-image")
file, header, err := r.FormFile("issue-image")
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -168,12 +155,25 @@ func UploadIssueImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
}
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 == b.ErrUnsupportedFormat {
http.Error(w, "Das Dateiformat wird nicht unterstützt.", http.StatusBadRequest)
return
}
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

View File

@ -1,69 +0,0 @@
package frontend
import (
"fmt"
"log"
"net/http"
"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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
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()
buffer := make([]byte, 512) // Should be enough for mime type
if _, err := file.Read(buffer); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if _, err := file.Seek(0, 0); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if http.DetectContentType(buffer) != "application/pdf" {
http.Error(w, "Die Datei ist kein PDF.", http.StatusBadRequest)
return
}
filename := fmt.Sprint(uuid.New(), ".pdf")
absFilepath, err := filepath.Abs(c.PDFDir + "/" + filename)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = b.WriteFile(absFilepath, file); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
}

View File

@ -26,29 +26,6 @@ func saveSession(w http.ResponseWriter, r *http.Request, s *b.CookieStore, u *b.
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 {
if err = template.Must(tmpl, tmplErr).ExecuteTemplate(w, "page-content", msg); err != nil {
return nil, fmt.Errorf("error executing template: %v", err)
}
return nil, fmt.Errorf("error getting session: %v", err)
}
session := &b.Session{Session: *tmpSession}
if session.IsNew {
if err = template.Must(tmpl, tmplErr).ExecuteTemplate(w, "page-content", msg); err != nil {
return nil, fmt.Errorf("error executing template: %v", err)
}
}
return session, nil
}
func HomePage(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
numRows, err := db.CountEntries("users")
@ -56,39 +33,21 @@ func HomePage(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
log.Fatalln(err)
}
files := make([]string, 2)
files[0] = c.WebDir + "/templates/index.html"
files := []string{c.WebDir + "/templates/index.html"}
if numRows == 0 {
files[1] = c.WebDir + "/templates/first-user.html"
files = append(files, c.WebDir+"/templates/first-user.html")
tmpl, err := template.ParseFiles(files...)
if err = template.Must(tmpl, err).Execute(w, nil); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
template.Must(tmpl, err).Execute(w, nil)
} else {
session, err := s.Get(r, "cookie")
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session, _ := s.Get(r, "cookie")
if auth, ok := session.Values["authenticated"].(bool); auth && ok {
files[1] = c.WebDir + "/templates/hub.html"
files = append(files, c.WebDir+"/templates/hub.html")
tmpl, err := template.ParseFiles(files...)
if err = template.Must(tmpl, err).Execute(w, session.Values["role"]); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
template.Must(tmpl, err).Execute(w, session.Values["role"])
} else {
files[1] = c.WebDir + "/templates/login.html"
files = append(files, c.WebDir+"/templates/login.html")
tmpl, err := template.ParseFiles(files...)
if err = template.Must(tmpl, err).Execute(w, nil); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
template.Must(tmpl, err).Execute(w, nil)
}
}
}
@ -99,8 +58,8 @@ func Login(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
userName := r.PostFormValue("username")
password := r.PostFormValue("password")
id := db.GetID(userName)
if id == 0 {
id, ok := db.GetID(userName)
if !ok {
http.Error(w, fmt.Sprintf("no such user: %v", userName), http.StatusBadRequest)
return
}
@ -125,11 +84,7 @@ func Login(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", user.Role); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", user.Role)
}
}
@ -137,8 +92,6 @@ func Logout(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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@ -150,11 +103,7 @@ func Logout(c *b.Config, s *b.CookieStore) http.HandlerFunc {
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil)
}
}
@ -162,8 +111,6 @@ 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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@ -175,10 +122,6 @@ func ShowHub(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", session.Values["role"].(int)); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", session.Values["role"].(int))
}
}

View File

@ -1,52 +0,0 @@
package frontend
import (
"html/template"
"log"
"net/http"
b "streifling.com/jason/cpolis/cmd/backend"
)
func CreateTag(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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-tag.html")
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
func AddTag(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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tag := r.PostFormValue("tag")
if len(tag) == 0 {
http.Error(w, "Bitte einen Tag eingeben.", http.StatusBadRequest)
return
}
db.AddTag(tag)
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", session.Values["role"]); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}

View File

@ -10,6 +10,11 @@ import (
b "streifling.com/jason/cpolis/cmd/backend"
)
type UserData struct {
*b.User
Msg string
}
func checkUserStrings(user *b.User) (string, int, bool) {
userLen := 15
nameLen := 50
@ -28,17 +33,11 @@ func checkUserStrings(user *b.User) (string, int, bool) {
func CreateUser(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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html")
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil)
}
}
@ -46,55 +45,58 @@ func AddUser(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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
user := &b.User{
UserName: r.PostFormValue("username"),
FirstName: r.PostFormValue("first-name"),
LastName: r.PostFormValue("last-name"),
}
pass := r.PostFormValue("password")
pass2 := r.PostFormValue("password2")
if len(user.UserName) == 0 || len(user.FirstName) == 0 ||
len(user.LastName) == 0 || len(pass) == 0 || len(pass2) == 0 {
http.Error(w, "Bitte alle Felder ausfüllen.", http.StatusBadRequest)
return
}
userString, stringLen, ok := checkUserStrings(user)
if !ok {
http.Error(w, fmt.Sprint(userString, " ist zu lang. Maximal ", stringLen, " Zeichen erlaubt."), http.StatusBadRequest)
return
}
if id := db.GetID(user.UserName); id != 0 {
http.Error(w, user.UserName+" ist bereits vergeben. Bitte anderen Benutzernamen wählen.", http.StatusBadRequest)
return
}
if pass != pass2 {
http.Error(w, "Die Passwörter stimmen nicht überein.", http.StatusBadRequest)
return
}
roleString := r.PostFormValue("role")
if len(roleString) == 0 {
http.Error(w, "Bitte eine Aufgabe vergeben.", http.StatusBadRequest)
return
}
user.Role, err = strconv.Atoi(roleString)
role, err := strconv.Atoi(r.PostFormValue("role"))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_, err = db.AddUser(user, pass)
htmlData := UserData{
User: &b.User{
UserName: r.PostFormValue("username"),
FirstName: r.PostFormValue("first-name"),
LastName: r.PostFormValue("last-name"),
Role: role,
},
}
pass := r.PostFormValue("password")
pass2 := r.PostFormValue("password2")
if len(htmlData.UserName) == 0 || len(htmlData.FirstName) == 0 ||
len(htmlData.LastName) == 0 || len(pass) == 0 || len(pass2) == 0 {
htmlData.Msg = "Alle Felder müssen ausgefüllt werden."
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData)
return
}
userString, stringLen, ok := checkUserStrings(htmlData.User)
if !ok {
htmlData.Msg = fmt.Sprint(userString, " ist zu lang. Maximal ",
stringLen, " Zeichen erlaubt.")
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData)
return
}
id, _ := db.GetID(htmlData.UserName)
if id != 0 {
htmlData.Msg = fmt.Sprint(htmlData.UserName,
" ist bereits vergeben. Bitte anderen Benutzernamen wählen.")
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData)
return
}
if pass != pass2 {
htmlData.Msg = "Die Passwörter stimmen nicht überein."
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData)
return
}
_, err = db.AddUser(htmlData.User, pass)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -103,11 +105,7 @@ func AddUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int)); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int))
}
}
@ -115,8 +113,6 @@ func EditSelf(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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@ -128,11 +124,7 @@ func EditSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-self.html")
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", user); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", user)
}
}
@ -140,100 +132,128 @@ func UpdateSelf(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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
user := &b.User{
ID: session.Values["id"].(int64),
UserName: r.PostFormValue("username"),
FirstName: r.PostFormValue("first-name"),
LastName: r.PostFormValue("last-name"),
userData := UserData{
User: &b.User{
ID: session.Values["id"].(int64),
UserName: r.PostFormValue("username"),
FirstName: r.PostFormValue("first-name"),
LastName: r.PostFormValue("last-name"),
},
}
oldPass := r.PostFormValue("old-password")
newPass := r.PostFormValue("password")
newPass2 := r.PostFormValue("password2")
if len(user.UserName) == 0 {
http.Error(w, "Bitte den Benutzernamen ausfüllen.", http.StatusBadRequest)
if len(userData.UserName) == 0 || len(userData.FirstName) == 0 ||
len(userData.LastName) == 0 {
userData.Msg = "Alle Felder mit * müssen ausgefüllt sein."
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", userData.Msg)
return
}
if len(user.FirstName) == 0 || len(user.LastName) == 0 {
http.Error(w, "Bitte den vollständigen Namen ausfüllen.", http.StatusBadRequest)
return
}
userString, stringLen, ok := checkUserStrings(user)
userString, stringLen, ok := checkUserStrings(userData.User)
if !ok {
http.Error(w, fmt.Sprint(userString, " ist zu lang. Maximal ", stringLen, " Zeichen erlaubt."), http.StatusBadRequest)
userData.Msg = fmt.Sprint(userString, " ist zu lang. Maximal ",
stringLen, " Zeichen erlaubt.")
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", userData)
return
}
if id := db.GetID(user.UserName); id != 0 && id != user.ID {
http.Error(w, user.UserName+" ist bereits vergeben. Bitte anderen Benutzernamen wählen.", http.StatusBadRequest)
return
if id, ok := db.GetID(userData.UserName); ok {
if id != userData.ID {
userData.Msg = "Benutzername bereits vergeben."
userData.UserName = ""
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", userData)
return
}
}
if err = db.UpdateOwnUserAttributes(user.ID, user.UserName, user.FirstName, user.LastName, oldPass, newPass, newPass2); err != nil {
log.Println("error: user:", user.ID, err)
http.Error(w, "Benutzerdaten konnten nicht aktualisiert werden.", http.StatusInternalServerError)
return
if err = db.UpdateOwnAttributes(
userData.ID,
userData.UserName,
userData.FirstName,
userData.LastName,
oldPass,
newPass,
newPass2); err != nil {
userData.Msg = "Aktualisierung der Benutzerdaten fehlgeschlagen."
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", userData)
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int)); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int))
}
}
func AddFirstUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var err error
user := &b.User{
UserName: r.PostFormValue("username"),
FirstName: r.PostFormValue("first-name"),
LastName: r.PostFormValue("last-name"),
Role: b.Admin,
htmlData := UserData{
User: &b.User{
UserName: r.PostFormValue("username"),
FirstName: r.PostFormValue("first-name"),
LastName: r.PostFormValue("last-name"),
Role: b.Admin,
},
}
pass := r.PostFormValue("password")
pass2 := r.PostFormValue("password2")
if len(user.UserName) == 0 || len(user.FirstName) == 0 ||
len(user.LastName) == 0 || len(pass) == 0 || len(pass2) == 0 {
http.Error(w, "Bitte alle Felder ausfüllen.", http.StatusBadRequest)
if len(htmlData.UserName) == 0 || len(htmlData.FirstName) == 0 ||
len(htmlData.LastName) == 0 || len(pass) == 0 || len(pass2) == 0 {
htmlData.Msg = "Alle Felder müssen ausgefüllt werden."
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData)
return
}
userString, stringLen, ok := checkUserStrings(user)
userString, stringLen, ok := checkUserStrings(htmlData.User)
if !ok {
http.Error(w, fmt.Sprint(userString, " ist zu lang. Maximal ", stringLen, " Zeichen erlaubt."), http.StatusBadRequest)
htmlData.Msg = fmt.Sprint(userString, " ist zu lang. Maximal ",
stringLen, " Zeichen erlaubt.")
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData)
return
}
id, _ := db.GetID(htmlData.UserName)
if id != 0 {
htmlData.Msg = fmt.Sprint(htmlData.UserName,
" ist bereits vergeben. Bitte anderen Benutzernamen wählen.")
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData)
return
}
if pass != pass2 {
http.Error(w, "Die Passwörter stimmen nicht überein.", http.StatusBadRequest)
htmlData.Msg = "Die Passwörter stimmen nicht überein."
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData)
return
}
user.ID, err = db.AddFirstUser(user, pass)
htmlData.ID, err = db.AddFirstUser(htmlData.User, pass)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if user.ID == -1 {
http.Error(w, "Bitte ein Benutzerkonto von einem Administrator anlegen lassen.", http.StatusInternalServerError)
if htmlData.ID > 1 {
errString := "error: there is already a first user"
log.Println(errString)
http.Error(w, errString, http.StatusInternalServerError)
return
}
if err := saveSession(w, r, s, user); err != nil {
if err := saveSession(w, r, s, htmlData.User); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -246,11 +266,7 @@ func AddFirstUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", 0); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", 0)
}
}
@ -258,17 +274,15 @@ func ShowAllUsers(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.H
return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data := new(struct {
type htmlData struct {
Users map[int64]*b.User
Action string
})
}
data.Action = action
data := &htmlData{Action: action}
data.Users, err = db.GetAllUsers()
if err != nil {
log.Println(err)
@ -278,19 +292,13 @@ func ShowAllUsers(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.H
delete(data.Users, session.Values["id"].(int64))
tmpl, err := template.ParseFiles(c.WebDir + "/templates/show-all-users.html")
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data)
}
}
func EditUser(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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@ -309,11 +317,7 @@ func EditUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html")
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", user); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", user)
}
}
@ -321,65 +325,81 @@ func UpdateUser(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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
user := new(b.User)
user.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.StatusInternalServerError)
return
}
user.Role, err = strconv.Atoi(r.PostFormValue("role"))
role, err := strconv.Atoi(r.PostFormValue("role"))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
user.UserName = r.PostFormValue("username")
if len(user.UserName) == 0 {
http.Error(w, "Bitte den Benutzernamen ausfüllen.", http.StatusInternalServerError)
return
userData := UserData{
User: &b.User{
ID: id,
UserName: r.PostFormValue("username"),
FirstName: r.PostFormValue("first-name"),
LastName: r.PostFormValue("last-name"),
Role: role,
},
}
user.FirstName = r.PostFormValue("first-name")
user.LastName = r.PostFormValue("last-name")
if len(user.FirstName) == 0 || len(user.LastName) == 0 {
http.Error(w, "Bitte den vollständigen Namen ausfüllen.", http.StatusInternalServerError)
return
}
newPass := r.PostFormValue("password")
newPass2 := r.PostFormValue("password2")
userString, stringLen, ok := checkUserStrings(user)
if len(userData.UserName) == 0 || len(userData.FirstName) == 0 ||
len(userData.LastName) == 0 {
userData.Msg = "Alle Felder mit * müssen ausgefüllt sein."
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", userData.Msg)
return
}
userString, stringLen, ok := checkUserStrings(userData.User)
if !ok {
http.Error(w, fmt.Sprint(userString, " ist zu lang. Maximal ", stringLen, " Zeichen erlaubt."), http.StatusBadRequest)
userData.Msg = fmt.Sprint(userString, " ist zu lang. Maximal ",
stringLen, " Zeichen erlaubt.")
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", userData)
return
}
if id := db.GetID(user.UserName); id != 0 && id != user.ID {
http.Error(w, user.UserName+" ist bereits vergeben. Bitte anderen Benutzernamen wählen.", http.StatusBadRequest)
return
if id, ok := db.GetID(userData.UserName); ok {
if id != userData.ID {
userData.Msg = "Benutzername bereits vergeben."
userData.UserName = ""
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", userData)
return
}
}
if err = db.UpdateUserAttributes(user.ID, user.UserName, user.FirstName, user.LastName, newPass, newPass2, user.Role); err != nil {
log.Println("error: user:", user.ID, err)
http.Error(w, "Benutzerdaten konnten nicht aktualisiert werden.", http.StatusInternalServerError)
return
if err = db.UpdateUserAttributes(
userData.ID,
userData.UserName,
userData.FirstName,
userData.LastName,
newPass,
newPass2,
userData.Role); err != nil {
userData.Msg = "Aktualisierung der Benutzerdaten fehlgeschlagen."
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", userData)
}
tmpl := template.Must(template.ParseFiles(c.WebDir + "/templates/hub.html"))
if err = tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int)); 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))
}
}
@ -387,8 +407,6 @@ func DeleteUser(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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@ -407,10 +425,6 @@ func DeleteUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int)); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int))
}
}

View File

@ -0,0 +1,29 @@
package frontend
import (
"errors"
"html/template"
"net/http"
"github.com/gorilla/sessions"
b "streifling.com/jason/cpolis/cmd/backend"
)
// 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) (*sessions.Session, error) {
msg := "Keine gültige Session. Bitte erneut anmelden."
tmpl, tmplErr := template.ParseFiles(c.WebDir+"/templates/index.html", c.WebDir+"/templates/login.html")
session, err := s.Get(r, "cookie")
if err != nil {
template.Must(tmpl, tmplErr).ExecuteTemplate(w, "page-content", msg)
return nil, err
}
if session.IsNew {
template.Must(tmpl, tmplErr).ExecuteTemplate(w, "page-content", msg)
return session, errors.New("error: no existing session")
}
return session, nil
}

View File

@ -49,19 +49,15 @@ func main() {
http.FileServer(http.Dir(config.WebDir+"/static/"))))
mux.HandleFunc("/", f.HomePage(config, db, store))
mux.HandleFunc("GET /article/allow-edit/{id}", f.AllowEditArticle(config, db, store))
mux.HandleFunc("GET /article/all-published/review-edit", f.ShowPublishedArticles(config, db, store, "review-edit"))
mux.HandleFunc("GET /article/all-published/delete", f.ShowPublishedArticles(config, db, store, "review-delete"))
mux.HandleFunc("GET /article/all-published", f.ShowPublishedArticles(config, db, store))
mux.HandleFunc("GET /article/all-rejected", f.ShowRejectedArticles(config, db, store))
mux.HandleFunc("GET /article/all-unpublished-unrejected-and-published-rejected", f.ShowUnpublishedUnrejectedAndPublishedRejectedArticles(config, db, store))
mux.HandleFunc("GET /article/all-unpublished", f.ShowUnpublishedArticles(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-deletion/{id}", f.ReviewArticleForDeletion(config, db, store))
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/review-unpublished/{id}", f.ReviewUnpublishedArticle(config, db, store))
mux.HandleFunc("GET /article/serve/{id}", c.ServeArticle(config, db))
mux.HandleFunc("GET /article/write", f.WriteArticle(config, db, store))
mux.HandleFunc("GET /hub", f.ShowHub(config, db, store))
@ -85,7 +81,6 @@ func main() {
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 /pdf/upload", f.UploadPDF(config, store))
mux.HandleFunc("POST /tag/add", f.AddTag(config, db, store))
mux.HandleFunc("POST /user/add", f.AddUser(config, db, store))
mux.HandleFunc("POST /user/add-first", f.AddFirstUser(config, db, store))

View File

@ -21,21 +21,18 @@ CREATE TABLE issues (
);
CREATE TABLE articles (
id INT AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
description TEXT NOT NULL,
link VARCHAR(255),
enc_url VARCHAR(255),
enc_length INT,
enc_type VARCHAR(255),
published BOOL NOT NULL,
rejected BOOL NOT NULL,
author_id INT NOT NULL,
issue_id INT NOT NULL,
edited_id INT,
is_in_issue BOOL NOT NULL,
auto_generated BOOL NOT NULL,
id INT AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
description TEXT NOT NULL,
link VARCHAR(255),
enc_url VARCHAR(255),
enc_length INT,
enc_type VARCHAR(255),
published BOOL NOT NULL,
rejected BOOL NOT NULL,
author_id INT NOT NULL,
issue_id INT NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (author_id) REFERENCES users (id),
FOREIGN KEY (issue_id) REFERENCES issues (id)

69
go.mod
View File

@ -1,64 +1,59 @@
module streifling.com/jason/cpolis
go 1.22.6
toolchain go1.23.1
go 1.22.0
require (
firebase.google.com/go/v4 v4.14.1
git.streifling.com/jason/rss v0.1.3
github.com/BurntSushi/toml v1.3.2
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/google/uuid v1.6.0
github.com/gorilla/sessions v1.2.2
github.com/microcosm-cc/bluemonday v1.0.26
github.com/yuin/goldmark v1.7.4
golang.org/x/crypto v0.27.0
golang.org/x/term v0.24.0
google.golang.org/api v0.191.0
github.com/yuin/goldmark v1.7.0
golang.org/x/crypto v0.21.0
golang.org/x/term v0.18.0
google.golang.org/api v0.170.0
)
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
cloud.google.com/go v0.112.1 // indirect
cloud.google.com/go/compute v1.24.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/firestore v1.15.0 // indirect
cloud.google.com/go/iam v1.1.7 // indirect
cloud.google.com/go/longrunning v0.5.5 // indirect
cloud.google.com/go/storage v1.40.0 // indirect
github.com/MicahParks/keyfunc v1.9.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/logr v1.4.1 // 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/google/s2a-go v0.1.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
github.com/googleapis/gax-go/v2 v2.12.3 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.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
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/oauth2 v0.18.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/appengine v1.6.8 // 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
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2 // indirect
google.golang.org/grpc v1.62.1 // indirect
google.golang.org/protobuf v1.33.0 // indirect
)

164
go.sum
View File

@ -1,20 +1,18 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=
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=
cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=
cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4=
cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg=
cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/firestore v1.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBpbFF8=
cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk=
cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM=
cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA=
cloud.google.com/go/longrunning v0.5.5 h1:GOE6pZFdSrTb4KAiKnXsJBtlE6mEyaW44oKyMILWnOg=
cloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDnoTk0yawPBB7s=
cloud.google.com/go/storage v1.40.0 h1:VEpDQV5CJxFmJ6ueWNsKxcr1QAYOXEgxDa+sBbJahPw=
cloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g=
firebase.google.com/go/v4 v4.14.1 h1:4qiUETaFRWoFGE1XP5VbcEdtPX93Qs+8B/7KvP2825g=
firebase.google.com/go/v4 v4.14.1/go.mod h1:fgk2XshgNDEKaioKco+AouiegSI9oTWVqRaBdTTGBoM=
git.streifling.com/jason/rss v0.1.3 h1:fd3j4ZtcLehapcmmroo3AP3X34gRHC4xzpfV6bDV1ZU=
@ -27,15 +25,11 @@ github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x9
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/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=
@ -43,8 +37,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
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/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/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=
@ -67,6 +61,8 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
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.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
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=
@ -75,21 +71,22 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
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.5.5/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/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/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
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/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/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA=
github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4=
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/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
@ -107,98 +104,113 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
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=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
github.com/yuin/goldmark v1.7.0/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=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw=
go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
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/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
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/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
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/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
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/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.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-20201119102817-f84b799fce68/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/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
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/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
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/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.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/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/api v0.170.0 h1:zMaruDePM88zxZBG+NG8+reALO2rfLhe/JShitLyT48=
google.golang.org/api v0.170.0/go.mod h1:/xql9M2btF85xac/VAm4PsLMTLVGUOpq4BE9R8jyNy8=
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 v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
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/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y=
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s=
google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c h1:kaI7oewGK5YnVwj+Y+EJBO/YN1ht8iTL9XkFHtVZLsc=
google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c/go.mod h1:VQW3tUculP/D4B+xVCo+VgSq8As6wA9ZjHl//pmk+6s=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2 h1:9IZDv+/GcI6u+a4jRFRLxQs0RUCfavGfoOgEW6jpkI0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs=
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/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
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=
@ -208,8 +220,10 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
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=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
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=

View File

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

View File

@ -43,9 +43,7 @@ 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
go build -o $BIN_DIR/cpolis $CPOLIS_DIR/cmd/main.go
echo '\nSetting up system files...' >&2
sudo chown root:root $BIN_DIR/cpolis

View File

@ -2,71 +2,40 @@
@tailwind components;
@tailwind utilities;
body {
width: 800px;
@apply mx-auto text-slate-900;
}
h2 {
@apply font-bold mb-2 text-2xl;
}
h3 {
@apply font-bold mb-2 text-xl;
form {
@apply flex flex-col gap-y-3;
}
input[type="file"] {
@apply border rounded-md w-full;
}
input[type="password"],
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 {
@apply bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 h-32 rounded-md;
}
.btn-area-1 {
@apply grid grid-cols-1 gap-x-4 gap-y-1 mt-4;
@apply border h-32 rounded-md;
}
.btn-area {
@apply grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-1 mt-4;
}
.btn-area-3 {
@apply grid grid-cols-1 md:grid-cols-3 gap-x-4 gap-y-1 mt-4;
@apply flex gap-4 mt-4;
}
.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 {
@apply bg-slate-800 dark:bg-slate-200 hover:bg-slate-700 dark:hover:bg-slate-300 my-2 px-3 py-2 rounded-md text-slate-50 dark:text-slate-950 w-full;
}
.EasyMDEContainer .CodeMirror {
@apply bg-slate-50 dark:bg-slate-950 border-slate-200 dark:border-slate-800 text-slate-900 dark:text-slate-100
}
.EasyMDEContainer .cm-s-easymde .CodeMirror-cursor {
@apply border-slate-900 dark:border-slate-100
}
.EasyMDEContainer .editor-toolbar > * {
@apply text-slate-900 dark:text-slate-100
}
.EasyMDEContainer .editor-toolbar > .active, .editor-toolbar > button:hover, .editor-preview pre, .cm-s-easymde .cm-comment {
@apply bg-slate-100 dark:bg-slate-900
}
.EasyMDEContainer .CodeMirror-fullscreen {
@apply bg-slate-50 dark:bg-slate-950
}
.editor-toolbar {
@apply border border-slate-200 dark:border-slate-800
}
.editor-toolbar.fullscreen {
@apply bg-slate-50 dark:bg-slate-950
}
.editor-preview {
@apply bg-slate-50 dark:bg-slate-950
@apply bg-slate-800 border my-2 px-3 py-2 rounded-md text-slate-50 w-full hover:bg-slate-700;
}

View File

@ -2,7 +2,7 @@
<h2>Neuer Tag</h2>
<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">
<input class="action-btn" type="submit" value="Anlegen" hx-post="/tag/add" hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>

View File

@ -2,7 +2,7 @@
<h2>Neuer Benutzer</h2>
<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>
<label for="username">Benutzername</label>
<input class="w-full" required name="username" type="text" value="{{.UserName}}" />
@ -25,7 +25,7 @@
</div>
</div>
<div class="flex flex-wrap gap-4">
<div class="flex gap-4">
<div>
<input required id="author" name="role" type="radio" value="3" {{if eq .Role 3 }}checked{{end}} />
<label for="author">Autor</label>
@ -49,4 +49,11 @@
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
</div>
</form>
<script>
var msg = "{{.Msg}}";
if (msg != "") {
alert(msg);
}
</script>
{{end}}

View File

@ -7,7 +7,7 @@
<h3>Aktuelle Artikel</h3>
<div class="flex flex-col gap-4">
{{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>
<p>{{.Description}}</p>
</div>
@ -16,13 +16,8 @@
</div>
<div>
<h3>Titelseite</h3>
<div class="grid grid-cols-2 gap-4 items-center">
<input class="h-full" name="issue-title" placeholder="Titel" required type="text" />
<label class="btn text-center" for="image-upload">Titelbild</label>
<input class="hidden" id="image-upload" name="issue-image" type="file" required
hx-post="/issue/upload-image" />
</div>
<h3>Cover</h3>
<input id="image-upload" name="issue-image" type="file" required hx-post="/issue/upload-image" />
</div>
<div>

View File

@ -2,7 +2,7 @@
<h2>Profil bearbeiten</h2>
<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>
<label for="username">Benutzername</label>
<input class="w-full" name="username" type="text" value="{{.UserName}}" />

View File

@ -2,7 +2,7 @@
<h2>Profil von {{.FirstName}} {{.LastName}} bearbeiten</h2>
<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>
<label for="username">Benutzername</label>
<input class="w-full" name="username" type="text" value="{{.UserName}}" />
@ -29,7 +29,7 @@
</div>
</div>
<div class="flex flex-wrap gap-4">
<div class="flex gap-4">
<div>
<input required id="author" name="role" type="radio" value="3" {{if eq .Role 3 }}checked{{end}} />
<label for="author">Autor</label>

View File

@ -4,47 +4,34 @@
<form id="edit-area">
<div class="flex flex-col gap-y-1">
<label for="article-title">Titel</label>
<div class="grid grid-cols-2 gap-4 items-center">
<input name="article-title" type="text" value="{{.Article.Title}}" />
<label class="btn text-center" for="image-upload">Titelbild</label>
<input class="hidden" id="image-upload" name="issue-image" type="file" required
hx-post="/issue/upload-image" />
<!-- TODO: Route einfügen -->
</div>
<input name="article-title" type="text" value="{{.Title}}" />
</div>
<div class="flex flex-col gap-y-1">
<label for="article-description">Beschreibung</label>
<textarea name="article-description">{{.Article.Description}}</textarea>
<textarea name="article-description">{{.Description}}</textarea>
</div>
<div class="flex flex-col gap-y-1">
<label for="easyMDE">Artikel</label>
<textarea id="easyMDE">{{.Content}}</textarea>
<input id="article-content" name="article-content" type="hidden" value="{{.Content}}" />
<input id="article-content" name="article-content" type="hidden" />
</div>
<div>
<span>Tags</span>
<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}}
<div>
<input id="tag-{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" {{if index $.Selected
.ID}}checked{{end}} />
<label for="tag-{{.Name}}">{{.Name}}</label>
<input id="{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" />
<label for="{{.Name}}">{{.Name}}</label>
</div>
{{end}}
</div>
</div>
<div class="btn-area">
<input class="action-btn" type="submit" value="Senden" hx-post="/article/{{.Action}}"
hx-target="#page-content" />
<input class="action-btn" type="submit" value="Senden" hx-post="/article/submit" hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
</div>
</form>
@ -70,7 +57,6 @@
onSuccess(data);
})
.catch(error => {
htmx.trigger(htmx.find('#notification'), 'htmx:responseError', {xhr: {responseText: error.message}});
onError(error);
});
},

View File

@ -25,8 +25,15 @@
</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" />
</div>
</form>
<script>
var msg = "{{.Msg}}";
if (msg != "") {
alert(msg);
}
</script>
{{end}}

View File

@ -4,46 +4,44 @@
{{if lt . 4}}
<div class="mb-3">
<h2>Artikel</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-2">
<h2>Autor</h2>
<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="/article/all-rejected" hx-target="#page-content">Artikel bearbeiten</button>
{{if lt . 3}}<button class="btn" hx-get="/article/all-unpublished-unrejected-and-published-rejected"
hx-target="#page-content">Artikel veröffentlichen</button>{{end}}
{{if lt . 2}}<button class="btn" hx-get="/article/all-published/delete" hx-target="#page-content">Artikel
löschen</button>{{end}}
{{if lt . 2}}<button class="btn" hx-get="/article/all-published/review-edit"
hx-target="#page-content">Artikel bearbeiten lassen</button>{{end}}
{{if lt . 3}}<button class="btn" hx-get="/tag/create" hx-target="#page-content">Neuer Tag</button>{{end}}
<button class="btn" hx-get="/article/all-rejected" hx-target="#page-content">Abgelehnte Artikel</button>
<button class="btn" hx-get="/user/edit/self" hx-target="#page-content">Profil bearbeiten</button>
</div>
</div>
{{end}}
{{if lt . 3}}
<div class="mb-3">
<h2>Redakteur</h2>
<div class="grid grid-cols-2 gap-4">
<button class="btn" hx-get="/article/all-unpublished" hx-target="#page-content">
Unveröffentlichte Artikel
</button>
<button class="btn" hx-get="/tag/create" hx-target="#page-content">Neuer Tag</button>
</div>
</div>
{{end}}
{{if lt . 2}}
<div class="mb-3">
<h2>Ausgabe</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-2">
<h2>Herausgeber</h2>
<div class="grid grid-cols-2 gap-4">
<button class="btn" hx-get="/issue/this" hx-target="#page-content">Diese Ausgabe</button>
<form class="flex" hx-encoding="multipart/form-data">
<label class="btn text-center" for="pdf-upload">PDF hochladen</label>
<input accept=".pdf" class="hidden" id="pdf-upload" name="pdf-upload" type="file"
hx-post="/pdf/upload" />
</form>
<button class="btn" hx-get="/article/all-published" hx-target="#page-content">Artikel löschen</button>
</div>
{{end}}
</div>
{{end}}
{{if lt . 4}}
{{if eq . 0}}
<div class="mb-3">
<h2>Benutzer</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-2">
<button class="btn" hx-get="/user/edit/self" hx-target="#page-content">Mein Profil bearbeiten</button>
{{if eq . 0}}<button class="btn" hx-get="/user/create" hx-target="#page-content">Benutzer
hinzufügen</button>{{end}}
{{if eq . 0}}<button class="btn" hx-get="/user/show-all/edit" hx-target="#page-content">Benutzer
bearbeiten</button>{{end}}
{{if eq . 0}}<button class="btn" hx-get="/user/show-all/delete" hx-target="#page-content">Benutzer
löschen</button>{{end}}
<h2>Administrator</h2>
<div class="grid grid-cols-2 gap-x-4 gap-y-2">
<button class="btn" hx-get="/user/create" hx-target="#page-content">Benutzer hinzufügen</button>
<button class="btn" hx-get="/user/show-all/edit" hx-target="#page-content">Benutzer bearbeiten</button>
<button class="btn" hx-get="/user/show-all/delete" hx-target="#page-content">Benutzer löschen</button>
</div>
</div>
{{end}}

View File

@ -5,81 +5,32 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Orient Editor</title>
<link href="/web/static/css/style.css" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/easymde/dist/easymde.min.css">
</head>
<body
class="bg-slate-50 dark:bg-slate-950 container flex flex-col justify-between max-w-screen-lg min-h-screen mx-auto text-slate-900 dark:text-slate-100">
<body class="flex flex-col justify-between min-h-screen bg-slate-50">
<header class="my-8">
<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>
<main class="mx-4">
<div class="hidden bg-slate-950 dark:bg-slate-50 fixed font-bold p-4 right-8 rounded-lg shadow-lg text-slate-100 dark:text-slate-900 top-8 z-50"
id="notification">
</div>
<div id="page-content">
{{template "page-content" .}}
</div>
</main>
<footer class="text-center text-gray-500 my-8">
<p>&copy; 2024 Jason Streifling. Alle Rechte vorbehalten.</p>
<p>v0.12.0 - <strong>Alpha: Drastische Änderungen und Fehler vorbehalten.</strong></p>
<footer class="my-8">
<p class="text-center text-gray-500 dark:text-gray-400">
&copy; 2024 Jason Streifling. Alle Rechte vorbehalten.
</p>
<p class="text-center text-gray-500 dark:text-gray-400">
<strong>Hinweis:</strong> Diese Software befindet sich noch in der Entwicklung und kann Fehler enthalten.
</p>
</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>
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>
<script>
htmx.on('htmx:responseError', function (event) {
var notification = document.getElementById('notification');
notification.innerText = event.detail.xhr.responseText;
notification.classList.remove('hidden');
setTimeout(function () {
notification.classList.add('hidden');
}, 5000);
});
</script>
</body>
</html>

View File

@ -2,13 +2,11 @@
<h2>Anmeldung</h2>
<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="password" placeholder="Passwort" type="password" />
</div>
<div class="mt-2">
<input class="action-btn" type="submit" value="Anmelden" hx-post="/login" hx-target="#page-content" />
</div>
<input class="action-btn" type="submit" value="Anmelden" hx-post="/login" hx-target="#page-content" />
</form>
{{end}}

View File

@ -2,13 +2,12 @@
<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">
{{range .}}
<button class="btn" hx-get="/article/review-deletion/{{.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,5 +1,5 @@
{{define "page-content"}}
<h2>Artikel bearbeiten</h2>
<h2>Abgelehnte Artikel</h2>
<div class="flex flex-col gap-4">
{{range .RejectedArticles}}

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,73 @@
{{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 gap-y-1">
<label for="article-description">Beschreibung</label>
<textarea name="article-description">{{.Article.Description}}</textarea>
</div>
<div class="flex flex-col gap-y-1">
<label for="easyMDE">Artikel</label>
<textarea id="easyMDE">{{.Content}}</textarea>
<input id="article-content" name="article-content" type="hidden" />
</div>
<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="/article/resubmit/{{.Article.ID}}"
hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
</div>
</form>
<script>
document.getElementById('article-content').value = easyMDE.value();
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('article-content').value = easyMDE.value();
});
</script>
{{end}}

View File

@ -0,0 +1,36 @@
{{define "page-content"}}
<h2>Artikel löschen</h2>
<div>
<span>Titel</span>
<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
{{.Title}}
</div>
<span>Beschreibung</span>
<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
{{.Description}}
</div>
<span>Artikel</span>
<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
<div class="prose">
{{.Content}}
</div>
</div>
<span>Tags</span>
<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
{{range .Tags}}
{{.Name}}
<br>
{{end}}
</div>
<div class="btn-area">
<input class="action-btn" type="submit" value="Löschen" hx-get="/article/delete/{{.ID}}"
hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
</div>
</div>
{{end}}

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="/article/publish/{{.ID}}"
hx-target="#page-content" />
<input class="btn" type="submit" value="Ablehnen" hx-get="/article/reject/{{.ID}}" hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
</div>
</div>
{{end}}

View File

@ -1,5 +1,5 @@
{{define "page-content"}}
<h2>Artikel veröffentlichen</h2>
<h2>Unveröffentlichte Artikel</h2>
<div class="flex flex-col gap-4">
{{range .}}