Compare commits

..

81 Commits

Author SHA1 Message Date
d0c566f8df Merge branch 'devel' 2024-09-01 21:14:05 +02:00
0a387c1087 Remove unnecessery fmt.Println that was used for debugging 2024-09-01 21:13:52 +02:00
38caf02b2c Upgrade all dependencies 2024-09-01 20:56:30 +02:00
435157a42f Upgrade htmx version 2024-09-01 20:52:50 +02:00
4163afb0b7 Change footer in index.html to have version number on same line as warning 2024-09-01 20:51:35 +02:00
5e586aa49a Merge branch 'devel' 2024-09-01 18:51:33 +02:00
6cc0fb40d7 Add version number to index.html 2024-09-01 18:51:14 +02:00
66b2743d3d Merge branch 'devel' 2024-09-01 18:48:26 +02:00
18617f1dbc Give autogenerated articles tags as well 2024-09-01 18:48:18 +02:00
3723b2b5e6 Merge branch 'devel' 2024-09-01 18:18:18 +02:00
79ee20a50e Allow articles to be independent of an issue and allow a title for an issue 2024-09-01 18:18:07 +02:00
e05521591b Add route for PDF uploads 2024-09-01 18:16:26 +02:00
0c9a79e24a Allow for PDF uploads from hub 2024-09-01 18:16:09 +02:00
ce788bfd50 Merge branch 'devel' 2024-09-01 12:54:12 +02:00
8147d9bef6 Small bug fix in build process 2024-09-01 12:54:05 +02:00
230a6278cc Merge branch 'devel' 2024-09-01 12:49:30 +02:00
da0d65d40b Fixed bug where no RSS feed is being generated 2024-09-01 12:27:30 +02:00
42d6e0c198 Merge branch 'devel' 2024-08-31 12:17:41 +02:00
a2d219b2c0 Don't show autogenerated articles in list for deletion 2024-08-31 12:17:30 +02:00
e1af2979af Merge branch 'devel' 2024-08-31 11:27:15 +02:00
b02a882ed7 Fixed small bug that used article.Link instead of correct path for article deletion 2024-08-31 11:27:03 +02:00
f6dedc6f10 Merge branch 'devel' 2024-08-31 01:43:53 +02:00
edb448413b Remove deleted article file when deleting article 2024-08-31 01:42:37 +02:00
cdf0a49550 Merge branch 'devel' 2024-08-31 01:38:28 +02:00
222b791e90 Delete Ports from article links in articles.go and issues.go 2024-08-31 01:35:39 +02:00
c3c0650210 Merge branch 'devel' 2024-08-31 01:00:55 +02:00
bc58b1be44 Fixed small bug in ServeArticle where it was looking at the link to read the file 2024-08-31 01:00:39 +02:00
ab6b9b9a4f Add port to url of UploadArticleImage() 2024-08-31 00:52:10 +02:00
91ef195a56 Check if article has been published before adding it to RSS feed 2024-08-31 00:49:35 +02:00
d077f700d8 Merge branch 'devel' 2024-08-31 00:36:48 +02:00
e4589f3b84 Remove unnecessary mv from update.sh 2024-08-31 00:35:07 +02:00
cd67fe6df3 Delete unnecessary http:// from articles.go and issues.go 2024-08-31 00:22:32 +02:00
ec752b1c66 Merge branch 'devel' 2024-08-30 23:43:12 +02:00
4663cedec5 Store article content outside of DB and convert and serve on request from respective file 2024-08-30 23:43:01 +02:00
3f1b18c29f Instead of having entire articles in the RSS feed, items now contain just a link 2024-08-30 21:20:29 +02:00
4a11e1a497 Add warning text to index.html 2024-08-30 18:34:40 +02:00
a33e7f9896 Move images.go from frontend to calls 2024-08-30 16:06:19 +02:00
2b2ab0d428 small bug fixes 2024-08-30 15:58:09 +02:00
c52e35bf0b delete RSS button from hub 2024-08-30 15:44:12 +02:00
cddd88d2f6 restructure routes 2024-08-30 15:42:53 +02:00
be467521d9 Only provide link in item instead of the entire article via content 2024-08-30 15:17:14 +02:00
10d8fceb77 Fixed formatting errors in create_db.sql 2024-08-25 11:24:03 +02:00
46aef4f12f Merge branch 'devel' 2024-08-25 10:51:35 +02:00
714cdd9aaf Also delete article from RSS feed 2024-08-25 10:51:09 +02:00
1b29e328cf Merge branch 'devel' 2024-08-25 06:38:55 +02:00
5474b17ce5 Added ability to delete articles 2024-08-25 06:35:15 +02:00
a318a265d4 Cleanup 2024-08-25 06:34:13 +02:00
e50cb819f3 Merge branch 'devel' 2024-08-23 21:45:30 +02:00
365d5a68a1 Added config for Firebase key 2024-08-23 21:45:10 +02:00
b451da7e8e Fixed echo commands 2024-08-23 21:23:05 +02:00
6a4a592714 Fixed bug where tailwindcss was using a relative path 2024-08-23 21:14:25 +02:00
806cfb01bd Fixed bug where SYSTEMD_DIR is not defined 2024-08-23 21:09:36 +02:00
c32e38ca10 Merge branch 'devel' 2024-08-23 20:57:11 +02:00
c2cadd1542 Add messages for user 2024-08-23 20:56:44 +02:00
4cb2831e9a Minor changes 2024-08-23 20:51:33 +02:00
ee31a9f8e2 create check_dependency function 2024-08-23 20:36:01 +02:00
beec20cdda Make sure bin and service are owned by root 2024-08-23 20:34:13 +02:00
7ef957c2d7 Added comments for during the update for more context 2024-08-18 17:41:54 +02:00
d7c8c7a43a Merge branch 'devel' 2024-08-18 17:31:00 +02:00
d56cdc78eb Use directories that require administrative privileges 2024-08-18 17:30:43 +02:00
151d89d9f0 Create $EXTRACTION_PATH 2024-08-18 17:10:10 +02:00
8d2944d00c Only delete contents of $CPOLIS_PATH 2024-08-18 17:06:49 +02:00
9ddd8198ee Use /tmp directory for downloading .tar.gz archive 2024-08-18 17:03:31 +02:00
ff6c7a66d7 Fixed a typo 2024-08-18 16:59:49 +02:00
129c85929c Small cleanup 2024-08-18 13:56:41 +02:00
b1a6359473 Exit update script if any requirement is not met 2024-08-18 13:53:37 +02:00
b5d979dbf8 Created update script and systemd service 2024-08-18 13:49:31 +02:00
1cd3edc90c Merge branch 'devel' 2024-08-18 12:06:29 +02:00
77e8edbe16 Relocated ShowHub from articles.go to sessions.go because it has even less to do with articles than with sessions 2024-08-18 11:46:23 +02:00
8115c50974 Fixed a bug that let users get around verification. 2024-08-18 11:40:03 +02:00
61bfa85b13 Some cleaning up 2024-08-18 11:21:42 +02:00
cd27349d04 Created package "calls" and verification for frontend and calls 2024-08-18 11:20:06 +02:00
5b41892dff Renamed frontend/firebase.go to frontend/pdf.go 2024-08-17 21:00:00 +02:00
c7add76a12 Deleted ServePDFs() and added ServePDF() 2024-08-17 20:56:45 +02:00
f4ae2f9c04 Added rout for pdf list 2024-08-17 20:41:39 +02:00
64f85a34cb Added pdf directory to air config 2024-08-17 20:20:16 +02:00
0c87d91df1 Small error correction 2024-08-17 20:18:53 +02:00
472f00a107 Extracted verification logic for frontend into seperate function tokenIsVerified() and created ServePDFListe() 2024-08-17 20:15:35 +02:00
bebfe994ae First implementation of firebase auth 2024-08-17 18:21:56 +02:00
46bfd23b2a A bit of testing with firebase 2024-08-09 17:07:27 +02:00
0afbdde077 Change to remote htmx 2024-08-09 16:20:57 +02:00
41 changed files with 1613 additions and 487 deletions

View File

@ -4,15 +4,17 @@ tmp_dir = "tmp"
[build] [build]
args_bin = [ args_bin = [
"-articles tmp/articles",
"-desc 'Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität'", "-desc 'Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität'",
"-domain localhost", "-domain localhost",
"-key tmp/key.gob", "-key tmp/key.gob",
"-link https://distrikt-ni-st.de", "-link https://distrikt-ni-st.de",
"-log tmp/cpolis.log", "-log tmp/cpolis.log",
"-pdfs tmp/pdfs",
"-pics tmp/pics", "-pics tmp/pics",
"-rss tmp/orientexpress_alle.rss", "-rss tmp/orientexpress_alle.rss",
"-title 'Freimaurer Distrikt Niedersachsen und Sachsen-Anhalt'", "-title 'Freimaurer Distrikt Niedersachsen und Sachsen-Anhalt'",
"-web web" "-web web",
] ]
bin = "./tmp/main" bin = "./tmp/main"
cmd = "go build -o ./tmp/main ./cmd/main.go" cmd = "go build -o ./tmp/main ./cmd/main.go"

View File

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

View File

@ -12,12 +12,17 @@ type Article struct {
Title string Title string
Created time.Time Created time.Time
Description string Description string
Content string Link string
EncURL string
EncLength int
EncType string
Published bool Published bool
Rejected bool Rejected bool
ID int64 ID int64
AuthorID int64 AuthorID int64
IssueID int64 IssueID int64
IsInIssue bool
AutoGenerated bool
} }
func (db *DB) AddArticle(a *Article) (int64, error) { func (db *DB) AddArticle(a *Article) (int64, error) {
@ -26,8 +31,8 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
selectQuery := "SELECT id FROM issues WHERE published = false" selectQuery := "SELECT id FROM issues WHERE published = false"
insertQuery := ` insertQuery := `
INSERT INTO articles INSERT INTO articles
(title, description, content, published, rejected, author_id, issue_id) (title, description, link, enc_url, enc_length, enc_type, published, rejected, author_id, issue_id, is_in_issue, auto_generated)
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
` `
for i := 0; i < TxMaxRetries; i++ { for i := 0; i < TxMaxRetries; i++ {
@ -44,8 +49,9 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
return 0, fmt.Errorf("error getting issue ID when adding article to DB: %v", err) return 0, fmt.Errorf("error getting issue ID when adding article to DB: %v", err)
} }
result, err := tx.Exec(insertQuery, a.Title, a.Description, result, err := tx.Exec(insertQuery, a.Title, a.Description, a.Link,
a.Content, a.Published, a.Rejected, a.AuthorID, id) a.EncURL, a.EncLength, a.EncType, a.Published, a.Rejected, a.AuthorID, id,
a.IsInIssue, a.AutoGenerated)
if err != nil { if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
@ -79,7 +85,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
func (db *DB) GetArticle(id int64) (*Article, error) { func (db *DB) GetArticle(id int64) (*Article, error) {
query := ` query := `
SELECT title, created, description, content, published, author_id SELECT title, created, description, link, enc_url, enc_length, enc_type, published, author_id, issue_id, is_in_issue, auto_generated
FROM articles FROM articles
WHERE id = ? WHERE id = ?
` `
@ -90,7 +96,9 @@ func (db *DB) GetArticle(id int64) (*Article, error) {
var err error var err error
if err := row.Scan(&article.Title, &created, &article.Description, if err := row.Scan(&article.Title, &created, &article.Description,
&article.Content, &article.Published, &article.AuthorID); err != nil { &article.Link, &article.EncURL, &article.EncLength, &article.EncType,
&article.Published, &article.AuthorID, &article.IssueID,
&article.IsInIssue, &article.AutoGenerated); err != nil {
return nil, fmt.Errorf("error scanning article row: %v", err) return nil, fmt.Errorf("error scanning article row: %v", err)
} }
@ -105,7 +113,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) {
func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) { func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) {
query := ` query := `
SELECT id, title, created, description, content, author_id, issue_id SELECT id, title, created, description, link, enc_url, enc_length, enc_type, author_id, issue_id, is_in_issue, auto_generated
FROM articles FROM articles
WHERE published = ? WHERE published = ?
AND rejected = ? AND rejected = ?
@ -121,8 +129,9 @@ func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) {
var created []byte var created []byte
if err = rows.Scan(&article.ID, &article.Title, &created, if err = rows.Scan(&article.ID, &article.Title, &created,
&article.Description, &article.Content, &article.AuthorID, &article.Description, &article.Link, &article.EncURL, &article.EncLength,
&article.IssueID); err != nil { &article.EncType, &article.AuthorID, &article.IssueID,
&article.IsInIssue, &article.AutoGenerated); err != nil {
return nil, fmt.Errorf("error scanning article row: %v", err) return nil, fmt.Errorf("error scanning article row: %v", err)
} }
@ -143,9 +152,9 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) {
txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable} txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable}
issueQuery := "SELECT id FROM issues WHERE published = false" issueQuery := "SELECT id FROM issues WHERE published = false"
articlesQuery := ` articlesQuery := `
SELECT id, title, created, description, content, author_id SELECT id, title, created, description, link, enc_url, enc_length, enc_type, author_id, auto_generated
FROM articles FROM articles
WHERE issue_id = ? AND published = true WHERE issue_id = ? AND published = true AND is_in_issue = true
` `
for i := 0; i < TxMaxRetries; i++ { for i := 0; i < TxMaxRetries; i++ {
@ -177,7 +186,8 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) {
var created []byte var created []byte
if err = rows.Scan(&article.ID, &article.Title, &created, if err = rows.Scan(&article.ID, &article.Title, &created,
&article.Description, &article.Content, &article.AuthorID); err != nil { &article.Description, &article.Link, &article.EncURL, &article.EncLength,
&article.EncType, &article.AuthorID, &article.AutoGenerated); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
} }
@ -256,3 +266,21 @@ func (db *DB) AddArticleToCurrentIssue(id int64) error {
return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries) return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries)
} }
func (db *DB) DeleteArticle(id int64) error {
articlesTagsQuery := "DELETE FROM articles_tags WHERE article_id = ?"
_, 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)
}
return nil
}

View File

@ -11,12 +11,15 @@ import (
) )
type Config struct { type Config struct {
ArticleDir string
DBName string DBName string
Description string Description string
Domain string Domain string
FirebaseKey string
KeyFile string KeyFile string
Link string Link string
LogFile string LogFile string
PDFDir string
PicsDir string PicsDir string
Port string Port string
RSSFile string RSSFile string
@ -26,9 +29,12 @@ type Config struct {
func newConfig() *Config { func newConfig() *Config {
return &Config{ return &Config{
ArticleDir: "/var/www/cpolis/articles",
DBName: "cpolis", DBName: "cpolis",
FirebaseKey: "/var/www/cpolis/serviceAccountKey.json",
KeyFile: "/var/www/cpolis/cpolis.key", KeyFile: "/var/www/cpolis/cpolis.key",
LogFile: "/var/log/cpolis.log", LogFile: "/var/log/cpolis.log",
PDFDir: "/var/www/cpolis/pdfs",
PicsDir: "/var/www/cpolis/pics", PicsDir: "/var/www/cpolis/pics",
RSSFile: "/var/www/cpolis/cpolis.rss", RSSFile: "/var/www/cpolis/cpolis.rss",
WebDir: "/var/www/cpolis/web", WebDir: "/var/www/cpolis/web",
@ -73,19 +79,35 @@ func (c *Config) handleCliArgs() error {
var err error var err error
port := 8080 port := 8080
flag.StringVar(&c.ArticleDir, "articles", c.ArticleDir, "articles directory")
flag.StringVar(&c.DBName, "db", c.DBName, "DB name") flag.StringVar(&c.DBName, "db", c.DBName, "DB name")
flag.StringVar(&c.Description, "desc", c.Description, "Channel description") flag.StringVar(&c.Description, "desc", c.Description, "channel description")
flag.StringVar(&c.Domain, "domain", c.Domain, "domain name") flag.StringVar(&c.Domain, "domain", c.Domain, "domain name")
flag.StringVar(&c.FirebaseKey, "firebase", c.FirebaseKey, "Firebase service account key file")
flag.StringVar(&c.KeyFile, "key", c.KeyFile, "key file") flag.StringVar(&c.KeyFile, "key", c.KeyFile, "key file")
flag.StringVar(&c.Link, "link", c.Link, "Channel Link") flag.StringVar(&c.Link, "link", c.Link, "channel Link")
flag.StringVar(&c.LogFile, "log", c.LogFile, "log file") flag.StringVar(&c.LogFile, "log", c.LogFile, "log file")
flag.StringVar(&c.PDFDir, "pdfs", c.PDFDir, "pdf directory")
flag.StringVar(&c.PicsDir, "pics", c.PicsDir, "pictures directory") flag.StringVar(&c.PicsDir, "pics", c.PicsDir, "pictures directory")
flag.StringVar(&c.RSSFile, "rss", c.RSSFile, "RSS file") flag.StringVar(&c.RSSFile, "rss", c.RSSFile, "RSS file")
flag.StringVar(&c.Title, "title", c.Title, "Channel title") flag.StringVar(&c.Title, "title", c.Title, "channel title")
flag.StringVar(&c.WebDir, "web", c.WebDir, "web directory") flag.StringVar(&c.WebDir, "web", c.WebDir, "web directory")
flag.IntVar(&port, "port", port, "port") flag.IntVar(&port, "port", port, "port")
flag.Parse() flag.Parse()
c.ArticleDir, err = filepath.Abs(c.ArticleDir)
if err != nil {
return fmt.Errorf("error finding absolute path for articles directory: %v", err)
}
if err = os.MkdirAll(c.ArticleDir, 0755); err != nil {
return fmt.Errorf("error creating articles directory: %v", err)
}
c.FirebaseKey, err = filepath.Abs(c.FirebaseKey)
if err != nil {
return fmt.Errorf("error finding absolute path for Firebase service account key file: %v", err)
}
c.KeyFile, err = filepath.Abs(c.KeyFile) c.KeyFile, err = filepath.Abs(c.KeyFile)
if err != nil { if err != nil {
return fmt.Errorf("error finding absolute path for key file: %v", err) return fmt.Errorf("error finding absolute path for key file: %v", err)
@ -96,9 +118,20 @@ func (c *Config) handleCliArgs() error {
return fmt.Errorf("error finding absolute path for log file: %v", err) return fmt.Errorf("error finding absolute path for log file: %v", err)
} }
c.PDFDir, err = filepath.Abs(c.PDFDir)
if err != nil {
return fmt.Errorf("error finding absolute path for pdfs directory: %v", err)
}
if err = os.MkdirAll(c.PDFDir, 0755); err != nil {
return fmt.Errorf("error creating pdfs directory: %v", err)
}
c.PicsDir, err = filepath.Abs(c.PicsDir) c.PicsDir, err = filepath.Abs(c.PicsDir)
if err != nil { if err != nil {
return fmt.Errorf("error finding absolute path for pics dir: %v", err) return fmt.Errorf("error finding absolute path for pics directory: %v", err)
}
if err = os.MkdirAll(c.PicsDir, 0755); err != nil {
return fmt.Errorf("error creating pics directory: %v", err)
} }
c.Port = fmt.Sprint(":", port) c.Port = fmt.Sprint(":", port)
@ -110,7 +143,10 @@ func (c *Config) handleCliArgs() error {
c.WebDir, err = filepath.Abs(c.WebDir) c.WebDir, err = filepath.Abs(c.WebDir)
if err != nil { if err != nil {
return fmt.Errorf("error finding absolute path for web dir: %v", err) return fmt.Errorf("error finding absolute path for web directory: %v", err)
}
if err = os.MkdirAll(c.WebDir, 0755); err != nil {
return fmt.Errorf("error creating web directory: %v", err)
} }
return nil return nil

44
cmd/backend/firebase.go Normal file
View File

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

View File

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

55
cmd/calls/articles.go Normal file
View File

@ -0,0 +1,55 @@
package calls
import (
"fmt"
"log"
"net/http"
"os"
"strconv"
b "streifling.com/jason/cpolis/cmd/backend"
)
func ServeArticle(c *b.Config, db *b.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !tokenIsVerified(w, r, c) {
return
}
idString := r.PathValue("id")
id, err := strconv.ParseInt(idString, 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
article, err := db.GetArticle(id)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if !article.Published {
return
}
articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
contentBytes, err := os.ReadFile(articleAbsName)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
content, err := b.ConvertToHTML(string(contentBytes))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprint(w, content)
}
}

View File

@ -1,4 +1,4 @@
package frontend package calls
import ( import (
"log" "log"
@ -10,6 +10,10 @@ import (
func ServeImage(c *b.Config, s *b.CookieStore) http.HandlerFunc { func ServeImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if !tokenIsVerified(w, r, c) {
return
}
absFilepath, err := filepath.Abs(c.PicsDir) absFilepath, err := filepath.Abs(c.PicsDir)
if err != nil { if err != nil {
log.Println(err) log.Println(err)

48
cmd/calls/pdf.go Normal file
View File

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

17
cmd/calls/rss.go Normal file
View File

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

34
cmd/calls/verification.go Normal file
View File

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

View File

@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"html/template" "html/template"
"io" "io"
"io/fs"
"log" "log"
"net/http" "net/http"
"os" "os"
@ -23,29 +22,13 @@ const (
PreviewMode PreviewMode
) )
func ShowHub(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session, err := s.Get(r, "cookie") session, err := getSession(w, r, c, s)
if err != nil { if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
session.Values["article"] = nil
if err = session.Save(r, w); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", session.Values["role"].(int))
}
}
func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
type editorHTMLData struct { type editorHTMLData struct {
Title string Title string
Description string Description string
@ -55,13 +38,6 @@ func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
Mode int Mode int
} }
session, err := s.Get(r, "cookie")
if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
var data editorHTMLData var data editorHTMLData
if session.Values["article"] == nil { if session.Values["article"] == nil {
data = editorHTMLData{} data = editorHTMLData{}
@ -84,11 +60,9 @@ func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session, err := s.Get(r, "cookie") session, err := getSession(w, r, c, s)
if err != nil { if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html") return
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
} }
session.Values["article"] = nil session.Values["article"] = nil
@ -101,10 +75,11 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
article := &b.Article{ article := &b.Article{
Title: r.PostFormValue("article-title"), Title: r.PostFormValue("article-title"),
Description: r.PostFormValue("article-description"), Description: r.PostFormValue("article-description"),
Content: r.PostFormValue("article-content"),
Published: false, Published: false,
Rejected: false, Rejected: false,
AuthorID: session.Values["id"].(int64), AuthorID: session.Values["id"].(int64),
IsInIssue: r.PostFormValue("issue") == "on",
AutoGenerated: false,
} }
article.ID, err = db.AddArticle(article) article.ID, err = db.AddArticle(article)
@ -114,6 +89,20 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return return
} }
articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
if err = os.WriteFile(articleAbsName, []byte(r.PostFormValue("article-content")), 0644); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
article.Link = fmt.Sprint(c.Domain, "/article/serve/", article.ID)
if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "link", Value: article.Link}); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
r.ParseForm() r.ParseForm()
tags := make([]int64, 0) tags := make([]int64, 0)
for _, tag := range r.Form["tags"] { for _, tag := range r.Form["tags"] {
@ -139,6 +128,11 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s)
if err != nil {
return
}
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
@ -150,11 +144,18 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
description := r.PostFormValue("article-description") description := r.PostFormValue("article-description")
content := r.PostFormValue("article-content") content := r.PostFormValue("article-content")
link := fmt.Sprint(c.ArticleDir, "/", id, ".md")
if err = os.WriteFile(link, []byte(content), 0644); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = db.UpdateAttributes( if err = db.UpdateAttributes(
&b.Attribute{Table: "articles", ID: id, AttName: "title", Value: title}, &b.Attribute{Table: "articles", ID: id, AttName: "title", Value: title},
&b.Attribute{Table: "articles", ID: id, AttName: "description", Value: description}, &b.Attribute{Table: "articles", ID: id, AttName: "description", Value: description},
&b.Attribute{Table: "articles", ID: id, AttName: "content", Value: content},
&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false}, &b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false},
&b.Attribute{Table: "articles", ID: id, AttName: "is_in_issue", Value: r.PostFormValue("issue") == "on"},
); err != nil { ); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -178,21 +179,18 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return return
} }
session, err := s.Get(r, "cookie")
if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html") tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err) tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"]) tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
} }
} }
func ShowUnpublishedArticles(c *b.Config, db *b.DB) http.HandlerFunc { func ShowUnpublishedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil {
return
}
unpublishedArticles, err := db.GetCertainArticles(false, false) unpublishedArticles, err := db.GetCertainArticles(false, false)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
@ -208,19 +206,17 @@ func ShowUnpublishedArticles(c *b.Config, db *b.DB) http.HandlerFunc {
func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s)
if err != nil {
return
}
type htmlData struct { type htmlData struct {
MyIDs map[int64]bool MyIDs map[int64]bool
RejectedArticles []*b.Article RejectedArticles []*b.Article
} }
data := new(htmlData) data := new(htmlData)
session, err := s.Get(r, "cookie")
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.RejectedArticles, err = db.GetCertainArticles(false, true) data.RejectedArticles, err = db.GetCertainArticles(false, true)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
@ -243,6 +239,297 @@ func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerF
func ReviewUnpublishedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { func ReviewUnpublishedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil {
return
}
data := new(struct {
Article *b.Article
Content template.HTML
Tags []*b.Tag
})
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Article, err = db.GetArticle(id)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
articleAbsName := fmt.Sprint(c.ArticleDir, "/", data.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.Article.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 {
return
}
data := new(struct {
Selected map[int64]bool
Article *b.Article
Content string
Tags []*b.Tag
})
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Article, err = db.GetArticle(id)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
articleAbsName := fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md")
contentBytes, err := os.ReadFile(articleAbsName)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Content = string(contentBytes)
data.Tags, err = db.GetTagList()
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
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/rework-article.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", data)
}
}
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 {
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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = db.UpdateAttributes(
&b.Attribute{Table: "articles", ID: id, AttName: "published", Value: true},
&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false},
&b.Attribute{Table: "articles", ID: id, AttName: "created", Value: time.Now().Format("2006-01-02 15:04:05")},
); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
feed, err := b.GenerateRSS(c, db)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = b.SaveRSS(c.RSSFile, feed); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
}
}
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 {
return
}
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = db.UpdateAttributes(
&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: true},
); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
}
}
func 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 {
return
}
articles, err := db.GetCurrentIssueArticles()
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/current-articles.html")
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 {
return
}
file, header, err := r.FormFile("article-image")
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
defer file.Close()
nameStrings := strings.Split(header.Filename, ".")
extension := "." + nameStrings[len(nameStrings)-1]
filename := fmt.Sprint(uuid.New(), extension)
absFilepath, err := filepath.Abs(fmt.Sprint(c.PicsDir, "/", filename))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
img, err := os.Create(absFilepath)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer img.Close()
if _, err = io.Copy(img, file); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
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) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil {
return
}
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 {
filteredArticles = append(filteredArticles, article)
}
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/published-articles.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", filteredArticles)
}
}
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 {
return
}
type htmlData struct { type htmlData struct {
Title string Title string
Description string Description string
@ -282,7 +569,15 @@ func ReviewUnpublishedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Hand
return return
} }
content, err := b.ConvertToHTML(article.Content) 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 { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -297,20 +592,18 @@ func ReviewUnpublishedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Hand
return return
} }
tmpl, err := template.ParseFiles(c.WebDir + "/templates/to-be-published.html") tmpl, err := template.ParseFiles(c.WebDir + "/templates/to-be-deleted.html")
tmpl = template.Must(tmpl, err) tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", data) tmpl.ExecuteTemplate(w, "page-content", data)
} }
} }
func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { func DeleteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
type htmlData struct { session, err := getSession(w, r, c, s)
Selected map[int64]bool if err != nil {
Article *b.Article return
Tags []*b.Tag
} }
data := new(htmlData)
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil { if err != nil {
@ -319,70 +612,19 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
return return
} }
data.Article, err = db.GetArticle(id) if err = db.DeleteArticle(id); err != nil {
if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
data.Tags, err = db.GetTagList() if err = os.Remove(fmt.Sprint(c.ArticleDir, "/", id, ".md")); err != nil {
if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
selectedTags, err := db.GetArticleTags(id) feed, err := b.GenerateRSS(c, db)
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
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/rework-article.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", data)
}
}
func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session, err := s.Get(r, "cookie")
if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
if err = db.AddArticleToCurrentIssue(id); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = db.UpdateAttributes(
&b.Attribute{Table: "articles", ID: id, AttName: "published", Value: true},
&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false},
&b.Attribute{Table: "articles", ID: id, AttName: "created", Value: time.Now().Format("2006-01-02 15:04:05")},
); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
feed, err := b.GenerateRSS(db, c.Title, c.Link, c.Description)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -396,96 +638,6 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html") tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err) tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"]) tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int))
}
}
func RejectArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session, err := s.Get(r, "cookie")
if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
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
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
}
}
func ShowCurrentArticles(c *b.Config, db *b.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
articles, err := db.GetCurrentIssueArticles()
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/current-articles.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", articles)
}
}
func UploadImage(c *b.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
file, header, err := r.FormFile("article-image")
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer file.Close()
nameStrings := strings.Split(header.Filename, ".")
extension := "." + nameStrings[len(nameStrings)-1]
filename := fmt.Sprint(uuid.New(), extension)
absFilepath, err := filepath.Abs(fmt.Sprint(c.PicsDir, "/", filename))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = os.MkdirAll(fmt.Sprint(c.PicsDir, "/"), fs.FileMode(0755)); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
img, err := os.Create(absFilepath)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer img.Close()
if _, err = io.Copy(img, file); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
url := fmt.Sprint(c.Domain, "/pics/", filename)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(url)
} }
} }

View File

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

View File

@ -1,26 +1,131 @@
package frontend package frontend
import ( import (
"fmt"
"html/template" "html/template"
"io"
"log" "log"
"mime"
"net/http" "net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/google/uuid"
b "streifling.com/jason/cpolis/cmd/backend" b "streifling.com/jason/cpolis/cmd/backend"
) )
func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s)
if err != nil {
return
}
session.Values["article"] = nil
if err = session.Save(r, w); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if session.Values["issue-image"] == nil {
err := "error: Image required"
log.Println(err)
http.Error(w, err, http.StatusBadRequest)
return
}
imgFileName := session.Values["issue-image"].(string)
imgAbsName := fmt.Sprint(c.PicsDir, "/", imgFileName)
imgFile, err := os.Open(imgAbsName)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer imgFile.Close()
imgInfo, err := imgFile.Stat()
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
imgSize := imgInfo.Size()
mimeType := mime.TypeByExtension(filepath.Ext(imgAbsName))
article := &b.Article{
Title: r.PostFormValue("issue-title"),
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),
AutoGenerated: true,
}
article.ID, err = db.AddArticle(article)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
if err = os.WriteFile(articleAbsName, []byte(r.PostFormValue("article-content")), 0644); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
article.Link = fmt.Sprint(c.Domain, "/article/serve/", article.ID)
if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "link", Value: article.Link}); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "link", Value: article.Link}); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = db.AddArticleToCurrentIssue(article.ID); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := db.PublishLatestIssue(); err != nil { if err := db.PublishLatestIssue(); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
session, err := s.Get(r, "cookie") feed, err := b.GenerateRSS(c, db)
if err != nil { if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html") log.Println(err)
msg := "Session nicht mehr gültig. Bitte erneut anmelden." http.Error(w, err.Error(), http.StatusInternalServerError)
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg) return
}
if err = b.SaveRSS(c.RSSFile, feed); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session.Values["issue-image"] = nil
if err = session.Save(r, w); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
} }
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html") tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
@ -28,3 +133,59 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"]) tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
} }
} }
func UploadIssueImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s)
if err != nil {
return
}
if err := r.ParseMultipartForm(10 << 20); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
file, header, err := r.FormFile("issue-image")
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer file.Close()
nameStrings := strings.Split(header.Filename, ".")
extension := "." + nameStrings[len(nameStrings)-1]
filename := fmt.Sprint(uuid.New(), extension)
absFilepath, err := filepath.Abs(fmt.Sprint(c.PicsDir, "/", filename))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
img, err := os.Create(absFilepath)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer img.Close()
if _, err = io.Copy(img, file); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session.Values["issue-image"] = filename
if err = session.Save(r, w); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
}

59
cmd/frontend/pdf.go Normal file
View File

@ -0,0 +1,59 @@
package frontend
import (
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"github.com/google/uuid"
b "streifling.com/jason/cpolis/cmd/backend"
)
func UploadPDF(c *b.Config, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil {
return
}
if err := r.ParseMultipartForm(10 << 20); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
file, _, err := r.FormFile("pdf-upload")
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer file.Close()
filename := fmt.Sprint(uuid.New(), ".pdf")
absFilepath, err := filepath.Abs(fmt.Sprint(c.PDFDir, "/", filename))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
pdf, err := os.Create(absFilepath)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer pdf.Close()
if _, err = io.Copy(pdf, file); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
}

View File

@ -60,7 +60,7 @@ func Login(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
id, ok := db.GetID(userName) id, ok := db.GetID(userName)
if !ok { if !ok {
http.Error(w, fmt.Sprintf("no such user: %v", userName), http.StatusInternalServerError) http.Error(w, fmt.Sprintf("no such user: %v", userName), http.StatusBadRequest)
return return
} }
@ -90,15 +90,12 @@ func Login(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func Logout(c *b.Config, s *b.CookieStore) http.HandlerFunc { func Logout(c *b.Config, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
session, err := s.Get(r, "cookie") session, err := getSession(w, r, c, s)
if err != nil { if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html") return
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
} }
session.Options.MaxAge = -1 session.Options.MaxAge = -1
if err = session.Save(r, w); err != nil { if err = session.Save(r, w); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -109,3 +106,22 @@ func Logout(c *b.Config, s *b.CookieStore) http.HandlerFunc {
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil) template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil)
} }
} }
func ShowHub(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s)
if err != nil {
return
}
session.Values["article"] = nil
if err = session.Save(r, w); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", session.Values["role"].(int))
}
}

View File

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

View File

@ -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

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

9
cpolis.service Normal file
View File

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

View File

@ -11,13 +11,13 @@ CREATE TABLE users (
first_name VARCHAR(50) NOT NULL, first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL, last_name VARCHAR(50) NOT NULL,
role INT NOT NULL, role INT NOT NULL,
PRIMARY KEY(id) PRIMARY KEY (id)
); );
CREATE TABLE issues ( CREATE TABLE issues (
id INT AUTO_INCREMENT, id INT AUTO_INCREMENT,
published BOOL NOT NULL, published BOOL NOT NULL,
PRIMARY KEY(id) PRIMARY KEY (id)
); );
CREATE TABLE articles ( CREATE TABLE articles (
@ -25,26 +25,31 @@ CREATE TABLE articles (
title VARCHAR(255) NOT NULL, title VARCHAR(255) NOT NULL,
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
description TEXT NOT NULL, description TEXT NOT NULL,
content TEXT NOT NULL, link VARCHAR(255),
enc_url VARCHAR(255),
enc_length INT,
enc_type VARCHAR(255),
published BOOL NOT NULL, published BOOL NOT NULL,
rejected BOOL NOT NULL, rejected BOOL NOT NULL,
author_id INT NOT NULL, author_id INT NOT NULL,
issue_id INT NOT NULL, issue_id INT NOT NULL,
PRIMARY KEY(id), is_in_issue BOOL NOT NULL,
FOREIGN KEY(author_id) REFERENCES users(id), auto_generated BOOL NOT NULL,
FOREIGN KEY(issue_id) REFERENCES issues(id) PRIMARY KEY (id),
FOREIGN KEY (author_id) REFERENCES users (id),
FOREIGN KEY (issue_id) REFERENCES issues (id)
); );
CREATE TABLE tags ( CREATE TABLE tags (
id INT AUTO_INCREMENT, id INT AUTO_INCREMENT,
name VARCHAR(50) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL UNIQUE,
PRIMARY KEY(id) PRIMARY KEY (id)
); );
CREATE TABLE articles_tags ( CREATE TABLE articles_tags (
article_id INT, article_id INT,
tag_id INT, tag_id INT,
PRIMARY KEY(article_id, tag_id), PRIMARY KEY (article_id, tag_id),
FOREIGN KEY(article_id) REFERENCES articles(id), FOREIGN KEY (article_id) REFERENCES articles (id),
FOREIGN KEY(tag_id) REFERENCES tags(id) FOREIGN KEY (tag_id) REFERENCES tags (id)
); );

63
go.mod
View File

@ -1,23 +1,62 @@
module streifling.com/jason/cpolis module streifling.com/jason/cpolis
go 1.22.0 go 1.23
toolchain go1.23.0
require ( require (
git.streifling.com/jason/rss v0.1.2 firebase.google.com/go/v4 v4.14.1
github.com/BurntSushi/toml v1.3.2 git.streifling.com/jason/rss v0.1.3
github.com/go-sql-driver/mysql v1.7.1 github.com/BurntSushi/toml v1.4.0
github.com/go-sql-driver/mysql v1.8.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/sessions v1.2.2 github.com/gorilla/sessions v1.4.0
github.com/microcosm-cc/bluemonday v1.0.26 github.com/microcosm-cc/bluemonday v1.0.27
github.com/yuin/goldmark v1.7.0 github.com/yuin/goldmark v1.7.4
golang.org/x/crypto v0.14.0 golang.org/x/crypto v0.26.0
golang.org/x/term v0.17.0 golang.org/x/term v0.23.0
google.golang.org/api v0.195.0
) )
require ( require (
cloud.google.com/go v0.115.1 // indirect
cloud.google.com/go/auth v0.9.2 // 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.2.0 // indirect
cloud.google.com/go/longrunning v0.6.0 // indirect
cloud.google.com/go/storage v1.43.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/MicahParks/keyfunc v1.9.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
github.com/gorilla/css v1.0.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/s2a-go v0.1.8 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.3 // indirect
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect
golang.org/x/net v0.17.0 // indirect go.opencensus.io v0.24.0 // indirect
golang.org/x/sys v0.17.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
go.opentelemetry.io/otel v1.29.0 // indirect
go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/oauth2 v0.22.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/text v0.17.0 // indirect
golang.org/x/time v0.6.0 // indirect
google.golang.org/appengine/v2 v2.0.6 // indirect
google.golang.org/genproto v0.0.0-20240827150818-7e3bb234dfed // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed // indirect
google.golang.org/grpc v1.66.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
) )

243
go.sum
View File

@ -1,30 +1,229 @@
git.streifling.com/jason/rss v0.1.2 h1:UB3UHJXMt5WDDh9y8n0Z6nS1XortbPXjEr7QZTdovY4= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
git.streifling.com/jason/rss v0.1.2/go.mod h1:gpZF0nZbQSstMpyHD9DTAvlQEG7v4pjO5c7aIMWM4Jg= cloud.google.com/go v0.115.1 h1:Jo0SM9cQnSkYfp44+v+NQXHpcHqlnRJk2qxh6yvxxxQ=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= cloud.google.com/go v0.115.1/go.mod h1:DuujITeaufu3gL68/lOFIirVNJwQeyf5UXyi+Wbgknc=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= cloud.google.com/go/auth v0.9.2 h1:I+Rq388FYU8QdbVB1IiPd+6KNdrqtAPE/asiKHShBLM=
cloud.google.com/go/auth v0.9.2/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842BgCsmTk=
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.2.0 h1:kZKMKVNk/IsSSc/udOb83K0hL/Yh/Gcqpz+oAkoIFN8=
cloud.google.com/go/iam v1.2.0/go.mod h1:zITGuWgsLZxd8OwAlX+eMFgZDXzBm7icj1PVTYG766Q=
cloud.google.com/go/longrunning v0.6.0 h1:mM1ZmaNsQsnb+5n1DNPeL0KwQd9jQRqSqSDEkBZr+aI=
cloud.google.com/go/longrunning v0.6.0/go.mod h1:uHzSZqW89h7/pasCWNYdUpwGz3PcVWhrWupreVPYLts=
cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs=
cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
firebase.google.com/go/v4 v4.14.1 h1:4qiUETaFRWoFGE1XP5VbcEdtPX93Qs+8B/7KvP2825g=
firebase.google.com/go/v4 v4.14.1/go.mod h1:fgk2XshgNDEKaioKco+AouiegSI9oTWVqRaBdTTGBoM=
git.streifling.com/jason/rss v0.1.3 h1:fd3j4ZtcLehapcmmroo3AP3X34gRHC4xzpfV6bDV1ZU=
git.streifling.com/jason/rss v0.1.3/go.mod h1:gpZF0nZbQSstMpyHD9DTAvlQEG7v4pjO5c7aIMWM4Jg=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.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 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/googleapis/enterprise-certificate-proxy v0.3.3 h1:QRje2j5GZimBzlbhGA2V2QlGNgL8G6e+wGo/+/2bWI0=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/googleapis/enterprise-certificate-proxy v0.3.3/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=
github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 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.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-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.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.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.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
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.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/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.195.0 h1:Ude4N8FvTKnnQJHU48RFI40jOBgIrL8Zqr3/QeST6yU=
google.golang.org/api v0.195.0/go.mod h1:DOGRWuv3P8TU8Lnz7uQc4hyNqrBpMtD9ppW3wBJurgc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw=
google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI=
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-20240827150818-7e3bb234dfed h1:4C4dbrVFtfIp3GXJdMX1Sj25mahfn5DywOo65/2ISQ8=
google.golang.org/genproto v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:ICjniACoWvcDz8c8bOsHVKuuSGDJy1z5M4G0DM3HzTc=
google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed h1:3RgNmBoI9MZhsj3QxC+AP/qQhNwpCLOvYDYYsFrhFt0=
google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed h1:J6izYgfBXAI3xTKLgxzTmUltdYaLsuBxFCgDHWJ/eXg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
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.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c=
google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

58
update.sh Executable file
View File

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

File diff suppressed because one or more lines are too long

View File

@ -4,7 +4,7 @@
<form> <form>
<input required name="tag" placeholder="Tag eingeben" type="text" /> <input required name="tag" placeholder="Tag eingeben" type="text" />
<div class="btn-area"> <div class="btn-area">
<input class="action-btn" type="submit" value="Anlegen" hx-post="/add-tag" hx-target="#page-content" /> <input class="action-btn" type="submit" value="Anlegen" hx-post="/tag/add" hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button> <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
</div> </div>
</form> </form>

View File

@ -45,7 +45,7 @@
</div> </div>
<div class="btn-area"> <div class="btn-area">
<input class="action-btn" type="submit" value="Anlegen" hx-post="/add-user" hx-target="#page-content" /> <input class="action-btn" type="submit" value="Anlegen" hx-post="/user/add" hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button> <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
</div> </div>
</form> </form>

View File

@ -1,17 +1,79 @@
{{define "page-content"}} {{define "page-content"}}
<h2>Aktuelle Artikel</h2> <h2>Diese Ausgabe</h2>
<div class="flex flex-col gap-4"> <form hx-encoding="multipart/form-data">
<div class="flex flex-col gap-4">
<div>
<h3>Aktuelle Artikel</h3>
<div class="flex flex-col gap-4">
{{range .}} {{range .}}
<div class="border px-2 py-1 rounded-md"> <div class="border px-2 py-1 rounded-md">
<h1 class="font-bold text-2xl">{{.Title}}</h1> <h1 class="font-bold text-2xl">{{.Title}}</h1>
<p>{{.Description}}</p> <p>{{.Description}}</p>
</div> </div>
{{end}} {{end}}
</div> </div>
</div>
<div class="btn-area"> <div>
<button class="action-btn" hx-get="/publish-issue" hx-target="#page-content">Ausgabe publizieren</button> <h3>Cover</h3>
<div class="flex">
<label class="btn text-center" for="image-upload">Bild hochladen</label>
<input class="hidden" id="image-upload" name="issue-image" type="file" required
hx-post="/issue/upload-image" />
</div>
</div>
<div>
<h3>Titel</h3>
<div class="flex flex-col gap-y-1">
<input name="issue-title" type="text" />
</div>
</div>
<div>
<h3>Über diese Ausgabe</h3>
<div>
<textarea id="easyMDE" placeholder="Beschreibung dieser Ausgabe"></textarea>
<input id="issue-content" name="issue-content" type="hidden" />
</div>
</div>
</div>
<div class="btn-area">
<button class="action-btn" hx-post="/issue/publish" hx-target="#page-content">Ausgabe publizieren</button>
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button> <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
</div> </div>
</form>
<script>
var easyMDE = new EasyMDE({
element: document.getElementById('easyMDE'),
hideIcons: ['image'],
imageTexts: {sbInit: ''},
showIcons: ["code", "table", "upload-image"],
uploadImage: true,
imageUploadFunction: function (file, onSuccess, onError) {
var formData = new FormData();
formData.append('article-image', file);
fetch('/article/upload-image', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
onSuccess(data);
})
.catch(error => {
onError(error);
});
},
});
easyMDE.codemirror.on("change", () => {
document.getElementById('issue-content').value = easyMDE.value();
});
</script>
{{end}} {{end}}

View File

@ -35,7 +35,7 @@
</div> </div>
<div class="btn-area"> <div class="btn-area">
<input class="action-btn" type="submit" value="Aktualisieren" hx-post="/update-self" <input class="action-btn" type="submit" value="Aktualisieren" hx-post="/user/update/self"
hx-target="#page-content" /> hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button> <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
</div> </div>

View File

@ -49,7 +49,7 @@
</div> </div>
<div class="btn-area"> <div class="btn-area">
<input class="action-btn" type="submit" value="Aktualisieren" hx-post="/update-user/{{.ID}}" <input class="action-btn" type="submit" value="Aktualisieren" hx-post="/user/update/{{.ID}}"
hx-target="#page-content" /> hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button> <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
</div> </div>

View File

@ -6,19 +6,26 @@
<label for="article-title">Titel</label> <label for="article-title">Titel</label>
<input name="article-title" type="text" value="{{.Title}}" /> <input name="article-title" type="text" value="{{.Title}}" />
</div> </div>
<div class="flex flex-col">
<div class="flex flex-col gap-y-1">
<label for="article-description">Beschreibung</label> <label for="article-description">Beschreibung</label>
<textarea name="article-description">{{.Description}}</textarea> <textarea name="article-description">{{.Description}}</textarea>
</div> </div>
<div class="flex flex-col">
<label for="article-content">Artikel</label> <div class="flex flex-col gap-y-1">
<label for="easyMDE">Artikel</label>
<textarea id="easyMDE">{{.Content}}</textarea> <textarea id="easyMDE">{{.Content}}</textarea>
</div>
<input id="article-content" name="article-content" type="hidden" /> <input id="article-content" name="article-content" type="hidden" />
</div>
<div> <div>
<span>Tags</span> <span>Tags</span>
<div class="flex flex-wrap gap-x-4"> <div class="flex flex-wrap gap-x-4">
<div>
<input id="issue" name="issue" type="checkbox" />
<label for="issue">Orient Express</label>
</div>
{{range .Tags}} {{range .Tags}}
<div> <div>
<input id="{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" /> <input id="{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" />
@ -29,7 +36,7 @@
</div> </div>
<div class="btn-area"> <div class="btn-area">
<input class="action-btn" type="submit" value="Senden" hx-post="/submit-article" hx-target="#page-content" /> <input class="action-btn" type="submit" value="Senden" hx-post="/article/submit" hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button> <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
</div> </div>
</form> </form>
@ -46,7 +53,7 @@
var formData = new FormData(); var formData = new FormData();
formData.append('article-image', file); formData.append('article-image', file);
fetch('/upload-image', { fetch('/article/upload-image', {
method: 'POST', method: 'POST',
body: formData body: formData
}) })

View File

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

View File

@ -6,10 +6,9 @@
<div class="mb-3"> <div class="mb-3">
<h2>Autor</h2> <h2>Autor</h2>
<div class="grid grid-cols-2 gap-x-4 gap-y-2"> <div class="grid grid-cols-2 gap-x-4 gap-y-2">
<button class="btn" hx-get="/write-article" hx-target="#page-content">Artikel schreiben</button> <button class="btn" hx-get="/article/write" hx-target="#page-content">Artikel schreiben</button>
<button class="btn" hx-get="/rejected-articles" hx-target="#page-content">Abgelehnte Artikel</button> <button class="btn" hx-get="/article/all-rejected" hx-target="#page-content">Abgelehnte Artikel</button>
<a class="btn text-center" href="/rss">RSS Feed</a> <button class="btn" hx-get="/user/edit/self" hx-target="#page-content">Profil bearbeiten</button>
<button class="btn" hx-get="/edit-self" hx-target="#page-content">Profil bearbeiten</button>
</div> </div>
</div> </div>
{{end}} {{end}}
@ -17,11 +16,11 @@
{{if lt . 3}} {{if lt . 3}}
<div class="mb-3"> <div class="mb-3">
<h2>Redakteur</h2> <h2>Redakteur</h2>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-x-4 gap-y-2">
<button class="btn" hx-get="/unpublished-articles" hx-target="#page-content"> <button class="btn" hx-get="/article/all-unpublished" hx-target="#page-content">
Unveröffentlichte Artikel Unveröffentlichte Artikel
</button> </button>
<button class="btn" hx-get="/create-tag" hx-target="#page-content">Neuer Tag</button> <button class="btn" hx-get="/tag/create" hx-target="#page-content">Neuer Tag</button>
</div> </div>
</div> </div>
{{end}} {{end}}
@ -29,8 +28,14 @@
{{if lt . 2}} {{if lt . 2}}
<div class="mb-3"> <div class="mb-3">
<h2>Herausgeber</h2> <h2>Herausgeber</h2>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-x-4 gap-y-2">
<button class="btn" hx-get="/this-issue" hx-target="#page-content">Diese Ausgabe</button> <button class="btn" hx-get="/issue/this" hx-target="#page-content">Diese Ausgabe</button>
<button class="btn" hx-get="/article/all-published" hx-target="#page-content">Artikel löschen</button>
<form class="flex" hx-encoding="multipart/form-data">
<label class="btn text-center" for="pdf-upload">PDF hochladen</label>
<input accept=".pdf" class="hidden" id="pdf-upload" name="pdf-upload" type="file"
hx-post="/pdf/upload" />
</form>
</div> </div>
</div> </div>
{{end}} {{end}}
@ -38,10 +43,10 @@
{{if eq . 0}} {{if eq . 0}}
<div class="mb-3"> <div class="mb-3">
<h2>Administrator</h2> <h2>Administrator</h2>
<div class="grid grid-cols-3 gap-4"> <div class="grid grid-cols-2 gap-x-4 gap-y-2">
<button class="btn" hx-get="/create-user" hx-target="#page-content">Benutzer hinzufügen</button> <button class="btn" hx-get="/user/create" hx-target="#page-content">Benutzer hinzufügen</button>
<button class="btn" hx-get="/show-all-users-edit" hx-target="#page-content">Benutzer bearbeiten</button> <button class="btn" hx-get="/user/show-all/edit" hx-target="#page-content">Benutzer bearbeiten</button>
<button class="btn" hx-get="/show-all-users-delete" hx-target="#page-content">Benutzer löschen</button> <button class="btn" hx-get="/user/show-all/delete" hx-target="#page-content">Benutzer löschen</button>
</div> </div>
</div> </div>
{{end}} {{end}}

View File

@ -20,13 +20,17 @@
</div> </div>
</main> </main>
<footer class="my-8"> <footer class="text-center text-gray-500 dark:text-gray-400 my-8">
<p class="text-center text-gray-500 dark:text-gray-400"> <p>
&copy; 2024 Jason Streifling. Alle Rechte vorbehalten. &copy; 2024 Jason Streifling. Alle Rechte vorbehalten.
</p> </p>
<p>
v0.8.3 - <strong>Hinweis:</strong> Diese Software befindet sich noch in der Entwicklung und kann Fehler
enthalten.
</p>
</footer> </footer>
<script src="https://unpkg.com/htmx.org@2.0.1"></script> <script src="https://unpkg.com/htmx.org@2.0.2"></script>
<script src="https://unpkg.com/easymde/dist/easymde.min.js"></script> <script src="https://unpkg.com/easymde/dist/easymde.min.js"></script>
</body> </body>

View File

@ -0,0 +1,13 @@
{{define "page-content"}}
<h2>Artikel löschen</h2>
<div class="flex flex-col gap-4">
{{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

@ -4,7 +4,7 @@
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
{{range .RejectedArticles}} {{range .RejectedArticles}}
{{if index $.MyIDs .ID}} {{if index $.MyIDs .ID}}
<button class="btn" hx-get="/review-rejected-article/{{.ID}}" hx-target="#page-content"> <button class="btn" hx-get="/article/review-rejected/{{.ID}}" hx-target="#page-content">
<h1 class="font-bold text-2xl">{{.Title}}</h1> <h1 class="font-bold text-2xl">{{.Title}}</h1>
<p>{{.Description}}</p> <p>{{.Description}}</p>
</button> </button>

View File

@ -6,19 +6,26 @@
<label for="article-title">Titel</label> <label for="article-title">Titel</label>
<input name="article-title" type="text" value="{{.Article.Title}}" /> <input name="article-title" type="text" value="{{.Article.Title}}" />
</div> </div>
<div class="flex flex-col">
<div class="flex flex-col gap-y-1">
<label for="article-description">Beschreibung</label> <label for="article-description">Beschreibung</label>
<textarea name="article-description">{{.Article.Description}}</textarea> <textarea name="article-description">{{.Article.Description}}</textarea>
</div> </div>
<div class="flex flex-col">
<label for="article-content">Artikel</label> <div class="flex flex-col gap-y-1">
<textarea name="article-content" placeholder="Artikel">{{.Article.Content}}</textarea> <label for="easyMDE">Artikel</label>
</div> <textarea id="easyMDE">{{.Content}}</textarea>
<input id="article-content" name="article-content" type="hidden" /> <input id="article-content" name="article-content" type="hidden" />
</div>
<div> <div>
<span>Tags</span> <span>Tags</span>
<div class="flex flex-wrap gap-x-4"> <div class="flex flex-wrap gap-x-4">
<div>
<input id="issue" name="issue" type="checkbox" {{if .Article.IsInIssue}}checked{{end}} />
<label for="issue">Orient Express</label>
</div>
{{range .Tags}} {{range .Tags}}
<div> <div>
<input id="tag-{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" {{if index $.Selected <input id="tag-{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" {{if index $.Selected
@ -30,13 +37,15 @@
</div> </div>
<div class="btn-area"> <div class="btn-area">
<input class="action-btn" type="submit" value="Senden" hx-post="/resubmit-article/{{.Article.ID}}" <input class="action-btn" type="submit" value="Senden" hx-post="/article/resubmit/{{.Article.ID}}"
hx-target="#page-content" /> hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button> <button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
</div> </div>
</form> </form>
<script> <script>
document.getElementById('article-content').value = easyMDE.value();
var easyMDE = new EasyMDE({ var easyMDE = new EasyMDE({
element: document.getElementById('easyMDE'), element: document.getElementById('easyMDE'),
hideIcons: ['image'], hideIcons: ['image'],
@ -48,7 +57,7 @@
var formData = new FormData(); var formData = new FormData();
formData.append('article-image', file); formData.append('article-image', file);
fetch('/upload-image', { fetch('/article/upload-image', {
method: 'POST', method: 'POST',
body: formData body: formData
}) })

View File

@ -3,7 +3,7 @@
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
{{range .Users}} {{range .Users}}
<button class="btn" hx-get="/{{$.Action}}/{{.ID}}" hx-target="#page-content"> <button class="btn" hx-get="/user/{{$.Action}}/{{.ID}}" hx-target="#page-content">
<h1 class="font-bold text-2xl"> <h1 class="font-bold text-2xl">
{{.UserName}} {{.UserName}}
({{if eq .Role 0}} ({{if eq .Role 0}}

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

@ -4,12 +4,12 @@
<div> <div>
<span>Titel</span> <span>Titel</span>
<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full"> <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
{{.Title}} {{.Article.Title}}
</div> </div>
<span>Beschreibung</span> <span>Beschreibung</span>
<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full"> <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
{{.Description}} {{.Article.Description}}
</div> </div>
<span>Artikel</span> <span>Artikel</span>
@ -21,16 +21,21 @@
<span>Tags</span> <span>Tags</span>
<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full"> <div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
{{if .Article.IsInIssue}}
<span>Orient Express</span>
<br>
{{end}}
{{range .Tags}} {{range .Tags}}
{{.Name}} <span>{{.Name}}</span>
<br> <br>
{{end}} {{end}}
</div> </div>
<div class="btn-area"> <div class="btn-area">
<input class="action-btn" type="submit" value="Veröffentlichen" hx-get="/publish-article/{{.ID}}" <input class="action-btn" type="submit" value="Veröffentlichen" hx-get="/article/publish/{{.Article.ID}}"
hx-target="#page-content" />
<input class="btn" type="submit" value="Ablehnen" hx-get="/article/reject/{{.Article.ID}}"
hx-target="#page-content" /> hx-target="#page-content" />
<input class="btn" type="submit" value="Ablehnen" hx-get="/reject-article/{{.ID}}" hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button> <button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
</div> </div>
</div> </div>

View File

@ -3,7 +3,7 @@
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
{{range .}} {{range .}}
<button class="btn" hx-get="/review-unpublished-article/{{.ID}}" hx-target="#page-content"> <button class="btn" hx-get="/article/review-unpublished/{{.ID}}" hx-target="#page-content">
<h1 class="font-bold text-2xl">{{.Title}}</h1> <h1 class="font-bold text-2xl">{{.Title}}</h1>
<p>{{.Description}}</p> <p>{{.Description}}</p>
</button> </button>