Compare commits

...

100 Commits

Author SHA1 Message Date
e03fd78ea9 Merge branch 'devel' 2025-01-12 14:36:01 +01:00
36e9a16a6e Update all dependencies 2025-01-12 14:35:29 +01:00
d239a86444 Make creator's author checkbox work sensibly 2025-01-12 14:22:11 +01:00
8b1b99d0f7 Update version number 2025-01-12 14:18:25 +01:00
c0e446d6d9 Update systemd script 2025-01-07 09:54:46 +01:00
370ef205a9 Merge branch 'devel' 2024-12-27 21:36:02 +01:00
544dddc893 Add a way to get info about clicks 2024-12-27 11:09:36 +01:00
523bdb24cd Cleanup 2024-12-27 10:53:06 +01:00
376a1264f5 Add further support for clicks counter 2024-12-27 10:52:57 +01:00
82faacb9ec Let the article deletion logic happen entirely in the backend 2024-12-27 10:40:34 +01:00
7f85b60916 Add backend support for clicks counter 2024-12-27 10:37:19 +01:00
ca43ec1a81 Add support for multiple authors and contributors 2024-12-27 10:30:15 +01:00
0a14545a19 Bug fix 2024-12-27 07:20:24 +01:00
8d41caf40a Make issue compatible with multiple authors 2024-12-01 10:04:42 +01:00
8fb0733908 Make atom feed compatible with multiple authors 2024-12-01 09:59:39 +01:00
81f0e46ba6 Correct spelling mistake 2024-11-02 16:26:02 +01:00
19da2ae60c Add articles_authors.go and articles_contributors.go 2024-11-02 16:25:47 +01:00
2078be920e Change articles.go to not include an author 2024-11-02 16:25:15 +01:00
eb8e14ff6d Adapt DB layout to multiple authors and contributors 2024-11-02 16:24:39 +01:00
485eaaca70 Delete fmt.Println() uses 2024-11-01 16:32:42 +01:00
b2a8578c72 Add profile pic and correct usage of banner link 2024-11-01 16:31:47 +01:00
dbddff6e55 Delete content link from everywhere since it is only a combination of already saved info 2024-11-01 16:26:37 +01:00
f663592019 Changed version 2024-11-01 16:22:50 +01:00
c59029cf3d Add rel="self" to Atom feed 2024-11-01 16:22:31 +01:00
d328ddb749 Merge branch 'devel' 2024-10-30 03:25:18 +01:00
ac740cf4b8 Update version number 2024-10-30 03:25:05 +01:00
b7d82f15e9 Serve images from only one route enabling image previews in articles 2024-10-30 03:24:29 +01:00
b4003c8216 Fix bug that made the issue content unavailable 2024-10-30 03:23:59 +01:00
5dc5590da9 Merge branch 'devel' 2024-10-30 02:24:50 +01:00
81c046c1b0 Add summary checks back 2024-10-30 02:22:19 +01:00
364112a0a4 Merge branch 'devel' 2024-10-30 02:14:16 +01:00
1e9fdd2ab9 Update version number 2024-10-30 02:13:40 +01:00
a38523e933 Merge branch 'devel' 2024-10-30 02:13:03 +01:00
1fbc0ddcf6 Make banner link and summary optional 2024-10-30 02:12:53 +01:00
0dd2101a08 Cleanup 2024-10-29 17:00:51 +01:00
e95871ee70 Fix bug that allowed empty banner images 2024-10-29 16:43:42 +01:00
083718711d Use empty alt tag for banner image 2024-10-29 16:37:41 +01:00
20a12c6299 Implement banner images in a more standard way 2024-10-29 16:36:40 +01:00
200672dae2 Merge branch 'devel' 2024-10-28 18:28:31 +01:00
be829e662b Change version number 2024-10-28 18:28:22 +01:00
3d3aad88c8 Merge branch 'devel' 2024-10-28 18:27:33 +01:00
0036b42d82 Update htmx version 2024-10-28 18:27:24 +01:00
e4e43d1a83 Merge branch 'devel' 2024-10-28 18:02:56 +01:00
59deb69e2f Lift size constraints from image and pdf uploads 2024-10-28 17:59:53 +01:00
3aef27585a Rename AtomFeed to AtomFile everywhere else 2024-10-28 17:57:51 +01:00
3aa8796537 Rename AtomFeed to AtomFile in config 2024-10-27 15:50:45 +01:00
737a9ec314 Merge branch 'devel' 2024-10-27 15:29:50 +01:00
4b75fc61cd Update version number 2024-10-27 15:29:42 +01:00
1ebe0380ee Merge branch 'devel' 2024-10-27 15:28:22 +01:00
c439e048c1 Update dependencies 2024-10-27 15:28:07 +01:00
f86f2ba146 Get rid of RSS completely 2024-10-27 15:27:57 +01:00
d62d71b5d1 Merge branch 'devel' 2024-10-27 15:14:11 +01:00
07e1983fcc Add call to serve atom feed 2024-10-27 15:12:30 +01:00
878f57af08 Split up max value for img height and with into values for img and banner 2024-10-27 15:05:11 +01:00
d86b9027bf Correct lengths for username and names 2024-10-27 14:53:37 +01:00
31484dd44a Add encrypted email to user info 2024-10-27 14:43:38 +01:00
3b4e1e01d2 Make version part of config 2024-10-27 13:58:19 +01:00
03425fc2c6 Make action-btn and btn clicky 2024-10-27 13:58:03 +01:00
0b9bae3bf8 Get rid of rss 2024-10-27 13:29:58 +01:00
8ed0676e51 Encrypt sensitive user data with aes256 2024-10-27 13:29:46 +01:00
d7cbb34814 Prepare sql for encrypted user info, email address and profile pic 2024-10-27 07:34:44 +01:00
23f0c6f4a4 Make banner_link and content_link NOT NULL 2024-10-27 07:29:51 +01:00
cb322c57f9 Delete unnecessary enc_url, enc_length and enc_type from sql 2024-10-27 07:25:53 +01:00
b36e0ea503 Merge branch 'devel' 2024-10-04 16:06:57 +02:00
bc4d8fa37e Merge branch 'devel' 2024-09-28 13:59:34 +02:00
d2b21e7405 Merge branch 'devel' 2024-09-28 12:36:46 +02:00
e3c192359f Merge branch 'devel' 2024-09-11 18:15:31 +02:00
46532e4c85 Merge branch 'devel' 2024-09-11 18:15:07 +02:00
c722135a56 Merge branch 'devel' 2024-09-11 17:18:42 +02:00
887fa863bc Merge branch 'devel' 2024-09-10 19:43:22 +02:00
74d71cfb6a Merge branch 'devel' 2024-09-09 22:03:43 +02:00
ca7e7cddd3 Merge branch 'devel' 2024-09-08 16:22:59 +02:00
94431a2aa9 Merge branch 'devel' 2024-09-08 16:21:38 +02:00
5b1f20c5bc Merge branch 'devel' 2024-09-08 13:36:22 +02:00
d0c566f8df Merge branch 'devel' 2024-09-01 21:14:05 +02:00
5e586aa49a Merge branch 'devel' 2024-09-01 18:51:33 +02:00
66b2743d3d Merge branch 'devel' 2024-09-01 18:48:26 +02:00
3723b2b5e6 Merge branch 'devel' 2024-09-01 18:18:18 +02:00
ce788bfd50 Merge branch 'devel' 2024-09-01 12:54:12 +02:00
230a6278cc Merge branch 'devel' 2024-09-01 12:49:30 +02:00
42d6e0c198 Merge branch 'devel' 2024-08-31 12:17:41 +02:00
e1af2979af Merge branch 'devel' 2024-08-31 11:27:15 +02:00
f6dedc6f10 Merge branch 'devel' 2024-08-31 01:43:53 +02:00
cdf0a49550 Merge branch 'devel' 2024-08-31 01:38:28 +02:00
c3c0650210 Merge branch 'devel' 2024-08-31 01:00:55 +02:00
d077f700d8 Merge branch 'devel' 2024-08-31 00:36:48 +02:00
ec752b1c66 Merge branch 'devel' 2024-08-30 23:43:12 +02:00
46aef4f12f Merge branch 'devel' 2024-08-25 10:51:35 +02:00
1b29e328cf Merge branch 'devel' 2024-08-25 06:38:55 +02:00
e50cb819f3 Merge branch 'devel' 2024-08-23 21:45:30 +02:00
c32e38ca10 Merge branch 'devel' 2024-08-23 20:57:11 +02:00
d7c8c7a43a Merge branch 'devel' 2024-08-18 17:31:00 +02:00
1cd3edc90c Merge branch 'devel' 2024-08-18 12:06:29 +02:00
0e768c9f61 Merge branch 'devel' 2024-08-08 21:27:07 +02:00
1fcd775cc5 Merge branch 'devel' 2024-08-08 21:14:24 +02:00
203a1ed147 Implemented EasyMDE 2024-08-08 21:13:25 +02:00
ef1914ee5c Implemented article preview 2024-08-08 21:13:25 +02:00
084b101e31 Register f.ArticlePreviewHtmlData in init() 2024-08-08 21:13:25 +02:00
b2db128aa9 Shorten lines by referencing frontend as f and backend as b 2024-08-08 21:13:25 +02:00
081e880fb6 Change structure of code tor frontend and backend one 2024-08-08 21:13:25 +02:00
36 changed files with 1647 additions and 1002 deletions

View File

@ -4,22 +4,23 @@ tmp_dir = "tmp"
[build]
args_bin = [
"-aes tmp/cpolis.aes",
"-articles tmp/articles",
"-banner-width 512",
"-config tmp/config.toml",
"-desc 'Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität'",
"-domain localhost",
"-feed tmp/cpolis.atom",
"-firebase tmp/firebase.json",
"-key tmp/key.gob",
"-gob tmp/cpolis.gob",
"-img-width 256",
"-link https://distrikt-ni-st.de",
"-log tmp/cpolis.log",
"-pdfs tmp/pdfs",
"-pics tmp/pics",
"-port 8080",
"-rss tmp/orientexpress_alle.rss",
"-title 'Freimaurer Distrikt Niedersachsen und Sachsen-Anhalt'",
"-web web",
"-width 1024",
]
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ./cmd/main.go"

View File

@ -5,6 +5,7 @@ import (
"database/sql"
"fmt"
"log"
"os"
"time"
)
@ -13,11 +14,11 @@ type Article struct {
Title string
BannerLink string
Summary string
ContentLink string
ID int64
AuthorID int64
CreatorID int64
IssueID int64
EditedID int64
Clicks int
Published bool
Rejected bool
IsInIssue bool
@ -30,7 +31,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
selectQuery := "SELECT id FROM issues WHERE published = false"
insertQuery := `
INSERT INTO articles
(title, banner_link, summary, content_link, published, rejected, author_id, issue_id, edited_id, is_in_issue, auto_generated)
(title, banner_link, summary, published, rejected, creator_id, issue_id, edited_id, clicks, is_in_issue, auto_generated)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`
@ -48,7 +49,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
return 0, fmt.Errorf("error getting issue ID when adding article to DB: %v", err)
}
result, err := tx.Exec(insertQuery, a.Title, a.BannerLink, a.Summary, a.ContentLink, a.Published, a.Rejected, a.AuthorID, id, a.EditedID, a.IsInIssue, a.AutoGenerated)
result, err := tx.Exec(insertQuery, a.Title, a.BannerLink, a.Summary, a.Published, a.Rejected, a.CreatorID, id, a.EditedID, 0, a.IsInIssue, a.AutoGenerated)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
@ -82,7 +83,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
func (db *DB) GetArticle(id int64) (*Article, error) {
query := `
SELECT title, created, banner_link, summary, content_link, published, author_id, issue_id, edited_id, is_in_issue, auto_generated
SELECT title, created, banner_link, summary, published, creator_id, issue_id, edited_id, clicks, is_in_issue, auto_generated
FROM articles
WHERE id = ?
`
@ -92,7 +93,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) {
var created []byte
var err error
if err := row.Scan(&article.Title, &created, &article.BannerLink, &article.Summary, &article.ContentLink, &article.Published, &article.AuthorID, &article.IssueID, &article.EditedID, &article.IsInIssue, &article.AutoGenerated); err != nil {
if err := row.Scan(&article.Title, &created, &article.BannerLink, &article.Summary, &article.Published, &article.CreatorID, &article.IssueID, &article.EditedID, &article.Clicks, &article.IsInIssue, &article.AutoGenerated); err != nil {
return nil, fmt.Errorf("error scanning article row: %v", err)
}
@ -107,7 +108,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) {
func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, error) {
query := fmt.Sprintf(`
SELECT id, title, created, banner_link, summary, content_link, author_id, issue_id, published, rejected, is_in_issue, auto_generated
SELECT id, title, created, banner_link, summary, creator_id, issue_id, clicks, published, rejected, is_in_issue, auto_generated
FROM articles
WHERE %s = ?
`, attribute)
@ -121,7 +122,7 @@ func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, erro
article := new(Article)
var created []byte
if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.ContentLink, &article.AuthorID, &article.IssueID, &article.Published, &article.Rejected, &article.IsInIssue, &article.AutoGenerated); err != nil {
if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.CreatorID, &article.IssueID, &article.Clicks, &article.Published, &article.Rejected, &article.IsInIssue, &article.AutoGenerated); err != nil {
return nil, fmt.Errorf("error scanning article row: %v", err)
}
@ -141,7 +142,7 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) {
txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable}
issueQuery := "SELECT id FROM issues WHERE published = false"
articlesQuery := `
SELECT id, title, created, banner_link, summary, content_link, author_id, auto_generated
SELECT id, title, created, banner_link, summary, clicks, auto_generated
FROM articles
WHERE issue_id = ? AND published = true AND is_in_issue = true
`
@ -174,7 +175,7 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) {
article := new(Article)
var created []byte
if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.ContentLink, &article.AuthorID, &article.AutoGenerated); err != nil {
if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.Clicks, &article.AutoGenerated); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
@ -256,11 +257,23 @@ func (db *DB) AddArticleToCurrentIssue(id int64) error {
func (db *DB) DeleteArticle(id int64) error {
articlesTagsQuery := "DELETE FROM articles_tags WHERE article_id = ?"
articlesContributorsQuery := "DELETE FROM articles_contributors WHERE article_id = ?"
articlesAuthorsQuery := "DELETE FROM articles_authors WHERE article_id = ?"
articlesQuery := "DELETE FROM articles WHERE id = ?"
_, err := db.Exec(articlesTagsQuery, id)
if err != nil {
return fmt.Errorf("error deleting article %v from DB: %v", id, err)
return fmt.Errorf("error deleting articles_tags %v from DB: %v", id, err)
}
_, err = db.Exec(articlesContributorsQuery, id)
if err != nil {
return fmt.Errorf("error deleting articles_contributors %v from DB: %v", id, err)
}
_, err = db.Exec(articlesAuthorsQuery, id)
if err != nil {
return fmt.Errorf("error deleting articles_authors %v from DB: %v", id, err)
}
_, err = db.Exec(articlesQuery, id)
@ -270,3 +283,13 @@ func (db *DB) DeleteArticle(id int64) error {
return nil
}
func WriteArticleToFile(c *Config, articleID int64, content []byte) error {
articleAbsName := fmt.Sprint(c.ArticleDir, "/", articleID, ".md")
if err := os.WriteFile(articleAbsName, content, 0644); err != nil {
return fmt.Errorf("error writing article %v to file: %v", articleID, err)
}
return nil
}

View File

@ -0,0 +1,114 @@
package backend
import (
"fmt"
"log"
)
func (db *DB) WriteArticleAuthors(articleID int64, authorIDs []int64) error {
query := "INSERT INTO articles_authors (article_id, author_id) VALUES (?, ?)"
for i := 0; i < TxMaxRetries; i++ {
err := func() error {
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("error starting transaction: %v", err)
}
for _, authorID := range authorIDs {
if _, err := tx.Exec(query, articleID, authorID); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return fmt.Errorf("error inserting into articles_authors: %v", err)
}
}
if err = tx.Commit(); err != nil {
return fmt.Errorf("error committing transaction: %v", err)
}
return nil
}()
if err == nil {
return nil
}
log.Println(err)
wait(i)
}
return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries)
}
func (db *DB) GetArticleAuthors(c *Config, articleID int64) ([]*User, error) {
query := `
SELECT u.id
FROM articles a
INNER JOIN articles_authors aa ON a.id = aa.article_id
INNER JOIN users u ON aa.author_id = u.id
WHERE a.id = ?
`
rows, err := db.Query(query, articleID)
if err != nil {
return nil, fmt.Errorf("error querying articles_authors: %v", err)
}
authors := make([]*User, 0)
for rows.Next() {
var authorID int64
if err = rows.Scan(&authorID); err != nil {
return nil, fmt.Errorf("error scanning rows: %v", err)
}
author, err := db.GetUser(c, authorID)
if err != nil {
return nil, fmt.Errorf("error getting user info for article author: %v", err)
}
authors = append(authors, author)
}
return authors, nil
}
func (db *DB) UpdateArticleAuthors(articleID int64, authorIDs []int64) error {
deleteQuery := "DELETE FROM articles_authors WHERE article_id = ?"
insertQuery := "INSERT INTO articles_authors (article_id, author_id) VALUES (?, ?)"
for i := 0; i < TxMaxRetries; i++ {
err := func() error {
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("error starting transaction: %v", err)
}
if _, err := tx.Exec(deleteQuery, articleID); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return fmt.Errorf("error deleting entries from articles_authors before inserting new ones: %v", err)
}
for _, authorID := range authorIDs {
if _, err := tx.Exec(insertQuery, articleID, authorID); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return fmt.Errorf("error inserting new entries into articles_authors: %v", err)
}
}
if err = tx.Commit(); err != nil {
return fmt.Errorf("error committing transaction: %v", err)
}
return nil
}()
if err == nil {
return nil
}
log.Println(err)
wait(i)
}
return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries)
}

View File

@ -0,0 +1,114 @@
package backend
import (
"fmt"
"log"
)
func (db *DB) WriteArticleContributors(articleID int64, contributorIDs []int64) error {
query := "INSERT INTO articles_contributors (article_id, contributor_id) VALUES (?, ?)"
for i := 0; i < TxMaxRetries; i++ {
err := func() error {
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("error starting transaction: %v", err)
}
for _, contributorID := range contributorIDs {
if _, err := tx.Exec(query, articleID, contributorID); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return fmt.Errorf("error inserting into articles_contributors: %v", err)
}
}
if err = tx.Commit(); err != nil {
return fmt.Errorf("error committing transaction: %v", err)
}
return nil
}()
if err == nil {
return nil
}
log.Println(err)
wait(i)
}
return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries)
}
func (db *DB) GetArticleContributors(c *Config, articleID int64) ([]*User, error) {
query := `
SELECT u.id
FROM articles a
INNER JOIN articles_contributors ac ON a.id = ac.article_id
INNER JOIN users u ON ac.contributor_id = u.id
WHERE a.id = ?
`
rows, err := db.Query(query, articleID)
if err != nil {
return nil, fmt.Errorf("error querying articles_contributors: %v", err)
}
contributors := make([]*User, 0)
for rows.Next() {
var contributorID int64
if err = rows.Scan(&contributorID); err != nil {
return nil, fmt.Errorf("error scanning rows: %v", err)
}
contributor, err := db.GetUser(c, contributorID)
if err != nil {
return nil, fmt.Errorf("error getting user info for article contributor: %v", err)
}
contributors = append(contributors, contributor)
}
return contributors, nil
}
func (db *DB) UpdateArticleContributors(articleID int64, contributorIDs []int64) error {
deleteQuery := "DELETE FROM articles_contributors WHERE article_id = ?"
insertQuery := "INSERT INTO articles_contributors (article_id, contributor_id) VALUES (?, ?)"
for i := 0; i < TxMaxRetries; i++ {
err := func() error {
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("error starting transaction: %v", err)
}
if _, err := tx.Exec(deleteQuery, articleID); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return fmt.Errorf("error deleting entries from articles_contributors before inserting new ones: %v", err)
}
for _, contributorID := range contributorIDs {
if _, err := tx.Exec(insertQuery, articleID, contributorID); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return fmt.Errorf("error inserting new entries into articles_contributors: %v", err)
}
}
if err = tx.Commit(); err != nil {
return fmt.Errorf("error committing transaction: %v", err)
}
return nil
}()
if err == nil {
return nil
}
log.Println(err)
wait(i)
}
return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries)
}

View File

@ -43,8 +43,8 @@ func (db *DB) GetArticleTags(articleID int64) ([]*Tag, error) {
query := `
SELECT t.id, t.name
FROM articles a
INNER JOIN articles_tags at ON a.id = at.article_id
INNER JOIN tags t ON at.tag_id = t.id
INNER JOIN articles_tags at ON a.id = at.article_id
INNER JOIN tags t ON at.tag_id = t.id
WHERE a.id = ?
`
rows, err := db.Query(query, articleID)

View File

@ -8,55 +8,80 @@ import (
"git.streifling.com/jason/atom"
)
type Feed struct{ *atom.Feed }
func GenerateAtomFeed(c *Config, db *DB) (*string, error) {
feed := atom.NewFeed(c.Title)
feed.ID = atom.NewID("urn:feed:1")
feed.Subtitle = atom.NewText("text", c.Description)
feed.AddLink(atom.NewLink(c.Link))
linkID := feed.AddLink(atom.NewLink(c.Link))
feed.Links[linkID].Rel = "self"
feed.Generator = atom.NewGenerator("cpolis")
feed.Generator.URI = "https://git.streifling.com/jason/cpolis"
feed.Generator.Version = "0.13"
feed.Generator.Version = c.Version
articles, err := db.GetCertainArticles("published", true)
if err != nil {
return nil, fmt.Errorf("error getting published articles for RSS feed: %v", err)
return nil, fmt.Errorf("error getting published articles for Atom feed: %v", err)
}
for _, article := range articles {
articleTitle, err := ConvertToPlain(article.Title)
if err != nil {
return nil, fmt.Errorf("error converting title to plain text for RSS feed: %v", err)
return nil, fmt.Errorf("error converting title to plain text for Atom feed: %v", err)
}
entry := atom.NewEntry(articleTitle)
entry.ID = atom.NewID(fmt.Sprint("urn:entry:", article.ID))
entry.Content = atom.NewContent(atom.OutOfLine, "text/hmtl", article.ContentLink)
entry.Published = atom.NewDate(article.Created)
entry.Content = atom.NewContent(atom.OutOfLine, "text/hmtl", fmt.Sprint(c.Domain, "/article/serve/", article.ID))
linkID := entry.AddLink(atom.NewLink(article.BannerLink))
entry.Links[linkID].Rel = "enclosure"
entry.Links[linkID].Type = "image/webp"
user, err := db.GetUser(article.AuthorID)
if err != nil {
return nil, fmt.Errorf("error getting user user info for RSS feed: %v", err)
}
entry.AddAuthor(atom.NewPerson(user.FirstName + " " + user.LastName))
articleSummary, err := ConvertToPlain(article.Summary)
if err != nil {
return nil, fmt.Errorf("error converting description to plain text for RSS feed: %v", err)
}
if article.AutoGenerated {
articleSummary = "auto generated"
entry.Summary = atom.NewText("text", "automatically generated")
} else {
articleSummary, err := ConvertToPlain(article.Summary)
if err != nil {
return nil, fmt.Errorf("error converting description to plain text for Atom feed: %v", err)
}
entry.Summary = atom.NewText("text", articleSummary)
}
if len(article.BannerLink) > 0 {
linkID := entry.AddLink(atom.NewLink(c.Domain + "/image/serve/" + article.BannerLink))
entry.Links[linkID].Rel = "enclosure"
entry.Links[linkID].Type = "image/webp"
}
authors, err := db.GetArticleAuthors(c, article.ID)
if err != nil {
return nil, fmt.Errorf("error getting article's authors for Atom feed: %v", err)
}
for _, author := range authors {
user, err := db.GetUser(c, author.ID)
if err != nil {
return nil, fmt.Errorf("error getting user info for Atom feed: %v", err)
}
authorID := entry.AddAuthor(atom.NewPerson(user.FirstName + " " + user.LastName))
entry.Authors[authorID].URI = c.Domain + "/image/serve/" + user.ProfilePicLink
}
contributors, err := db.GetArticleContributors(c, article.ID)
if err != nil {
return nil, fmt.Errorf("error getting article's contributors for Atom feed: %v", err)
}
for _, contributor := range contributors {
user, err := db.GetUser(c, contributor.ID)
if err != nil {
return nil, fmt.Errorf("error getting user info for Atom feed: %v", err)
}
contributorID := entry.AddContributor(atom.NewPerson(user.FirstName + " " + user.LastName))
entry.Contributors[contributorID].URI = c.Domain + "/image/serve/" + user.ProfilePicLink
}
entry.Summary = atom.NewText("text", articleSummary)
tags, err := db.GetArticleTags(article.ID)
if err != nil {
return nil, fmt.Errorf("error getting tags for articles for RSS feed: %v", err)
return nil, fmt.Errorf("error getting tags for articles for Atom feed: %v", err)
}
for _, tag := range tags {
entry.AddCategory(atom.NewCategory(tag.Name))
@ -79,7 +104,7 @@ func GenerateAtomFeed(c *Config, db *DB) (*string, error) {
atom, err := feed.ToXML("UTF-8")
if err != nil {
return nil, fmt.Errorf("error converting RSS feed to XML: %v", err)
return nil, fmt.Errorf("error converting Atom feed to XML: %v", err)
}
return &atom, nil

View File

@ -12,42 +12,48 @@ import (
)
type Config struct {
ArticleDir string
ConfigFile string
DBName string
Description string
Domain string
AtomFeed string
FirebaseKey string
KeyFile string
Link string
LogFile string
PDFDir string
PicsDir string
Port string
RSSFile string
Title string
WebDir string
MaxImgHeight int
MaxImgWidth int
AESKeyFile string
ArticleDir string
ConfigFile string
DBName string
Description string
Domain string
AtomFile string
FirebaseKey string
GOBKeyFile string
Link string
LogFile string
PDFDir string
PicsDir string
Port string
Title string
Version string
WebDir string
MaxBannerHeight int
MaxBannerWidth int
MaxImgHeight int
MaxImgWidth int
}
func newConfig() *Config {
return &Config{
ArticleDir: "/var/www/cpolis/articles",
ConfigFile: "/etc/cpolis/config.toml",
DBName: "cpolis",
AtomFeed: "/var/www/cpolis/cpolis.atom",
FirebaseKey: "/var/www/cpolis/serviceAccountKey.json",
KeyFile: "/var/www/cpolis/cpolis.key",
LogFile: "/var/log/cpolis.log",
MaxImgHeight: 1080,
MaxImgWidth: 1920,
PDFDir: "/var/www/cpolis/pdfs",
PicsDir: "/var/www/cpolis/pics",
Port: ":8080",
RSSFile: "/var/www/cpolis/cpolis.rss",
WebDir: "/var/www/cpolis/web",
AESKeyFile: "/var/www/cpolis/aes.key",
ArticleDir: "/var/www/cpolis/articles",
AtomFile: "/var/www/cpolis/cpolis.atom",
ConfigFile: "/etc/cpolis/config.toml",
DBName: "cpolis",
FirebaseKey: "/var/www/cpolis/serviceAccountKey.json",
GOBKeyFile: "/var/www/cpolis/gob.key",
LogFile: "/var/log/cpolis.log",
MaxBannerHeight: 1080,
MaxBannerWidth: 1920,
MaxImgHeight: 1080,
MaxImgWidth: 1920,
PDFDir: "/var/www/cpolis/pdfs",
PicsDir: "/var/www/cpolis/pics",
Port: ":8080",
Version: "v0.14.1",
WebDir: "/var/www/cpolis/web",
}
}
@ -102,23 +108,25 @@ func (c *Config) handleCliArgs() error {
var port int
var err error
flag.StringVar(&c.AESKeyFile, "aes", c.AESKeyFile, "aes key file")
flag.StringVar(&c.ArticleDir, "articles", c.ArticleDir, "articles directory")
flag.StringVar(&c.AtomFeed, "feed", c.AtomFeed, "atom feed file")
flag.StringVar(&c.AtomFile, "feed", c.AtomFile, "atom feed file")
flag.StringVar(&c.ConfigFile, "config", c.ConfigFile, "config file")
flag.StringVar(&c.DBName, "db", c.DBName, "DB name")
flag.StringVar(&c.Description, "desc", c.Description, "channel description")
flag.StringVar(&c.Domain, "domain", c.Domain, "domain name")
flag.StringVar(&c.FirebaseKey, "firebase", c.FirebaseKey, "Firebase service account key file")
flag.StringVar(&c.KeyFile, "key", c.KeyFile, "key file")
flag.StringVar(&c.GOBKeyFile, "gob", c.GOBKeyFile, "gob key file")
flag.StringVar(&c.Link, "link", c.Link, "channel Link")
flag.StringVar(&c.LogFile, "log", c.LogFile, "log file")
flag.StringVar(&c.PDFDir, "pdfs", c.PDFDir, "pdf directory")
flag.StringVar(&c.PicsDir, "pics", c.PicsDir, "pictures directory")
flag.StringVar(&c.RSSFile, "rss", c.RSSFile, "RSS file")
flag.StringVar(&c.Title, "title", c.Title, "channel title")
flag.StringVar(&c.WebDir, "web", c.WebDir, "web directory")
flag.IntVar(&c.MaxImgHeight, "height", c.MaxImgHeight, "maximum image height")
flag.IntVar(&c.MaxImgWidth, "width", c.MaxImgWidth, "maximum image width")
flag.IntVar(&c.MaxBannerHeight, "banner-height", c.MaxBannerHeight, "maximum banner height")
flag.IntVar(&c.MaxBannerWidth, "banner-width", c.MaxBannerWidth, "maximum banner width")
flag.IntVar(&c.MaxImgHeight, "img-height", c.MaxImgHeight, "maximum image height")
flag.IntVar(&c.MaxImgWidth, "img-width", c.MaxImgWidth, "maximum image width")
flag.IntVar(&port, "port", port, "port")
flag.Parse()
@ -147,6 +155,14 @@ func (c *Config) setupConfig(cliConfig *Config) error {
var err error
defaultConfig := newConfig()
if cliConfig.AESKeyFile != defaultConfig.AESKeyFile {
c.AESKeyFile = cliConfig.AESKeyFile
}
c.AESKeyFile, err = mkFile(c.AESKeyFile, 0600, 0700)
if err != nil {
return fmt.Errorf("error setting up file: %v", err)
}
if cliConfig.ArticleDir != defaultConfig.ArticleDir {
c.ArticleDir = cliConfig.ArticleDir
}
@ -171,10 +187,10 @@ func (c *Config) setupConfig(cliConfig *Config) error {
c.Domain = "https://" + c.Domain
}
if cliConfig.AtomFeed != defaultConfig.AtomFeed {
c.AtomFeed = cliConfig.AtomFeed
if cliConfig.AtomFile != defaultConfig.AtomFile {
c.AtomFile = cliConfig.AtomFile
}
c.AtomFeed, err = mkFile(c.AtomFeed, 0644, 0744)
c.AtomFile, err = mkFile(c.AtomFile, 0644, 0744)
if err != nil {
return fmt.Errorf("error setting up file: %v", err)
}
@ -187,10 +203,10 @@ func (c *Config) setupConfig(cliConfig *Config) error {
return fmt.Errorf("error setting up file: %v", err)
}
if cliConfig.KeyFile != defaultConfig.KeyFile {
c.KeyFile = cliConfig.KeyFile
if cliConfig.GOBKeyFile != defaultConfig.GOBKeyFile {
c.GOBKeyFile = cliConfig.GOBKeyFile
}
c.KeyFile, err = mkFile(c.KeyFile, 0600, 0700)
c.GOBKeyFile, err = mkFile(c.GOBKeyFile, 0600, 0700)
if err != nil {
return fmt.Errorf("error setting up file: %v", err)
}
@ -207,6 +223,14 @@ func (c *Config) setupConfig(cliConfig *Config) error {
return fmt.Errorf("error setting up file: %v", err)
}
if cliConfig.MaxBannerHeight != defaultConfig.MaxBannerHeight {
c.MaxBannerHeight = cliConfig.MaxBannerHeight
}
if cliConfig.MaxBannerWidth != defaultConfig.MaxBannerWidth {
c.MaxBannerWidth = cliConfig.MaxBannerWidth
}
if cliConfig.MaxImgHeight != defaultConfig.MaxImgHeight {
c.MaxImgHeight = cliConfig.MaxImgHeight
}
@ -235,14 +259,6 @@ func (c *Config) setupConfig(cliConfig *Config) error {
c.Port = cliConfig.Port
}
if cliConfig.RSSFile != defaultConfig.RSSFile {
c.RSSFile = cliConfig.RSSFile
}
c.RSSFile, err = mkFile(c.RSSFile, 0600, 0700)
if err != nil {
return fmt.Errorf("error setting up file: %v", err)
}
if cliConfig.Title != defaultConfig.Title {
c.Title = cliConfig.Title
}

View File

@ -13,7 +13,7 @@ import (
var ErrUnsupportedFormat error = image.ErrFormat // used internally by imaging
func SaveImage(c *Config, src io.Reader) (string, error) {
func SaveImage(src io.Reader, maxHeight, maxWidth int, path string) (string, error) {
img, err := imaging.Decode(src, imaging.AutoOrientation(true))
if err != nil {
if err == ErrUnsupportedFormat {
@ -22,15 +22,15 @@ func SaveImage(c *Config, src io.Reader) (string, error) {
return "", fmt.Errorf("error decoding image: %v", err)
}
if img.Bounds().Dy() > c.MaxImgHeight {
img = imaging.Resize(img, 0, c.MaxImgHeight, imaging.Lanczos)
if img.Bounds().Dy() > maxHeight {
img = imaging.Resize(img, 0, maxHeight, imaging.Lanczos)
}
if img.Bounds().Dx() > c.MaxImgWidth {
img = imaging.Resize(img, c.MaxImgWidth, 0, imaging.Lanczos)
if img.Bounds().Dx() > maxWidth {
img = imaging.Resize(img, maxWidth, 0, imaging.Lanczos)
}
filename := fmt.Sprint(uuid.New(), ".webp")
file, err := os.Create(c.PicsDir + "/" + filename)
file, err := os.Create(path + filename)
if err != nil {
return "", fmt.Errorf("error creating new image file: %v", err)
}

View File

@ -1,103 +0,0 @@
package backend
import (
"fmt"
"io"
"os"
"time"
"git.streifling.com/jason/rss"
)
func GenerateRSS(c *Config, db *DB) (*string, error) {
channel := &rss.Channel{
Title: c.Title,
Link: c.Link,
Description: c.Description,
Items: make([]*rss.Item, 0),
}
articles, err := db.GetCertainArticles("published", true)
if err != nil {
return nil, fmt.Errorf("error getting published articles for RSS feed: %v", err)
}
for _, article := range articles {
tags, err := db.GetArticleTags(article.ID)
if err != nil {
return nil, fmt.Errorf("error getting tags for articles for RSS feed: %v", err)
}
tagNames := make([]string, 0)
for _, tag := range tags {
tagNames = append(tagNames, tag.Name)
}
if article.IsInIssue || article.AutoGenerated {
tagNames = append(tagNames, fmt.Sprint("Orient Express ", article.IssueID))
}
if article.AutoGenerated {
tagNames = append(tagNames, "autogenerated")
}
user, err := db.GetUser(article.AuthorID)
if err != nil {
return nil, fmt.Errorf("error getting user user info for RSS feed: %v", err)
}
articleTitle, err := ConvertToPlain(article.Title)
if err != nil {
return nil, fmt.Errorf("error converting title to plain text for RSS feed: %v", err)
}
articleDescription, err := ConvertToPlain(article.Summary)
if err != nil {
return nil, fmt.Errorf("error converting description to plain text for RSS feed: %v", err)
}
item := &rss.Item{
Author: user.FirstName + " " + user.LastName,
Categories: tagNames,
Description: articleDescription,
Guid: string(article.ID),
Link: article.ContentLink,
PubDate: article.Created.Format(time.RFC1123Z),
Title: articleTitle,
}
// if article.AutoGenerated {
// item.Enclosure = &rss.Enclosure{
// Url: article.EncURL,
// Lenght: article.EncLength,
// Type: article.EncType,
// }
// }
//
channel.Items = append(channel.Items, item)
}
feed := rss.NewFeed()
feed.Channels = append(feed.Channels, channel)
rss, err := feed.ToXML("UTF-8")
if err != nil {
return nil, fmt.Errorf("error converting RSS feed to XML: %v", err)
}
return &rss, nil
}
func SaveRSS(filename string, feed *string) error {
file, err := os.Create(filename)
if err != nil {
return fmt.Errorf("error creating file for RSS feed: %v", err)
}
defer file.Close()
if err = file.Chmod(0644); err != nil {
return fmt.Errorf("error setting permissions for RSS file: %v", err)
}
if _, err = io.WriteString(file, *feed); err != nil {
return fmt.Errorf("error writing to RSS file: %v", err)
}
return nil
}

View File

@ -2,9 +2,16 @@ package backend
import (
"context"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"database/sql"
"encoding/base64"
"errors"
"fmt"
"io"
"log"
"os"
"golang.org/x/crypto/bcrypt"
)
@ -18,24 +25,135 @@ const (
)
type User struct {
UserName string
FirstName string
LastName string
ID int64
Role int
UserName string
FirstName string
LastName string
Email string
ProfilePicLink string
ID int64
Role int
}
func (db *DB) AddUser(u *User, pass string) (int64, error) {
func readKey(filename string) ([]byte, error) {
key, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("error reading from aes key file: %v", err)
}
if len(key) != 44 {
return nil, errors.New("key is not 32 bytes long")
}
key, err = base64.StdEncoding.DecodeString(string(key))
if err != nil {
return nil, fmt.Errorf("error base64 decoding key: %v", err)
}
return key, nil
}
func key(c *Config) ([]byte, error) {
key, err := readKey(c.AESKeyFile)
if err != nil {
key = make([]byte, 32)
if _, err := rand.Read(key); err != nil {
return nil, fmt.Errorf("error generating random key: %v", err)
}
fileKey := make([]byte, 44)
base64.StdEncoding.Encode(fileKey, key)
if err = os.WriteFile(c.AESKeyFile, fileKey, 0600); err != nil {
return nil, fmt.Errorf("error writing key to file: %v", err)
}
}
return key, nil
}
func aesEncrypt(c *Config, plaintext string) (string, error) {
key, err := key(c)
if err != nil {
return "", fmt.Errorf("error retrieving key: %v", err)
}
block, err := aes.NewCipher(key)
if err != nil {
return "", fmt.Errorf("error creating cipher block: %v", err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", fmt.Errorf("error creating new gcm: %v", err)
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", fmt.Errorf("error creating nonce: %v", err)
}
cipherText := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
return base64.StdEncoding.EncodeToString(cipherText), nil
}
func aesDecrypt(c *Config, ciphertext string) (string, error) {
key, err := key(c)
if err != nil {
return "", fmt.Errorf("error retrieving key: %v", err)
}
block, err := aes.NewCipher(key)
if err != nil {
return "", fmt.Errorf("error creating cipher block: %v", err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", fmt.Errorf("error creating new gcm: %v", err)
}
data, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return "", fmt.Errorf("error base64 decoding ciphertext: %v", err)
}
nonceSize := gcm.NonceSize()
nonce, cipherText := data[:nonceSize], data[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, cipherText, nil)
if err != nil {
return "", fmt.Errorf("error aes decoding ciphertext: %v", err)
}
return string(plaintext), nil
}
func (db *DB) AddUser(c *Config, u *User, pass string) (int64, error) {
hashedPass, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
if err != nil {
return 0, fmt.Errorf("error creating password hash: %v", err)
}
aesFirstName, err := aesEncrypt(c, u.FirstName)
if err != nil {
return 0, fmt.Errorf("error encrypting first name: %v", err)
}
aesLastName, err := aesEncrypt(c, u.LastName)
if err != nil {
return 0, fmt.Errorf("error encrypting last name: %v", err)
}
aesEmail, err := aesEncrypt(c, u.Email)
if err != nil {
return 0, fmt.Errorf("error encrypting email: %v", err)
}
query := `
INSERT INTO users (username, password, first_name, last_name, role)
VALUES (?, ?, ?, ?, ?)
INSERT INTO users (username, password, first_name, last_name, email, profile_pic_link, role)
VALUES (?, ?, ?, ?, ?, ?, ?)
`
result, err := db.Exec(query, u.UserName, string(hashedPass), u.FirstName, u.LastName, u.Role)
result, err := db.Exec(query, u.UserName, string(hashedPass), aesFirstName, aesLastName, aesEmail, u.ProfilePicLink, u.Role)
if err != nil {
return 0, fmt.Errorf("error inserting new user %v into DB: %v", u.UserName, err)
}
@ -129,34 +247,44 @@ func (tx *Tx) ChangePassword(id int64, oldPass, newPass string) error {
}
// TODO: No need for ID field in general
func (db *DB) GetUser(id int64) (*User, error) {
func (db *DB) GetUser(c *Config, id int64) (*User, error) {
var aesFirstName, aesLastName, aesEmail string
var err error
user := new(User)
query := `
SELECT id, username, first_name, last_name, role
SELECT id, username, first_name, last_name, email, profile_pic_link, role
FROM users
WHERE id = ?
`
row := db.QueryRow(query, id)
if err := row.Scan(&user.ID, &user.UserName, &user.FirstName,
&user.LastName, &user.Role); err != nil {
if err := row.Scan(&user.ID, &user.UserName, &aesFirstName, &aesLastName, &aesEmail, &user.ProfilePicLink, &user.Role); err != nil {
return nil, fmt.Errorf("error reading user information: %v", err)
}
user.FirstName, err = aesDecrypt(c, aesFirstName)
if err != nil {
return nil, fmt.Errorf("error decrypting first name: %v", err)
}
user.LastName, err = aesDecrypt(c, aesLastName)
if err != nil {
return nil, fmt.Errorf("error decrypting last name: %v", err)
}
user.Email, err = aesDecrypt(c, aesEmail)
if err != nil {
return nil, fmt.Errorf("error decrypting email: %v", err)
}
return user, nil
}
func (db *DB) UpdateOwnUserAttributes(id int64, user, first, last, oldPass, newPass, newPass2 string) error {
passwordEmpty := true
if len(newPass) > 0 || len(newPass2) > 0 {
if newPass != newPass2 {
return fmt.Errorf("error: passwords do not match")
}
passwordEmpty = false
}
tx := new(Tx)
func (db *DB) UpdateOwnUserAttributes(c *Config, id int64, userName, firstName, lastName, email, profilePicLink, oldPass, newPass string) error {
var err error
tx := new(Tx)
passwordEmpty := len(newPass) == 0
for i := 0; i < TxMaxRetries; i++ {
err := func() error {
@ -174,10 +302,36 @@ func (db *DB) UpdateOwnUserAttributes(id int64, user, first, last, oldPass, newP
}
}
aesFirstName, err := aesEncrypt(c, firstName)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return fmt.Errorf("error encrypting first name: %v", err)
}
aesLastName, err := aesEncrypt(c, lastName)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return fmt.Errorf("error encrypting last name: %v", err)
}
aesEmail, err := aesEncrypt(c, email)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return fmt.Errorf("error encrypting email: %v", err)
}
if err = tx.UpdateAttributes(
&Attribute{Table: "users", ID: id, AttName: "username", Value: user},
&Attribute{Table: "users", ID: id, AttName: "first_name", Value: first},
&Attribute{Table: "users", ID: id, AttName: "last_name", Value: last},
&Attribute{Table: "users", ID: id, AttName: "username", Value: userName},
&Attribute{Table: "users", ID: id, AttName: "first_name", Value: aesFirstName},
&Attribute{Table: "users", ID: id, AttName: "last_name", Value: aesLastName},
&Attribute{Table: "users", ID: id, AttName: "email", Value: aesEmail},
&Attribute{Table: "users", ID: id, AttName: "profile_pic_link", Value: profilePicLink},
); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
@ -202,13 +356,13 @@ func (db *DB) UpdateOwnUserAttributes(id int64, user, first, last, oldPass, newP
return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries)
}
func (db *DB) AddFirstUser(u *User, pass string) (int64, error) {
func (db *DB) AddFirstUser(c *Config, u *User, pass string) (int64, error) {
var numUsers int64
txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable}
selectQuery := "SELECT COUNT(*) FROM users"
insertQuery := `
INSERT INTO users (username, password, first_name, last_name, role)
VALUES (?, ?, ?, ?, ?)
INSERT INTO users (username, password, first_name, last_name, email, profile_pic_link, role)
VALUES (?, ?, ?, ?, ?, ?, ?)
`
for i := 0; i < TxMaxRetries; i++ {
@ -239,7 +393,31 @@ func (db *DB) AddFirstUser(u *User, pass string) (int64, error) {
return 0, fmt.Errorf("error creating password hash: %v", err)
}
result, err := tx.Exec(insertQuery, u.UserName, string(hashedPass), u.FirstName, u.LastName, u.Role)
aesFirstName, err := aesEncrypt(c, u.FirstName)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return 0, fmt.Errorf("error encrypting first name: %v", err)
}
aesLastName, err := aesEncrypt(c, u.LastName)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return 0, fmt.Errorf("error encrypting last name: %v", err)
}
aesEmail, err := aesEncrypt(c, u.Email)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return 0, fmt.Errorf("error encrypting email: %v", err)
}
result, err := tx.Exec(insertQuery, u.UserName, string(hashedPass), aesFirstName, aesLastName, aesEmail, u.ProfilePicLink, u.Role)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
@ -270,8 +448,50 @@ func (db *DB) AddFirstUser(u *User, pass string) (int64, error) {
return 0, fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries)
}
func (db *DB) GetAllUsers() (map[int64]*User, error) {
query := "SELECT id, username, first_name, last_name, role FROM users"
func (db *DB) GetAllUsers(c *Config) ([]*User, error) {
var aesFirstName, aesLastName, aesEmail string
var err error
query := "SELECT id, username, first_name, last_name, email, profile_pic_link, role FROM users"
rows, err := db.Query(query)
if err != nil {
return nil, fmt.Errorf("error getting all users from DB: %v", err)
}
users := make([]*User, 0)
for rows.Next() {
user := new(User)
if err = rows.Scan(&user.ID, &user.UserName, &aesFirstName, &aesLastName, &aesEmail, &user.ProfilePicLink, &user.Role); err != nil {
return nil, fmt.Errorf("error getting user info: %v", err)
}
user.FirstName, err = aesDecrypt(c, aesFirstName)
if err != nil {
return nil, fmt.Errorf("error decrypting first name: %v", err)
}
user.LastName, err = aesDecrypt(c, aesLastName)
if err != nil {
return nil, fmt.Errorf("error decrypting last name: %v", err)
}
user.Email, err = aesDecrypt(c, aesEmail)
if err != nil {
return nil, fmt.Errorf("error decrypting email: %v", err)
}
users = append(users, user)
}
return users, nil
}
func (db *DB) GetAllUsersMap(c *Config) (map[int64]*User, error) {
var aesFirstName, aesLastName, aesEmail string
var err error
query := "SELECT id, username, first_name, last_name, email, profile_pic_link, role FROM users"
rows, err := db.Query(query)
if err != nil {
@ -281,10 +501,25 @@ func (db *DB) GetAllUsers() (map[int64]*User, error) {
users := make(map[int64]*User, 0)
for rows.Next() {
user := new(User)
if err = rows.Scan(&user.ID, &user.UserName, &user.FirstName,
&user.LastName, &user.Role); err != nil {
if err = rows.Scan(&user.ID, &user.UserName, &aesFirstName, &aesLastName, &aesEmail, &user.ProfilePicLink, &user.Role); err != nil {
return nil, fmt.Errorf("error getting user info: %v", err)
}
user.FirstName, err = aesDecrypt(c, aesFirstName)
if err != nil {
return nil, fmt.Errorf("error decrypting first name: %v", err)
}
user.LastName, err = aesDecrypt(c, aesLastName)
if err != nil {
return nil, fmt.Errorf("error decrypting last name: %v", err)
}
user.Email, err = aesDecrypt(c, aesEmail)
if err != nil {
return nil, fmt.Errorf("error decrypting email: %v", err)
}
users[user.ID] = user
}
@ -311,17 +546,10 @@ func (tx *Tx) SetPassword(id int64, newPass string) error {
return nil
}
func (db *DB) UpdateUserAttributes(id int64, user, first, last, newPass, newPass2 string, role int) error {
passwordEmpty := true
if len(newPass) > 0 || len(newPass2) > 0 {
if newPass != newPass2 {
return fmt.Errorf("error: passwords do not match")
}
passwordEmpty = false
}
tx := new(Tx)
func (db *DB) UpdateUserAttributes(c *Config, id int64, userName, firstName, lastName, email, profilePicLink, newPass string, role int) error {
var err error
tx := new(Tx)
passwordEmpty := len(newPass) == 0
for i := 0; i < TxMaxRetries; i++ {
err := func() error {
@ -339,10 +567,36 @@ func (db *DB) UpdateUserAttributes(id int64, user, first, last, newPass, newPass
}
}
aesFirstName, err := aesEncrypt(c, firstName)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return fmt.Errorf("error encrypting first name: %v", err)
}
aesLastName, err := aesEncrypt(c, lastName)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return fmt.Errorf("error encrypting last name: %v", err)
}
aesEmail, err := aesEncrypt(c, email)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return fmt.Errorf("error encrypting email: %v", err)
}
if err = tx.UpdateAttributes(
&Attribute{Table: "users", ID: id, AttName: "username", Value: user},
&Attribute{Table: "users", ID: id, AttName: "first_name", Value: first},
&Attribute{Table: "users", ID: id, AttName: "last_name", Value: last},
&Attribute{Table: "users", ID: id, AttName: "username", Value: userName},
&Attribute{Table: "users", ID: id, AttName: "first_name", Value: aesFirstName},
&Attribute{Table: "users", ID: id, AttName: "last_name", Value: aesLastName},
&Attribute{Table: "users", ID: id, AttName: "email", Value: aesEmail},
&Attribute{Table: "users", ID: id, AttName: "profile_pic_link", Value: profilePicLink},
&Attribute{Table: "users", ID: id, AttName: "role", Value: role},
); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {

View File

@ -10,6 +10,27 @@ import (
b "streifling.com/jason/cpolis/cmd/backend"
)
func incrementClicks(db *b.DB, a *b.Article) error {
a.Clicks++
if err := db.UpdateAttributes(&b.Attribute{Table: "articles", ID: a.ID, AttName: "clicks", Value: a.Clicks}); err != nil {
return fmt.Errorf("error updating click attribute of article %v: %v", a.ID, err)
}
if a.IsInIssue {
issue, err := db.GetArticle(a.IssueID)
if err != nil {
return fmt.Errorf("error getting issue %v: %v", a.IssueID, err)
}
issue.Clicks++
if err := db.UpdateAttributes(&b.Attribute{Table: "articles", ID: issue.ID, AttName: "clicks", Value: issue.Clicks}); err != nil {
return fmt.Errorf("error updating click attribute of issue %v: %v", issue.ID, err)
}
}
return nil
}
func ServeArticle(c *b.Config, db *b.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !tokenIsVerified(w, r, c) {
@ -50,6 +71,41 @@ func ServeArticle(c *b.Config, db *b.DB) http.HandlerFunc {
return
}
fmt.Fprint(w, content)
if err = incrementClicks(db, article); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if _, err = fmt.Fprint(w, content); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
func ServeClicks(db *b.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
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 _, err = fmt.Fprint(w, article.Clicks); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}

26
cmd/calls/atom.go Normal file
View File

@ -0,0 +1,26 @@
package calls
import (
"log"
"net/http"
"path/filepath"
b "streifling.com/jason/cpolis/cmd/backend"
)
func ServeAtomFeed(c *b.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !tokenIsVerified(w, r, c) {
return
}
absFilepath, err := filepath.Abs(c.AtomFile)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.ServeFile(w, r, absFilepath)
}
}

View File

@ -6,12 +6,15 @@ import (
"path/filepath"
b "streifling.com/jason/cpolis/cmd/backend"
f "streifling.com/jason/cpolis/cmd/frontend"
)
func ServeImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !tokenIsVerified(w, r, c) {
return
if _, err := f.GetSession(w, r, c, s); err != nil {
if !tokenIsVerified(w, r, c) {
return
}
}
absFilepath, err := filepath.Abs(c.PicsDir)

View File

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

View File

@ -18,21 +18,36 @@ const (
PreviewMode
)
const (
None = iota
Author
Contributor
)
type ArticleUser struct {
*b.User
ArticleRole int
}
type EditorHTMLData struct {
Selected map[int64]bool
Content string
Action string
ActionTitle string
ActionButton string
BannerImage string
Image string
HTMLContent template.HTML
Article *b.Article
Tags []*b.Tag
ArticleUsers map[string]*ArticleUser // A map is way more efficient in ReviewRejectedArticle()
Creator *ArticleUser
Authors []*b.User
Contributors []*b.User
}
func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s)
session, err := GetSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -41,11 +56,31 @@ func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
var data *EditorHTMLData
if session.Values["article"] == nil {
data = &EditorHTMLData{Action: "submit", Article: new(b.Article)}
data = &EditorHTMLData{Action: "submit", Article: new(b.Article), ArticleUsers: make(map[string]*ArticleUser)}
} else {
data = session.Values["article"].(*EditorHTMLData)
}
users, err := db.GetAllUsers(c)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for _, user := range users {
data.ArticleUsers[fmt.Sprint(user.LastName, user.FirstName, user.ID)] = &ArticleUser{User: user, ArticleRole: None}
}
creator, err := db.GetUser(c, session.Values["id"].(int64))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Creator = data.ArticleUsers[fmt.Sprint(creator.LastName, creator.FirstName, creator.ID)]
data.Creator.ArticleRole = Author
delete(data.ArticleUsers, fmt.Sprint(creator.LastName, creator.FirstName, creator.ID))
data.Tags, err = db.GetTagList()
if err != nil {
log.Println(err)
@ -64,7 +99,7 @@ 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 {
return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s)
session, err := GetSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -82,26 +117,55 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
Title: r.PostFormValue("article-title"),
BannerLink: r.PostFormValue("article-banner-url"),
Summary: r.PostFormValue("article-summary"),
CreatorID: session.Values["id"].(int64),
Published: false,
Rejected: false,
AuthorID: session.Values["id"].(int64),
IsInIssue: r.PostFormValue("issue") == "on",
AutoGenerated: false,
EditedID: 0,
}
if len(article.Title) == 0 {
http.Error(w, "Bitte den Titel eingeben.", http.StatusBadRequest)
return
}
if len(article.BannerLink) == 0 {
http.Error(w, "Bitte ein Titelbild einfügen.", http.StatusBadRequest)
return
}
if len(article.Summary) == 0 {
http.Error(w, "Bitte die Beschreibung eingeben.", http.StatusBadRequest)
return
}
r.ParseForm()
authors := make([]int64, 0)
contributors := make([]int64, 0)
for key, values := range r.Form {
if strings.HasPrefix(key, "user-") && len(values) > 0 {
id, err := strconv.ParseInt(strings.Split(key, "-")[1], 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
switch values[0] {
case "author":
authors = append(authors, id)
case "contributor":
contributors = append(contributors, id)
}
}
}
if r.PostFormValue("creator") == "author" {
authors = append(authors, article.CreatorID)
} else {
contributors = append(contributors, article.CreatorID)
}
if len(authors) == 0 {
http.Error(w, "Es muss mindestens einen Autor geben.", http.StatusBadRequest)
return
}
article.ID, err = db.AddArticle(article)
if err != nil {
log.Println(err)
@ -114,30 +178,34 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
http.Error(w, "Bitte den Artikel eingeben.", http.StatusBadRequest)
return
}
articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
if err = os.WriteFile(articleAbsName, content, 0644); err != nil {
if err := b.WriteArticleToFile(c, article.ID, content); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
article.ContentLink = fmt.Sprint(c.Domain, "/article/serve/", article.ID)
if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "content_link", Value: article.ContentLink}); err != nil {
if err = db.WriteArticleAuthors(article.ID, authors); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if len(contributors) > 0 {
if err = db.WriteArticleContributors(article.ID, contributors); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
r.ParseForm()
tags := make([]int64, 0)
for _, tag := range r.Form["tags"] {
tagID, err := strconv.ParseInt(tag, 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
tags = append(tags, tagID)
}
if err = db.WriteArticleTags(article.ID, tags); err != nil {
@ -146,9 +214,12 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return
}
data := new(struct{ Role int })
data.Role = session.Values["role"].(int)
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", session.Values["role"]); err != nil {
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -158,39 +229,75 @@ 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 {
return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s)
session, err := GetSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
article := &b.Article{
Title: r.PostFormValue("article-title"),
BannerLink: r.PostFormValue("article-banner-url"),
Summary: r.PostFormValue("article-summary"),
CreatorID: session.Values["id"].(int64),
IsInIssue: r.PostFormValue("issue") == "on",
}
title := r.PostFormValue("article-title")
if len(title) == 0 {
if len(article.Title) == 0 {
http.Error(w, "Bitte den Titel eingeben.", http.StatusBadRequest)
return
}
summary := r.PostFormValue("article-summary")
if len(summary) == 0 {
if len(article.Summary) == 0 {
http.Error(w, "Bitte die Beschreibung eingeben.", http.StatusBadRequest)
return
}
r.ParseForm()
authors := make([]int64, 0)
contributors := make([]int64, 0)
for key, values := range r.Form {
if strings.HasPrefix(key, "user-") && len(values) > 0 {
id, err := strconv.ParseInt(strings.Split(key, "-")[1], 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
switch values[0] {
case "author":
authors = append(authors, id)
case "contributor":
contributors = append(contributors, id)
}
}
}
if r.PostFormValue("creator") == "author" {
authors = append(authors, article.CreatorID)
} else {
contributors = append(contributors, article.CreatorID)
}
if len(authors) == 0 {
http.Error(w, "Es muss mindestens einen Autor geben.", http.StatusBadRequest)
return
}
article.ID, err = strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
content := r.PostFormValue("article-content")
if len(content) == 0 {
http.Error(w, "Bitte den Artikel eingeben.", http.StatusBadRequest)
return
}
contentLink := fmt.Sprint(c.ArticleDir, "/", id, ".md")
contentLink := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
if err = os.WriteFile(contentLink, []byte(content), 0644); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -198,17 +305,30 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
if err = db.UpdateAttributes(
&b.Attribute{Table: "articles", ID: id, AttName: "title", Value: title},
&b.Attribute{Table: "articles", ID: id, AttName: "summary", Value: summary},
&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"},
&b.Attribute{Table: "articles", ID: article.ID, AttName: "title", Value: article.Title},
&b.Attribute{Table: "articles", ID: article.ID, AttName: "banner_link", Value: article.BannerLink},
&b.Attribute{Table: "articles", ID: article.ID, AttName: "summary", Value: article.Summary},
&b.Attribute{Table: "articles", ID: article.ID, AttName: "rejected", Value: false},
&b.Attribute{Table: "articles", ID: article.ID, AttName: "is_in_issue", Value: article.IsInIssue},
); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
r.ParseForm()
if err = db.UpdateArticleAuthors(article.ID, authors); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if len(contributors) > 0 {
if err = db.UpdateArticleContributors(article.ID, contributors); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
tags := make([]int64, 0)
for _, tag := range r.Form["tags"] {
tagID, err := strconv.ParseInt(tag, 10, 64)
@ -219,15 +339,18 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
tags = append(tags, tagID)
}
if err = db.UpdateArticleTags(id, tags); err != nil {
if err = db.UpdateArticleTags(article.ID, tags); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data := new(struct{ Role int })
data.Role = session.Values["role"].(int)
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", session.Values["role"]); err != nil {
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -237,7 +360,7 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func ShowUnpublishedUnrejectedAndPublishedRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil {
if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -281,7 +404,7 @@ func ShowUnpublishedUnrejectedAndPublishedRejectedArticles(c *b.Config, db *b.DB
func ShowRejectedArticles(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)
session, err := GetSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -302,7 +425,7 @@ func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerF
data.MyIDs = make(map[int64]bool)
for _, article := range data.RejectedArticles {
if article.AuthorID == session.Values["id"].(int64) {
if article.CreatorID == session.Values["id"].(int64) {
data.MyIDs[article.ID] = true
}
}
@ -319,7 +442,8 @@ func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerF
func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil {
session, err := GetSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -340,14 +464,7 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
return
}
imgURL := strings.Split(data.Article.BannerLink, "/")
imgFileName := imgURL[len(imgURL)-1]
data.BannerImage, err = serveBase64Image(c, imgFileName)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Image = data.Article.BannerLink
articleAbsName := fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md")
content, err := os.ReadFile(articleAbsName)
@ -365,6 +482,46 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
return
}
data.ArticleUsers = make(map[string]*ArticleUser)
users, err := db.GetAllUsers(c)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for _, user := range users {
data.ArticleUsers[fmt.Sprint(user.LastName, user.FirstName, user.ID)] = &ArticleUser{User: user, ArticleRole: None}
}
authors, err := db.GetArticleAuthors(c, data.Article.ID)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for _, author := range authors {
data.ArticleUsers[fmt.Sprint(author.LastName, author.FirstName, author.ID)].ArticleRole = Author
}
contributors, err := db.GetArticleContributors(c, data.Article.ID)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for _, contributor := range contributors {
data.ArticleUsers[fmt.Sprint(contributor.LastName, contributor.FirstName, contributor.ID)].ArticleRole = Contributor
}
creator, err := db.GetUser(c, session.Values["id"].(int64))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Creator = data.ArticleUsers[fmt.Sprint(creator.LastName, creator.FirstName, creator.ID)]
delete(data.ArticleUsers, fmt.Sprint(creator.LastName, creator.FirstName, creator.ID))
selectedTags, err := db.GetArticleTags(id)
if err != nil {
log.Println(err)
@ -390,7 +547,7 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s)
session, err := GetSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -418,9 +575,9 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
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")},
&b.Attribute{Table: "articles", ID: article.ID, AttName: "published", Value: true},
&b.Attribute{Table: "articles", ID: article.ID, AttName: "rejected", Value: false},
&b.Attribute{Table: "articles", ID: article.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)
@ -447,10 +604,7 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return
}
if err = db.UpdateAttributes(
&b.Attribute{Table: "articles", ID: id, AttName: "content_link", Value: fmt.Sprint(c.Domain, "/article/serve/", article.ID)},
&b.Attribute{Table: "articles", ID: id, AttName: "edited_id", Value: 0},
); err != nil {
if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "edited_id", Value: 0}); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -463,15 +617,18 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = b.SaveAtomFeed(c.AtomFeed, feed); err != nil {
if err = b.SaveAtomFeed(c.AtomFile, feed); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data := new(struct{ Role int })
data.Role = session.Values["role"].(int)
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", session.Values["role"]); err != nil {
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -481,7 +638,7 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func RejectArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s)
session, err := GetSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -501,9 +658,12 @@ func RejectArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return
}
data := new(struct{ Role int })
data.Role = session.Values["role"].(int)
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", session.Values["role"]); err != nil {
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -513,7 +673,7 @@ func RejectArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func ShowCurrentIssue(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 {
if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -537,7 +697,7 @@ func ShowCurrentIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc
func ShowPublishedArticles(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil {
if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -574,7 +734,7 @@ func ShowPublishedArticles(c *b.Config, db *b.DB, s *b.CookieStore, action strin
func ReviewArticle(c *b.Config, db *b.DB, s *b.CookieStore, action, title, button string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil {
if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -611,14 +771,7 @@ func ReviewArticle(c *b.Config, db *b.DB, s *b.CookieStore, action, title, butto
return
}
imgURL := strings.Split(article.BannerLink, "/")
imgFileName := imgURL[len(imgURL)-1]
data.BannerImage, err = serveBase64Image(c, imgFileName)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Image = article.BannerLink
data.Article.Summary, err = b.ConvertToPlain(article.Summary)
if err != nil {
@ -642,6 +795,22 @@ func ReviewArticle(c *b.Config, db *b.DB, s *b.CookieStore, action, title, butto
}
data.HTMLContent = template.HTML(data.Content)
data.Authors, err = db.GetArticleAuthors(c, data.Article.ID)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
sortUsersByName(data.Authors)
data.Contributors, err = db.GetArticleContributors(c, data.Article.ID)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
sortUsersByName(data.Contributors)
data.Tags, err = db.GetArticleTags(id)
if err != nil {
log.Println(err)
@ -660,7 +829,7 @@ func ReviewArticle(c *b.Config, db *b.DB, s *b.CookieStore, action, title, butto
func DeleteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s)
session, err := GetSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -692,15 +861,18 @@ func DeleteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = b.SaveAtomFeed(c.AtomFeed, feed); err != nil {
if err = b.SaveAtomFeed(c.AtomFile, feed); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data := new(struct{ Role int })
data.Role = session.Values["role"].(int)
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int)); err != nil {
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -710,7 +882,7 @@ func DeleteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func AllowEditArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s)
session, err := GetSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -731,32 +903,69 @@ func AllowEditArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc
return
}
newArticle := oldArticle
newArticle := *oldArticle
newArticle.Published = false
newArticle.Rejected = true
newArticle.EditedID = oldArticle.ID
newID, err := db.AddArticle(newArticle)
newArticle.ID, err = db.AddArticle(&newArticle)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: oldID, AttName: "edited_id", Value: newID}); err != nil {
if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: oldArticle.ID, AttName: "edited_id", Value: newArticle.ID}); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = b.CopyFile(fmt.Sprint(c.ArticleDir, "/", oldID, ".md"), fmt.Sprint(c.ArticleDir, "/", newID, ".md")); err != nil {
src := fmt.Sprint(c.ArticleDir, "/", oldArticle.ID, ".md")
dst := fmt.Sprint(c.ArticleDir, "/", newArticle.ID, ".md")
if err = b.CopyFile(src, dst); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
authors, err := db.GetArticleAuthors(c, oldArticle.ID)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
authorIDs := make([]int64, len(authors))
for i, author := range authors {
authorIDs[i] = author.ID
}
if err = db.WriteArticleAuthors(newArticle.ID, authorIDs); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
contributors, err := db.GetArticleContributors(c, oldArticle.ID)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
contributorIDs := make([]int64, len(contributors))
for i, contributor := range contributors {
contributorIDs[i] = contributor.ID
}
if err = db.WriteArticleContributors(newArticle.ID, contributorIDs); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data := new(struct{ Role int })
data.Role = session.Values["role"].(int)
tmpl := template.Must(template.ParseFiles(c.WebDir + "/templates/hub.html"))
if err = tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int)); err != nil {
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -766,7 +975,7 @@ func AllowEditArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc
func EditArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil {
if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -788,14 +997,7 @@ func EditArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return
}
imgURL := strings.Split(data.Article.BannerLink, "/")
imgFileName := imgURL[len(imgURL)-1]
data.BannerImage, err = serveBase64Image(c, imgFileName)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Image = data.Article.BannerLink
content, err := os.ReadFile(fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md"))
if err != nil {

View File

@ -1,49 +1,22 @@
package frontend
import (
"encoding/base64"
"encoding/json"
"fmt"
"html/template"
"io"
"log"
"net/http"
"os"
b "streifling.com/jason/cpolis/cmd/backend"
)
func serveBase64Image(c *b.Config, filename string) (string, error) {
file := c.PicsDir + "/" + filename
img, err := os.Open(file)
if err != nil {
return "", fmt.Errorf("error opening file %v: %v", file, err)
}
defer img.Close()
imgBytes, err := io.ReadAll(img)
if err != nil {
return "", fmt.Errorf("error turning %v into bytes: %v", file, err)
}
return base64.StdEncoding.EncodeToString(imgBytes), nil
}
func UploadImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
func UploadEasyMDEImage(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 {
if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := r.ParseMultipartForm(10 << 20); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
file, _, err := r.FormFile("article-image")
if err != nil {
log.Println(err)
@ -52,7 +25,7 @@ func UploadImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
}
defer file.Close()
filename, err := b.SaveImage(c, file)
filename, err := b.SaveImage(file, c.MaxImgHeight, c.MaxImgWidth, c.PicsDir+"/")
if err != nil {
if err == b.ErrUnsupportedFormat {
http.Error(w, "Das Dateiformat wird nicht unterstützt.", http.StatusBadRequest)
@ -69,54 +42,9 @@ func UploadImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
}
}
func UploadIssueImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
func UploadImage(c *b.Config, s *b.CookieStore, fileKey, htmlFile, htmlTemplate string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := r.ParseMultipartForm(10 << 20); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
file, _, err := r.FormFile("issue-image")
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer file.Close()
filename, err := b.SaveImage(c, file)
if err != nil {
if err == b.ErrUnsupportedFormat {
http.Error(w, "Das Dateiformat wird nicht unterstützt.", http.StatusBadRequest)
return
}
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
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)
}
}
func UploadBanner(c *b.Config, s *b.CookieStore, fileKey, htmlFile, htmlTemplate string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil {
if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -130,7 +58,7 @@ func UploadBanner(c *b.Config, s *b.CookieStore, fileKey, htmlFile, htmlTemplate
}
defer file.Close()
filename, err := b.SaveImage(c, file)
filename, err := b.SaveImage(file, c.MaxBannerHeight, c.MaxBannerWidth, c.PicsDir+"/")
if err != nil {
if err == b.ErrUnsupportedFormat {
http.Error(w, "Das Dateiformat wird nicht unterstützt.", http.StatusBadRequest)
@ -141,20 +69,8 @@ func UploadBanner(c *b.Config, s *b.CookieStore, fileKey, htmlFile, htmlTemplate
return
}
base64Img, err := serveBase64Image(c, filename)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data := new(struct {
BannerImage string
URL string
})
data.BannerImage = base64Img
data.URL = c.Domain + "/image/serve/" + filename
data := new(struct{ Image string })
data.Image = filename
tmpl, err := template.ParseFiles(c.WebDir + "/templates/" + htmlFile)
if err = template.Must(tmpl, err).ExecuteTemplate(w, htmlTemplate, data); err != nil {

View File

@ -13,7 +13,7 @@ import (
func PublishLatestIssue(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)
session, err := GetSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -33,7 +33,6 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
Published: true,
Rejected: false,
Created: time.Now(),
AuthorID: session.Values["id"].(int64),
AutoGenerated: true,
}
@ -41,10 +40,6 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
http.Error(w, "Bitte den Titel eingeben.", http.StatusBadRequest)
return
}
if len(article.BannerLink) == 0 {
http.Error(w, "Bitte ein Titelbild einfügen.", http.StatusBadRequest)
return
}
article.ID, err = db.AddArticle(article)
if err != nil {
@ -53,6 +48,22 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
return
}
authorIDs := make([]int64, 1)
var ok bool
if authorIDs[0], ok = session.Values["id"].(int64); !ok {
msg := "fälschlicherweise session.Values[\"id\"].(int64) für authorIDs[0] angenommen"
log.Println(msg)
http.Error(w, msg, http.StatusInternalServerError)
return
}
if err = db.WriteArticleAuthors(article.ID, authorIDs); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
content := []byte(r.PostFormValue("issue-content"))
if len(content) == 0 {
http.Error(w, "Bitte eine Beschreibung eingeben.", http.StatusBadRequest)
@ -66,19 +77,6 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
return
}
article.ContentLink = fmt.Sprint(c.Domain, "/article/serve/", article.ID)
if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "content_link", Value: article.ContentLink}); 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: "content_link", Value: article.ContentLink}); 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)
@ -97,7 +95,7 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = b.SaveAtomFeed(c.AtomFeed, feed); err != nil {
if err = b.SaveAtomFeed(c.AtomFile, feed); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -110,9 +108,12 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
return
}
data := new(struct{ Role int })
data.Role = session.Values["role"].(int)
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", session.Values["role"]); err != nil {
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return

View File

@ -12,18 +12,12 @@ import (
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 {
if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := r.ParseMultipartForm(10 << 20); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
file, _, err := r.FormFile("pdf-upload")
if err != nil {
log.Println(err)

View File

@ -26,8 +26,8 @@ func saveSession(w http.ResponseWriter, r *http.Request, s *b.CookieStore, u *b.
return nil
}
// getSession is used for verifying that the user is logged in and returns their session and an error.
func getSession(w http.ResponseWriter, r *http.Request, c *b.Config, s *b.CookieStore) (*b.Session, error) {
// GetSession is used for verifying that the user is logged in and returns their session and an error.
func GetSession(w http.ResponseWriter, r *http.Request, c *b.Config, s *b.CookieStore) (*b.Session, error) {
msg := "Keine gültige Session. Bitte erneut anmelden."
tmpl, tmplErr := template.ParseFiles(c.WebDir+"/templates/index.html", c.WebDir+"/templates/login.html")
@ -56,12 +56,24 @@ func HomePage(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
log.Fatalln(err)
}
data := new(struct {
*UserHTMLData
Version string
})
data.UserHTMLData = &UserHTMLData{User: new(b.User)}
data.Version = c.Version
files := make([]string, 2)
files[0] = c.WebDir + "/templates/index.html"
if numRows == 0 {
files[1] = c.WebDir + "/templates/first-user.html"
data.Role = b.NonExistent
data.Title = "Erster Benutzer (Administrator)"
data.ButtonText = "Anlegen"
data.URL = "/user/add-first"
files[1] = c.WebDir + "/templates/edit-user.html"
tmpl, err := template.ParseFiles(files...)
if err = template.Must(tmpl, err).Execute(w, nil); err != nil {
if err = template.Must(tmpl, err).Execute(w, data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -73,18 +85,21 @@ func HomePage(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if auth, ok := session.Values["authenticated"].(bool); auth && ok {
data.Role = session.Values["role"].(int)
files[1] = c.WebDir + "/templates/hub.html"
tmpl, err := template.ParseFiles(files...)
if err = template.Must(tmpl, err).Execute(w, session.Values["role"]); err != nil {
if err = template.Must(tmpl, err).Execute(w, data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
} else {
data.Role = b.Author
files[1] = c.WebDir + "/templates/login.html"
tmpl, err := template.ParseFiles(files...)
if err = template.Must(tmpl, err).Execute(w, nil); err != nil {
if err = template.Must(tmpl, err).Execute(w, data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -111,7 +126,7 @@ func Login(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return
}
user, err := db.GetUser(id)
user, err := db.GetUser(c, id)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -125,7 +140,7 @@ func Login(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", user.Role); err != nil {
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", user); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -135,7 +150,7 @@ func Login(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func Logout(c *b.Config, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s)
session, err := GetSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -160,7 +175,7 @@ func Logout(c *b.Config, s *b.CookieStore) http.HandlerFunc {
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)
session, err := GetSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -174,8 +189,11 @@ func ShowHub(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return
}
data := new(struct{ Role int })
data.Role = session.Values["role"].(int)
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", session.Values["role"].(int)); err != nil {
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return

View File

@ -10,7 +10,7 @@ import (
func CreateTag(c *b.Config, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil {
if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -27,7 +27,7 @@ func CreateTag(c *b.Config, 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) {
session, err := getSession(w, r, c, s)
session, err := GetSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -41,9 +41,12 @@ func AddTag(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
db.AddTag(tag)
data := new(struct{ Role int })
data.Role = session.Values["role"].(int)
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", session.Values["role"]); err != nil {
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return

View File

@ -5,14 +5,23 @@ import (
"html/template"
"log"
"net/http"
"sort"
"strconv"
b "streifling.com/jason/cpolis/cmd/backend"
)
type UserHTMLData struct {
*b.User
Title string
ButtonText string
URL string
Image string
}
func checkUserStrings(user *b.User) (string, int, bool) {
userLen := 15
nameLen := 50
userLen := 63 // max value for utf-8 at 255 bytes
nameLen := 56 // max value when aes encrypting utf-8 at up to 255 bytes
if len(user.UserName) > userLen {
return "Benutzername", userLen, false
@ -25,16 +34,32 @@ func checkUserStrings(user *b.User) (string, int, bool) {
}
}
func sortUsersByName(users []*b.User) {
sort.SliceStable(users, func(i, j int) bool {
if users[i].LastName == users[j].LastName {
return users[i].FirstName < users[j].FirstName
}
return users[i].LastName < users[j].LastName
})
}
func CreateUser(c *b.Config, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil {
if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html")
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil); err != nil {
data := &UserHTMLData{
User: &b.User{Role: b.Author},
Title: "Neuer Benutzer",
ButtonText: "Anlegen",
URL: "/user/add",
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html")
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -44,7 +69,7 @@ func CreateUser(c *b.Config, 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) {
session, err := getSession(w, r, c, s)
session, err := GetSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -52,15 +77,17 @@ func AddUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
user := &b.User{
UserName: r.PostFormValue("username"),
FirstName: r.PostFormValue("first-name"),
LastName: r.PostFormValue("last-name"),
UserName: r.PostFormValue("username"),
FirstName: r.PostFormValue("first-name"),
LastName: r.PostFormValue("last-name"),
Email: r.PostFormValue("email"),
ProfilePicLink: r.PostFormValue("profile-pic-url"),
}
pass := r.PostFormValue("password")
pass2 := r.PostFormValue("password2")
email2 := r.PostFormValue("email2")
if len(user.UserName) == 0 || len(user.FirstName) == 0 ||
len(user.LastName) == 0 || len(pass) == 0 || len(pass2) == 0 {
if len(user.UserName) == 0 || len(user.FirstName) == 0 || len(user.LastName) == 0 || len(user.Email) == 0 || len(email2) == 0 || len(pass) == 0 || len(pass2) == 0 {
http.Error(w, "Bitte alle Felder ausfüllen.", http.StatusBadRequest)
return
}
@ -76,6 +103,11 @@ func AddUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return
}
if user.Email != email2 {
http.Error(w, "Die Emailadressen stimmen nicht überein.", http.StatusBadRequest)
return
}
if pass != pass2 {
http.Error(w, "Die Passwörter stimmen nicht überein.", http.StatusBadRequest)
return
@ -94,16 +126,19 @@ func AddUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return
}
_, err = db.AddUser(user, pass)
_, err = db.AddUser(c, user, pass)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data := new(struct{ Role int })
data.Role = session.Values["role"].(int)
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int)); err != nil {
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -113,22 +148,30 @@ 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 {
return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s)
session, err := GetSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
user, err := db.GetUser(session.Values["id"].(int64))
user, err := db.GetUser(c, session.Values["id"].(int64))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-self.html")
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", user); err != nil {
data := &UserHTMLData{
User: user,
Title: "Mein Profil bearbeiten",
ButtonText: "Übernehmen",
URL: "/user/update/self",
Image: user.ProfilePicLink,
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html")
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -138,7 +181,7 @@ 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 {
return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s)
session, err := GetSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -146,15 +189,18 @@ func UpdateSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
user := &b.User{
ID: session.Values["id"].(int64),
UserName: r.PostFormValue("username"),
FirstName: r.PostFormValue("first-name"),
LastName: r.PostFormValue("last-name"),
ID: session.Values["id"].(int64),
UserName: r.PostFormValue("username"),
FirstName: r.PostFormValue("first-name"),
LastName: r.PostFormValue("last-name"),
Email: r.PostFormValue("email"),
ProfilePicLink: r.PostFormValue("profile-pic-url"),
}
oldPass := r.PostFormValue("old-password")
newPass := r.PostFormValue("password")
newPass2 := r.PostFormValue("password2")
email2 := r.PostFormValue("email2")
if len(user.UserName) == 0 {
http.Error(w, "Bitte den Benutzernamen ausfüllen.", http.StatusBadRequest)
@ -166,6 +212,20 @@ func UpdateSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return
}
if len(user.Email) == 0 || len(email2) == 0 {
http.Error(w, "Bitte die Emailadresse ausfüllen.", http.StatusBadRequest)
return
}
if user.Email != email2 {
http.Error(w, "Die Emailadressen stimmen nicht überein", http.StatusBadRequest)
return
}
if newPass != newPass2 {
http.Error(w, "Die Passwörter stimmen nicht überein", http.StatusBadRequest)
return
}
userString, stringLen, ok := checkUserStrings(user)
if !ok {
http.Error(w, fmt.Sprint(userString, " ist zu lang. Maximal ", stringLen, " Zeichen erlaubt."), http.StatusBadRequest)
@ -177,15 +237,18 @@ func UpdateSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return
}
if err = db.UpdateOwnUserAttributes(user.ID, user.UserName, user.FirstName, user.LastName, oldPass, newPass, newPass2); err != nil {
if err = db.UpdateOwnUserAttributes(c, user.ID, user.UserName, user.FirstName, user.LastName, user.Email, user.ProfilePicLink, oldPass, newPass); err != nil {
log.Println("error: user:", user.ID, err)
http.Error(w, "Benutzerdaten konnten nicht aktualisiert werden.", http.StatusInternalServerError)
return
}
data := new(struct{ Role int })
data.Role = session.Values["role"].(int)
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int)); err != nil {
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -197,16 +260,18 @@ func AddFirstUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var err error
user := &b.User{
UserName: r.PostFormValue("username"),
FirstName: r.PostFormValue("first-name"),
LastName: r.PostFormValue("last-name"),
Role: b.Admin,
UserName: r.PostFormValue("username"),
FirstName: r.PostFormValue("first-name"),
LastName: r.PostFormValue("last-name"),
Email: r.PostFormValue("email"),
ProfilePicLink: r.PostFormValue("profile-pic-url"),
Role: b.Admin,
}
pass := r.PostFormValue("password")
pass2 := r.PostFormValue("password2")
email2 := r.PostFormValue("email2")
if len(user.UserName) == 0 || len(user.FirstName) == 0 ||
len(user.LastName) == 0 || len(pass) == 0 || len(pass2) == 0 {
if len(user.UserName) == 0 || len(user.FirstName) == 0 || len(user.LastName) == 0 || len(user.Email) == 0 || len(email2) == 0 || len(pass) == 0 || len(pass2) == 0 {
http.Error(w, "Bitte alle Felder ausfüllen.", http.StatusBadRequest)
return
}
@ -217,12 +282,17 @@ func AddFirstUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return
}
if user.Email != email2 {
http.Error(w, "Die Emailadressen stimmen nicht überein.", http.StatusBadRequest)
return
}
if pass != pass2 {
http.Error(w, "Die Passwörter stimmen nicht überein.", http.StatusBadRequest)
return
}
user.ID, err = db.AddFirstUser(user, pass)
user.ID, err = db.AddFirstUser(c, user, pass)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -245,8 +315,11 @@ func AddFirstUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return
}
data := new(struct{ Role int })
data.Role = 0
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", 0); err != nil {
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -256,7 +329,7 @@ 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 {
return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s)
session, err := GetSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -269,14 +342,14 @@ func ShowAllUsers(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.H
})
data.Action = action
data.Users, err = db.GetAllUsers()
data.Users, err = db.GetAllUsersMap(c)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
delete(data.Users, session.Values["id"].(int64))
tmpl, err := template.ParseFiles(c.WebDir + "/templates/show-all-users.html")
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
@ -288,7 +361,7 @@ func ShowAllUsers(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.H
func EditUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil {
if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -301,15 +374,23 @@ func EditUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return
}
user, err := db.GetUser(id)
user, err := db.GetUser(c, id)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data := &UserHTMLData{
User: user,
Title: "Profil von " + user.FirstName + " " + user.LastName + " bearbeiten",
ButtonText: "Übernehmen",
URL: fmt.Sprint("/user/update/", user.ID),
Image: user.ProfilePicLink,
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html")
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", user); err != nil {
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -319,7 +400,7 @@ func EditUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func UpdateUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s)
session, err := GetSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -343,19 +424,36 @@ func UpdateUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
user.UserName = r.PostFormValue("username")
if len(user.UserName) == 0 {
http.Error(w, "Bitte den Benutzernamen ausfüllen.", http.StatusInternalServerError)
http.Error(w, "Bitte den Benutzernamen ausfüllen.", http.StatusBadRequest)
return
}
user.FirstName = r.PostFormValue("first-name")
user.LastName = r.PostFormValue("last-name")
if len(user.FirstName) == 0 || len(user.LastName) == 0 {
http.Error(w, "Bitte den vollständigen Namen ausfüllen.", http.StatusInternalServerError)
http.Error(w, "Bitte den vollständigen Namen ausfüllen.", http.StatusBadRequest)
return
}
user.Email = r.PostFormValue("email")
email2 := r.PostFormValue("email2")
if len(user.Email) == 0 || len(email2) == 0 {
http.Error(w, "Bitte die Emailadresse ausfüllen.", http.StatusBadRequest)
return
}
if user.Email != email2 {
http.Error(w, "Die Emailadressen stimmen nicht überein.", http.StatusBadRequest)
return
}
user.ProfilePicLink = r.PostFormValue("profile-pic-url")
newPass := r.PostFormValue("password")
newPass2 := r.PostFormValue("password2")
if newPass != newPass2 {
http.Error(w, "Die Passwörter stimmen nicht überein.", http.StatusBadRequest)
return
}
userString, stringLen, ok := checkUserStrings(user)
if !ok {
@ -368,14 +466,17 @@ func UpdateUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return
}
if err = db.UpdateUserAttributes(user.ID, user.UserName, user.FirstName, user.LastName, newPass, newPass2, user.Role); err != nil {
if err = db.UpdateUserAttributes(c, user.ID, user.UserName, user.FirstName, user.LastName, user.Email, user.ProfilePicLink, newPass, user.Role); err != nil {
log.Println("error: user:", user.ID, err)
http.Error(w, "Benutzerdaten konnten nicht aktualisiert werden.", http.StatusInternalServerError)
return
}
data := new(struct{ Role int })
data.Role = session.Values["role"].(int)
tmpl := template.Must(template.ParseFiles(c.WebDir + "/templates/hub.html"))
if err = tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int)); err != nil {
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -385,7 +486,7 @@ 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 {
return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s)
session, err := GetSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -405,9 +506,12 @@ func DeleteUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return
}
data := new(struct{ Role int })
data.Role = session.Values["role"].(int)
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int)); err != nil {
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return

View File

@ -34,13 +34,13 @@ func main() {
}
defer db.Close()
key, err := b.LoadKey(config.KeyFile)
key, err := b.LoadKey(config.GOBKeyFile)
if err != nil {
key, err = b.NewKey()
if err != nil {
log.Fatalln(err)
}
if err = b.SaveKey(key, config.KeyFile); err != nil {
if err = b.SaveKey(key, config.GOBKeyFile); err != nil {
log.Fatalln(err)
}
}
@ -65,14 +65,15 @@ func main() {
mux.HandleFunc("GET /article/review-rejected/{id}", f.ReviewRejectedArticle(config, db, store))
mux.HandleFunc("GET /article/review-unpublished/{id}", f.ReviewArticle(config, db, store, "publish", "Artikel veröffentlichen", "Veröffentlichen"))
mux.HandleFunc("GET /article/serve/{id}", c.ServeArticle(config, db))
mux.HandleFunc("GET /article/serve/{id}/clicks", c.ServeClicks(db))
mux.HandleFunc("GET /article/write", f.WriteArticle(config, db, store))
mux.HandleFunc("GET /atom/serve", c.ServeAtomFeed(config))
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.ShowCurrentIssue(config, db, store))
mux.HandleFunc("GET /logout", f.Logout(config, store))
mux.HandleFunc("GET /pdf/get-list", c.ServePDFList(config))
mux.HandleFunc("GET /pdf/serve/{id}", c.ServePDF(config))
mux.HandleFunc("GET /rss/serve", c.ServeRSS(config))
mux.HandleFunc("GET /tag/create", f.CreateTag(config, store))
mux.HandleFunc("GET /user/create", f.CreateUser(config, store))
mux.HandleFunc("GET /user/delete/{id}", f.DeleteUser(config, db, store))
@ -83,10 +84,10 @@ func main() {
mux.HandleFunc("POST /article/resubmit/{id}", f.ResubmitArticle(config, db, store))
mux.HandleFunc("POST /article/submit", f.SubmitArticle(config, db, store))
mux.HandleFunc("POST /article/upload-banner", f.UploadBanner(config, store, "article-banner", "editor.html", "article-banner-template"))
mux.HandleFunc("POST /article/upload-image", f.UploadImage(config, store))
mux.HandleFunc("POST /article/upload-banner", f.UploadImage(config, store, "article-banner", "editor.html", "article-banner-template"))
mux.HandleFunc("POST /article/upload-image", f.UploadEasyMDEImage(config, store))
mux.HandleFunc("POST /issue/publish", f.PublishLatestIssue(config, db, store))
mux.HandleFunc("POST /issue/upload-banner", f.UploadBanner(config, store, "issue-banner", "current-issue.html", "issue-banner-template"))
mux.HandleFunc("POST /issue/upload-banner", f.UploadImage(config, store, "issue-banner", "current-issue.html", "issue-banner-template"))
mux.HandleFunc("POST /login", f.Login(config, db, store))
mux.HandleFunc("POST /pdf/upload", f.UploadPDF(config, store))
mux.HandleFunc("POST /tag/add", f.AddTag(config, db, store))
@ -94,6 +95,7 @@ func main() {
mux.HandleFunc("POST /user/add-first", f.AddFirstUser(config, db, store))
mux.HandleFunc("POST /user/update/{id}", f.UpdateUser(config, db, store))
mux.HandleFunc("POST /user/update/self", f.UpdateSelf(config, db, store))
mux.HandleFunc("POST /user/upload-profile-pic", f.UploadImage(config, store, "upload-profile-pic", "edit-user.html", "profile-pic-template"))
log.Fatalln(http.ListenAndServe(config.Port, mux))
}

View File

@ -1,9 +1,14 @@
[Unit]
Description=cpolis
After=network.target
[Service]
ExecStart=/usr/local/bin/cpolis
Restart=on-failure
User=cpolis
Group=cpolis
Restart=always
RestartSec=2s
Type=simple
[Install]
WantedBy=default.target
WantedBy=multi-user.target

View File

@ -1,56 +1,73 @@
DROP TABLE IF EXISTS articles_tags;
DROP TABLE IF EXISTS articles_contributors;
DROP TABLE IF EXISTS articles_authors;
DROP TABLE IF EXISTS tags;
DROP TABLE IF EXISTS articles;
DROP TABLE IF EXISTS issues;
DROP TABLE IF EXISTS users;
CREATE TABLE users (
id INT AUTO_INCREMENT,
username VARCHAR(15) NOT NULL UNIQUE,
password VARCHAR(60) NOT NULL,
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL,
role INT NOT NULL,
id INT AUTO_INCREMENT,
username VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(60) NOT NULL,
first_name VARCHAR(255) NOT NULL,
last_name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
profile_pic_link VARCHAR(255),
role INT NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE issues (
id INT AUTO_INCREMENT,
published BOOL NOT NULL,
id INT AUTO_INCREMENT,
published BOOL NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE articles (
id INT AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
banner_link VARCHAR(255),
summary TEXT NOT NULL,
content_link VARCHAR(255),
enc_url VARCHAR(255),
enc_length INT,
enc_type VARCHAR(255),
published BOOL NOT NULL,
rejected BOOL NOT NULL,
author_id INT NOT NULL,
issue_id INT NOT NULL,
edited_id INT,
is_in_issue BOOL NOT NULL,
auto_generated BOOL NOT NULL,
id INT AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
banner_link VARCHAR(255),
summary TEXT NOT NULL,
published BOOL NOT NULL,
rejected BOOL NOT NULL,
creator_id INT NOT NULL,
issue_id INT NOT NULL,
edited_id INT NOT NULL,
clicks INT NOT NULL,
is_in_issue BOOL NOT NULL,
auto_generated BOOL NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (author_id) REFERENCES users (id),
FOREIGN KEY (creator_id) REFERENCES users (id),
FOREIGN KEY (issue_id) REFERENCES issues (id)
);
CREATE TABLE tags (
id INT AUTO_INCREMENT,
name VARCHAR(50) NOT NULL UNIQUE,
id INT AUTO_INCREMENT,
name VARCHAR(50) NOT NULL UNIQUE,
PRIMARY KEY (id)
);
CREATE TABLE articles_authors (
article_id INT NOT NULL,
author_id INT NOT NULL,
PRIMARY KEY (article_id, author_id),
FOREIGN KEY (article_id) REFERENCES articles (id),
FOREIGN KEY (author_id) REFERENCES users (id)
);
CREATE TABLE articles_contributors (
article_id INT NOT NULL,
contributor_id INT NOT NULL,
PRIMARY KEY (article_id, contributor_id),
FOREIGN KEY (article_id) REFERENCES articles (id),
FOREIGN KEY (contributor_id) REFERENCES users (id)
);
CREATE TABLE articles_tags (
article_id INT,
tag_id INT,
article_id INT NOT NULL,
tag_id INT NOT NULL,
PRIMARY KEY (article_id, tag_id),
FOREIGN KEY (article_id) REFERENCES articles (id),
FOREIGN KEY (tag_id) REFERENCES tags (id)

91
go.mod
View File

@ -3,9 +3,8 @@ module streifling.com/jason/cpolis
go 1.23.2
require (
firebase.google.com/go/v4 v4.14.1
firebase.google.com/go/v4 v4.15.1
git.streifling.com/jason/atom v1.0.0
git.streifling.com/jason/rss v0.1.3
github.com/BurntSushi/toml v1.4.0
github.com/chai2010/webp v1.1.1
github.com/disintegration/imaging v1.6.2
@ -14,66 +13,64 @@ require (
github.com/gorilla/sessions v1.4.0
github.com/microcosm-cc/bluemonday v1.0.27
github.com/yuin/goldmark v1.7.8
golang.org/x/crypto v0.28.0
golang.org/x/term v0.25.0
google.golang.org/api v0.201.0
golang.org/x/crypto v0.32.0
golang.org/x/term v0.28.0
google.golang.org/api v0.216.0
)
require (
cel.dev/expr v0.16.2 // indirect
cloud.google.com/go v0.116.0 // indirect
cloud.google.com/go/auth v0.9.8 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
cloud.google.com/go/compute/metadata v0.5.2 // indirect
cloud.google.com/go/firestore v1.17.0 // indirect
cloud.google.com/go/iam v1.2.1 // indirect
cloud.google.com/go/longrunning v0.6.1 // indirect
cloud.google.com/go/monitoring v1.21.1 // indirect
cloud.google.com/go/storage v1.45.0 // indirect
cel.dev/expr v0.19.1 // indirect
cloud.google.com/go v0.118.0 // indirect
cloud.google.com/go/auth v0.14.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect
cloud.google.com/go/firestore v1.18.0 // indirect
cloud.google.com/go/iam v1.3.1 // indirect
cloud.google.com/go/longrunning v0.6.4 // indirect
cloud.google.com/go/monitoring v1.22.1 // indirect
cloud.google.com/go/storage v1.50.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.3 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.3 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.3 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.49.0 // indirect
github.com/MicahParks/keyfunc v1.9.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect
github.com/envoyproxy/go-control-plane v0.13.1 // indirect
github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.32.3 // indirect
github.com/envoyproxy/protoc-gen-validate v1.1.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-jwt/jwt/v4 v4.5.1 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/s2a-go v0.1.8 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.31.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect
go.opentelemetry.io/otel v1.31.0 // indirect
go.opentelemetry.io/otel/metric v1.31.0 // indirect
go.opentelemetry.io/otel/sdk v1.31.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.31.0 // indirect
go.opentelemetry.io/otel/trace v1.31.0 // indirect
golang.org/x/image v0.21.0 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/oauth2 v0.23.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.19.0 // indirect
golang.org/x/time v0.7.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.33.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
go.opentelemetry.io/otel v1.33.0 // indirect
go.opentelemetry.io/otel/metric v1.33.0 // indirect
go.opentelemetry.io/otel/sdk v1.33.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.33.0 // indirect
go.opentelemetry.io/otel/trace v1.33.0 // indirect
golang.org/x/image v0.23.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/oauth2 v0.25.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.9.0 // indirect
google.golang.org/appengine/v2 v2.0.6 // indirect
google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect
google.golang.org/grpc v1.67.1 // indirect
google.golang.org/grpc/stats/opentelemetry v0.0.0-20241018153737-98959d9a4904 // indirect
google.golang.org/protobuf v1.35.1 // indirect
google.golang.org/genproto v0.0.0-20250106144421-5f5ef82da422 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 // indirect
google.golang.org/grpc v1.69.2 // indirect
google.golang.org/protobuf v1.36.2 // indirect
)

282
go.sum
View File

@ -1,73 +1,63 @@
cel.dev/expr v0.16.2 h1:RwRhoH17VhAu9U5CMvMhH1PDVgf0tuz9FT+24AfMLfU=
cel.dev/expr v0.16.2/go.mod h1:gXngZQMkWJoSbE8mOzehJlXQyubn/Vg0vR9/F3W7iw8=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=
cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U=
cloud.google.com/go/auth v0.9.8 h1:+CSJ0Gw9iVeSENVCKJoLHhdUykDgXSc4Qn+gu2BRtR8=
cloud.google.com/go/auth v0.9.8/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI=
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.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo=
cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k=
cloud.google.com/go/firestore v1.17.0 h1:iEd1LBbkDZTFsLw3sTH50eyg4qe8eoG6CjocmEXO9aQ=
cloud.google.com/go/firestore v1.17.0/go.mod h1:69uPx1papBsY8ZETooc71fOhoKkD70Q1DwMrtKuOT/Y=
cloud.google.com/go/iam v1.2.1 h1:QFct02HRb7H12J/3utj0qf5tobFh9V4vR6h9eX5EBRU=
cloud.google.com/go/iam v1.2.1/go.mod h1:3VUIJDPpwT6p/amXRC5GY8fCCh70lxPygguVtI0Z4/g=
cloud.google.com/go/logging v1.11.0 h1:v3ktVzXMV7CwHq1MBF65wcqLMA7i+z3YxbUsoK7mOKs=
cloud.google.com/go/logging v1.11.0/go.mod h1:5LDiJC/RxTt+fHc1LAt20R9TKiUTReDg6RuuFOZ67+A=
cloud.google.com/go/longrunning v0.6.1 h1:lOLTFxYpr8hcRtcwWir5ITh1PAKUD/sG2lKrTSYjyMc=
cloud.google.com/go/longrunning v0.6.1/go.mod h1:nHISoOZpBcmlwbJmiVk5oDRz0qG/ZxPynEGs1iZ79s0=
cloud.google.com/go/monitoring v1.21.1 h1:zWtbIoBMnU5LP9A/fz8LmWMGHpk4skdfeiaa66QdFGc=
cloud.google.com/go/monitoring v1.21.1/go.mod h1:Rj++LKrlht9uBi8+Eb530dIrzG/cU/lB8mt+lbeFK1c=
cloud.google.com/go/storage v1.45.0 h1:5av0QcIVj77t+44mV4gffFC/LscFRUhto6UBMB5SimM=
cloud.google.com/go/storage v1.45.0/go.mod h1:wpPblkIuMP5jCB/E48Pz9zIo2S/zD8g+ITmxKkPCITE=
cloud.google.com/go/trace v1.11.1 h1:UNqdP+HYYtnm6lb91aNA5JQ0X14GnxkABGlfz2PzPew=
cloud.google.com/go/trace v1.11.1/go.mod h1:IQKNQuBzH72EGaXEodKlNJrWykGZxet2zgjtS60OtjA=
cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4=
cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
cloud.google.com/go v0.118.0 h1:tvZe1mgqRxpiVa3XlIGMiPcEUbP1gNXELgD4y/IXmeQ=
cloud.google.com/go v0.118.0/go.mod h1:zIt2pkedt/mo+DQjcT4/L3NDxzHPR29j5HcclNH+9PM=
cloud.google.com/go/auth v0.14.0 h1:A5C4dKV/Spdvxcl0ggWwWEzzP7AZMJSEIgrkngwhGYM=
cloud.google.com/go/auth v0.14.0/go.mod h1:CYsoRL1PdiDuqeQpZE0bP2pnPrGqFcOkI0nldEQis+A=
cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M=
cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc=
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s=
cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU=
cloud.google.com/go/iam v1.3.1 h1:KFf8SaT71yYq+sQtRISn90Gyhyf4X8RGgeAVC8XGf3E=
cloud.google.com/go/iam v1.3.1/go.mod h1:3wMtuyT4NcbnYNPLMBzYRFiEfjKfJlLVLrisE7bwm34=
cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc=
cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA=
cloud.google.com/go/longrunning v0.6.4 h1:3tyw9rO3E2XVXzSApn1gyEEnH2K9SynNQjMlBi3uHLg=
cloud.google.com/go/longrunning v0.6.4/go.mod h1:ttZpLCe6e7EXvn9OxpBRx7kZEB0efv8yBO6YnVMfhJs=
cloud.google.com/go/monitoring v1.22.1 h1:KQbnAC4IAH+5x3iWuPZT5iN9VXqKMzzOgqcYB6fqPDE=
cloud.google.com/go/monitoring v1.22.1/go.mod h1:AuZZXAoN0WWWfsSvET1Cpc4/1D8LXq8KRDU87fMS6XY=
cloud.google.com/go/storage v1.50.0 h1:3TbVkzTooBvnZsk7WaAQfOsNrdoM8QHusXA1cpk6QJs=
cloud.google.com/go/storage v1.50.0/go.mod h1:l7XeiD//vx5lfqE3RavfmU9yvk5Pp0Zhcv482poyafY=
cloud.google.com/go/trace v1.11.3 h1:c+I4YFjxRQjvAhRmSsmjpASUKq88chOX854ied0K/pE=
cloud.google.com/go/trace v1.11.3/go.mod h1:pt7zCYiDSQjC9Y2oqCsh9jF4GStB/hmjrYLsxRR27q8=
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=
firebase.google.com/go/v4 v4.15.1 h1:tR2dzKw1MIfCfG2bhAyxa5KQ57zcE7iFKmeYClET6ZM=
firebase.google.com/go/v4 v4.15.1/go.mod h1:eunxbsh4UXI2rA8po3sOiebvWYuW0DVxAdZFO0I6wdY=
git.streifling.com/jason/atom v1.0.0 h1:E88z4S7JeT6T+WuAaJWnGwCWTx+vzSJ6giUL51MdptI=
git.streifling.com/jason/atom v1.0.0/go.mod h1:FNTYJfatYaIOQn4OKy8y+Mtohqm3MeyEGZUu4bMtZ9E=
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/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.3 h1:cb3br57K508pQEFgBxn9GDhPS9HefpyMPK1RzmtMNzk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.3/go.mod h1:itPGVDKf9cC/ov4MdvJ2QZ0khw4bfoo9jzwTJlaxy2k=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.3 h1:xir5X8TS8UBVPWg2jHL+cSTf0jZgqYQSA54TscSt1/0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.3/go.mod h1:SsdWig2J5PMnfMvfJuEb1uZa8Y+kvNyvrULFo69gTFk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.48.3 h1:Nl7phYyHjnqofWDpD+6FYdiwtNIxebn0AHLry7Sxb0M=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.48.3/go.mod h1:pNP/L2wDlaQnQlFvkDKGSruDoYRpmAxB6drgsskfYwg=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.3 h1:2vcVkrNdSMJpoOVAWi9ApsQR5iqNeFGt5Qx8Xlt3IoI=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.3/go.mod h1:wRbFgBQUVm1YXrvWKofAEmq9HNJTDphbAaJSSX01KUI=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0 h1:o90wcURuxekmXrtxmYWTyNla0+ZEHhud6DI1ZTxd1vI=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.49.0/go.mod h1:6fTWu4m3jocfUZLYF5KsZC1TUfRvEjs7lM4crme/irw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.49.0 h1:jJKWl98inONJAr/IZrdFQUWcwUO95DLY1XMD1ZIut+g=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.49.0/go.mod h1:l2fIqmwB+FKSfvn3bAD/0i+AXAxhIZjTK2svT/mgUXs=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.49.0 h1:GYUJLfvd++4DMuMhCFLgLXvFwofIxh/qOwoGuS/LTew=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.49.0/go.mod h1:wRbFgBQUVm1YXrvWKofAEmq9HNJTDphbAaJSSX01KUI=
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/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g=
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chai2010/webp v1.1.1 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk=
github.com/chai2010/webp v1.1.1/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI=
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 h1:boJj011Hh+874zpIySeApCX4GeOjPl9qhRF3QuIZq+Q=
github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.13.1 h1:vPfJZCkob6yTMEgS+0TwfTUfbHjfy/6vOJ8hUWX/uXE=
github.com/envoyproxy/go-control-plane v0.13.1/go.mod h1:X45hY0mufo6Fd0KW3rqsGvQMw58jvjymeCzBU3mWyHw=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
github.com/envoyproxy/go-control-plane/envoy v1.32.3 h1:hVEaommgvzTjTd4xCaFd+kEQ2iYBtGxP6luyLrx6uOk=
github.com/envoyproxy/go-control-plane/envoy v1.32.3/go.mod h1:F6hWupPfh75TBXGKA++MCT/CZHFq5r9/uwt/kQYkZfE=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
@ -80,31 +70,13 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre
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-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
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=
@ -112,15 +84,14 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
github.com/googleapis/enterprise-certificate-proxy v0.3.4/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/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
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=
@ -133,140 +104,91 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgm
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.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.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/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/detectors/gcp v1.31.0 h1:G1JQOreVrfhRkner+l4mrGxmfqYCAuy76asTDAo0xsA=
go.opentelemetry.io/contrib/detectors/gcp v1.31.0/go.mod h1:tzQL6E1l+iV44YFTkcAeNQqzXUiekSYP9jjJjXwEd00=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0 h1:yMkBS9yViCc7U7yeLzJPM2XizlfdVvBRSmsQDWu6qc0=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0/go.mod h1:n8MR6/liuGB5EmTETUBeU5ZgqMOlqKRxUaqPQBOANZ8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk=
go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0=
go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc=
go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8=
go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/detectors/gcp v1.33.0 h1:FVPoXEoILwgbZUu4X7YSgsESsAmGRgoYcnXkzgQPhP4=
go.opentelemetry.io/contrib/detectors/gcp v1.33.0/go.mod h1:ZHrLmr4ikK2AwRj9QL+c9s2SOlgoSRyMpNVzUj2fZqI=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I=
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
go.opentelemetry.io/otel/sdk/metric v1.33.0 h1:Gs5VK9/WUJhNXZgn8MR6ITatvAmKeIuCtNbsP3JkNqU=
go.opentelemetry.io/otel/sdk/metric v1.33.0/go.mod h1:dL5ykHZmm1B1nVRk9dDjChwDmt81MjVp3gLkQRwKf/Q=
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
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.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78=
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/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
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.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.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/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
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/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.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.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
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.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.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.201.0 h1:+7AD9JNM3tREtawRMu8sOjSbb8VYcYXJG/2eEOmfDu0=
google.golang.org/api v0.201.0/go.mod h1:HVY0FCHVs89xIW9fzf/pBvOEm+OolHa86G/txFezyq4=
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/api v0.216.0 h1:xnEHy+xWFrtYInWPy8OdGFsyIfWJjtVnO39g7pz2BFY=
google.golang.org/api v0.216.0/go.mod h1:K9wzQMvWi47Z9IU7OgdOofvZuw75Ge3PPITImZR/UyI=
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-20241015192408-796eee8c2d53 h1:Df6WuGvthPzc+JiQ/G+m+sNX24kc0aTBqoDN/0yyykE=
google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53/go.mod h1:fheguH3Am2dGp1LfXkrvwqC/KlFq8F0nLq3LryOMrrE=
google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 h1:fVoAXEKA4+yufmbdVYv+SE73+cPZbbbe8paLsHfkK+U=
google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
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.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/grpc/stats/opentelemetry v0.0.0-20241018153737-98959d9a4904 h1:Lplo3VKrYtWeryBkDI4SZ4kJTFaWO4qUGs+xX7N2bFc=
google.golang.org/grpc/stats/opentelemetry v0.0.0-20241018153737-98959d9a4904/go.mod h1:jzYlkSMbKypzuu6xoAEijsNVo9ZeDF1u/zCfFgsx7jg=
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/genproto v0.0.0-20250106144421-5f5ef82da422 h1:6GUHKGv2huWOHKmDXLMNE94q3fBDlEHI+oTRIZSebK0=
google.golang.org/genproto v0.0.0-20250106144421-5f5ef82da422/go.mod h1:1NPAxoesyw/SgLPqaUp9u1f9PWCLAk/jVmhx7gJZStg=
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24=
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 h1:3UsHvIr4Wc2aW4brOaSCmcxh9ksica6fHEr8P1XhkYw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4=
google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
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.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
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=
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -24,7 +24,7 @@ textarea {
}
.btn-area {
@apply grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-1 mt-4;
@apply grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-1;
}
.btn-area-3 {
@ -32,41 +32,41 @@ textarea {
}
.btn {
@apply bg-slate-200 dark:bg-slate-800 hover:bg-slate-100 dark:hover:bg-slate-900 border border-slate-200 dark:border-slate-800 my-2 px-3 py-2 rounded-md w-full;
@apply bg-slate-200 dark:bg-slate-800 hover:bg-slate-100 dark:hover:bg-slate-900 border border-slate-200 dark:border-slate-800 cursor-pointer my-2 px-3 py-2 rounded-md w-full;
}
.action-btn {
@apply bg-slate-800 dark:bg-slate-200 hover:bg-slate-700 dark:hover:bg-slate-300 my-2 px-3 py-2 rounded-md text-slate-50 dark:text-slate-950 w-full;
@apply bg-slate-800 dark:bg-slate-200 hover:bg-slate-700 dark:hover:bg-slate-300 cursor-pointer my-2 px-3 py-2 rounded-md text-slate-50 dark:text-slate-950 w-full;
}
.EasyMDEContainer .CodeMirror {
@apply bg-slate-50 dark:bg-slate-950 border-slate-200 dark:border-slate-800 text-slate-900 dark:text-slate-100
@apply bg-slate-50 dark:bg-slate-950 border-slate-200 dark:border-slate-800 text-slate-900 dark:text-slate-100;
}
.EasyMDEContainer .cm-s-easymde .CodeMirror-cursor {
@apply border-slate-900 dark:border-slate-100
@apply border-slate-900 dark:border-slate-100;
}
.EasyMDEContainer .editor-toolbar > * {
@apply text-slate-900 dark:text-slate-100
@apply text-slate-900 dark:text-slate-100;
}
.EasyMDEContainer .editor-toolbar > .active, .editor-toolbar > button:hover, .editor-preview pre, .cm-s-easymde .cm-comment {
@apply bg-slate-100 dark:bg-slate-900
@apply bg-slate-100 dark:bg-slate-900;
}
.EasyMDEContainer .CodeMirror-fullscreen {
@apply bg-slate-50 dark:bg-slate-950
@apply bg-slate-50 dark:bg-slate-950;
}
.editor-toolbar {
@apply border border-slate-200 dark:border-slate-800
@apply border border-slate-200 dark:border-slate-800;
}
.editor-toolbar.fullscreen {
@apply bg-slate-50 dark:bg-slate-950
@apply bg-slate-50 dark:bg-slate-950;
}
.editor-preview {
@apply bg-slate-50 dark:bg-slate-950
@apply bg-slate-50 dark:bg-slate-950;
}

View File

@ -1,52 +0,0 @@
{{define "page-content"}}
<h2>Neuer Benutzer</h2>
<form>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
<div>
<label for="username">Benutzername</label>
<input class="w-full" required name="username" type="text" value="{{.UserName}}" />
</div>
<div>
<label for="password">Passwort</label>
<input class="w-full" required name="password" placeholder="***" type="password" />
</div>
<div>
<label for="password2">Passwort wiederholen</label>
<input class="w-full" required name="password2" placeholder="***" type="password" />
</div>
<div>
<label for="first-name">Vorname</label>
<input class="w-full" required name="first-name" type="text" value="{{.FirstName}}" />
</div>
<div>
<label for="last-name">Nachname</label>
<input class="w-full" required name="last-name" type="text" value="{{.LastName}}" />
</div>
</div>
<div class="flex flex-wrap gap-4">
<div>
<input required id="author" name="role" type="radio" value="3" {{if eq .Role 3 }}checked{{end}} />
<label for="author">Autor</label>
</div>
<div>
<input required id="editor" name="role" type="radio" value="2" {{if eq .Role 2 }}checked{{end}} />
<label for="editor">Redakteur</label>
</div>
<div>
<input required id="publisher" name="role" type="radio" value="1" {{if eq .Role 1 }}checked{{end}} />
<label for="publisher">Herausgeber</label>
</div>
<div>
<input required id="admin" name="role" type="radio" value="0" {{if eq .Role 0 }}checked{{end}} />
<label for="admin">Administrator</label>
</div>
</div>
<div class="btn-area">
<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>
</div>
</form>
{{end}}

View File

@ -13,7 +13,7 @@
</div>
<div class="grid grid-cols-1 items-center">
<label class="btn cursor-pointer text-center" for="issue-banner-upload">Titelbild</label>
<label class="btn text-center" for="issue-banner-upload">Titelbild</label>
<input class="hidden" id="issue-banner-upload" name="issue-banner" type="file" required
hx-post="/issue/upload-banner" hx-target="#issue-banner-container" />
</div>
@ -79,8 +79,8 @@
{{end}}
{{define "issue-banner-template"}}
<div class="w-full" id="issue-banner-container">
<img src="data:image/webp;base64,{{.BannerImage}}" alt="Banner Image">
<input id="issue-banner-url" name="issue-banner-url" type="hidden" value="{{.URL}}" />
<div id="issue-banner-container">
<img src="/image/serve/{{.Image}}" alt="" />
<input id="issue-banner-url" name="issue-banner-url" type="hidden" value="{{.Image}}" />
</div>
{{end}}

View File

@ -1,43 +0,0 @@
{{define "page-content"}}
<h2>Profil bearbeiten</h2>
<form>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
<div>
<label for="username">Benutzername</label>
<input class="w-full" name="username" type="text" value="{{.UserName}}" />
</div>
<div>
<label for="first-name">Vorname</label>
<input class="w-full" name="first-name" type="text" value="{{.FirstName}}" />
</div>
<div>
<label for="last-name">Nachname</label>
<input class="w-full" name="last-name" type="text" value="{{.LastName}}" />
</div>
<div>
<label for="old-password">Altes Passwort</label>
<input class="w-full" name="old-password" placeholder="***" type="password" />
</div>
<div>
<label for="password">Passwort</label>
<input class="w-full" name="password" placeholder="***" type="password" />
</div>
<div>
<label for="password2">Passwort wiederholen</label>
<input class="w-full" name="password2" placeholder="***" type="password" />
</div>
</div>
<div class="btn-area">
<input class="action-btn" type="submit" value="Aktualisieren" hx-post="/user/update/self"
hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
</div>
</form>
{{end}}

View File

@ -1,57 +1,90 @@
{{define "page-content"}}
<h2>Profil von {{.FirstName}} {{.LastName}} bearbeiten</h2>
<h2>{{.Title}}</h2>
<form class="flex flex-col gap-4" hx-encoding="multipart/form-data">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{{template "profile-pic-template" .}}
<form>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
<div>
<label for="username">Benutzername</label>
<input class="w-full" name="username" type="text" value="{{.UserName}}" />
</div>
<div>
<label for="first-name">Vorname</label>
<input class="w-full" name="first-name" type="text" value="{{.FirstName}}" />
</div>
<div>
<label for="last-name">Nachname</label>
<input class="w-full" name="last-name" type="text" value="{{.LastName}}" />
<input class="w-full" required name="username" type="text" {{if lt .Role 4}}value="{{.UserName}}" {{end}} />
</div>
<div>
<label for="password">Passwort</label>
<input class="w-full" name="password" placeholder="***" type="password" />
<input class="w-full" required name="password" placeholder="***" type="password" />
</div>
<div>
<label for="password2">Passwort wiederholen</label>
<input class="w-full" name="password2" placeholder="***" type="password" />
<input class="w-full" required name="password2" placeholder="***" type="password" />
</div>
<div>
<label for="first-name">Vorname</label>
<input class="w-full" required name="first-name" type="text" {{if lt .Role 4}}value="{{.FirstName}}"
{{end}} />
</div>
<div>
<label for="last-name">Nachname</label>
<input class="w-full" required name="last-name" type="text" {{if lt .Role 4}}value="{{.LastName}}"
{{end}} />
</div>
<div>
<label for="email">Emailadresse</label>
<input class="w-full" required name="email" type="text" {{if lt .Role 4}}value="{{.Email}}" {{end}} />
</div>
<div>
<label for="email2">Emailadresse wiederholen</label>
<input class="w-full" required name="email2" type="text" {{if lt .Role 4}}value="{{.Email}}" {{end}} />
</div>
</div>
{{if lt .Role 4}}
<div class="flex flex-wrap gap-4">
<div>
<input required id="author" name="role" type="radio" value="3" {{if eq .Role 3 }}checked{{end}} />
<input required id="author" name="role" type="radio" value="3" {{if eq .Role 3}}checked{{end}} />
<label for="author">Autor</label>
</div>
<div>
<input required id="editor" name="role" type="radio" value="2" {{if eq .Role 2 }}checked{{end}} />
<input required id="editor" name="role" type="radio" value="2" {{if eq .Role 2}}checked{{end}} />
<label for="editor">Redakteur</label>
</div>
<div>
<input required id="publisher" name="role" type="radio" value="1" {{if eq .Role 1 }}checked{{end}} />
<input required id="publisher" name="role" type="radio" value="1" {{if eq .Role 1}}checked{{end}} />
<label for="publisher">Herausgeber</label>
</div>
<div>
<input required id="admin" name="role" type="radio" value="0" {{if eq .Role 0 }}checked{{end}} />
<input required id="admin" name="role" type="radio" value="0" {{if eq .Role 0}}checked{{end}} />
<label for="admin">Administrator</label>
</div>
</div>
{{end}}
<div class="btn-area">
<input class="action-btn" type="submit" value="Aktualisieren" hx-post="/user/update/{{.ID}}"
hx-target="#page-content" />
<input class="action-btn" type="submit" value="{{.ButtonText}}" hx-post="{{.URL}}" hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
</div>
</form>
{{end}}
{{define "profile-pic-template"}}
<div class="flex items-center justify-center row-span-3 self-center" id="profile-pic-container">
<label
class="bg-slate-200 dark:bg-slate-800 hover:bg-slate-100 dark:hover:bg-slate-900 border border-slate-200 dark:border-slate-800 cursor-pointer flex flex-col h-48 items-center justify-center overflow-hidden rounded-full w-48"
for="upload-profile-pic">
{{if gt (len .Image) 0}}
<img src="/image/serve/{{.Image}}" alt="" />
{{else}}
<span class="text-2xl">+</span>
<span>Profilbild</span>
{{end}}
</label>
<input class="hidden" id="upload-profile-pic" name="upload-profile-pic" type="file"
hx-post="/user/upload-profile-pic" hx-swap="outerHTML" hx-target="#profile-pic-container" />
<input id="profile-pic-url" name="profile-pic-url" type="hidden" value="{{.Image}}" />
</div>
{{end}}

View File

@ -1,41 +1,38 @@
{{define "page-content"}}
<h2>Editor</h2>
<form id="edit-area" hx-encoding="multipart/form-data">
<form class="flex flex-col gap-4" id="edit-area" hx-encoding="multipart/form-data">
<div class="flex flex-col gap-y-1">
<div class="w-full" id="article-banner-container">
<img src="data:image/webp;base64,{{.BannerImage}}" alt="Banner Image">
<input id="article-banner-url" name="article-banner-url" type="hidden" value="{{.Article.BannerLink}}" />
</div>
{{template "article-banner-template" .}}
<div class="grid grid-cols-2 gap-4 items-center">
<div class="flex flex-col">
<label for="article-title">Titel</label>
<h3>Titel</h3>
<input name="article-title" type="text" value="{{.Article.Title}}" />
</div>
<div class="grid grid-cols-1 items-center">
<label class="btn cursor-pointer text-center" for="article-banner">Titelbild</label>
<div class="flex">
<label class="btn text-center" for="article-banner">Titelbild</label>
<input class="hidden" id="article-banner" name="article-banner" type="file" required
hx-post="/article/upload-banner" hx-target="#article-banner-container" />
hx-post="/article/upload-banner" hx-swap="outerHTML" hx-target="#article-banner-container" />
</div>
</div>
</div>
<div class="flex flex-col gap-y-1">
<label for="article-summary">Beschreibung</label>
<div class="flex flex-col gap-1">
<h3>Beschreibung</h3>
<textarea name="article-summary">{{.Article.Summary}}</textarea>
</div>
<div class="flex flex-col gap-y-1">
<label for="easyMDE">Artikel</label>
<div class="flex flex-col gap-1">
<h3>Artikel</h3>
<textarea id="easyMDE">{{.Content}}</textarea>
<input id="article-content" name="article-content" type="hidden" value="{{.Content}}" />
</div>
<div>
<span>Tags</span>
<div class="flex flex-wrap gap-x-4">
<h3>Tags</h3>
<div class="flex flex-wrap gap-4">
<div>
<input id="issue" name="issue" type="checkbox" {{if .Article.IsInIssue}}checked{{end}} />
<label for="issue">Orient Express</label>
@ -51,6 +48,38 @@
</div>
</div>
<div>
<h3>Beteiligte</h3>
{{range .ArticleUsers}}
<div class="border border-slate-200 dark:border-slate-800 flex gap-4 px-2 py-1 rounded-md">
<span>{{.FirstName}} {{.LastName}}: </span>
<div>
<input id="{{.ID}}-author" name="user-{{.ID}}" type="radio" value="author" {{if eq .ArticleRole
1}}checked{{end}} />
<label for="{{.ID}}-author">Autor</label>
</div>
<div>
<input id="{{.ID}}-contributor" name="user-{{.ID}}" type="radio" value="contributor" {{if eq
.ArticleRole 2}}checked{{end}} />
<label for="{{.ID}}-contributor">Mitwirkender</label>
</div>
<div>
<input id="{{.ID}}-none" name="user-{{.ID}}" type="radio" {{if eq .ArticleRole 0}}checked{{end}} />
<label for="{{.ID}}-none">Unbeteiligt</label>
</div>
</div>
{{end}}
</div>
<div>
<input id="creator" name="creator" type="checkbox" value="author" {{if eq .Creator.ArticleRole
1}}checked{{end}} />
<label for="creator">Ich bin Autor.</label>
</div>
<div class="btn-area">
<input class="action-btn" type="submit" value="Senden" hx-post="/article/{{.Action}}"
hx-target="#page-content" />
@ -92,8 +121,8 @@
{{end}}
{{define "article-banner-template"}}
<div class="w-full" id="article-banner-container">
<img src="data:image/webp;base64,{{.BannerImage}}" alt="Banner Image">
<input id="article-banner-url" name="article-banner-url" type="hidden" value="{{.URL}}" />
<div id="article-banner-container">
<img src="/image/serve/{{.Image}}" alt="" />
<input id="article-banner-url" name="article-banner-url" type="hidden" value="{{.Image}}" />
</div>
{{end}}

View File

@ -1,32 +0,0 @@
{{define "page-content"}}
<h2>Erster Benutzer (Administrator)</h2>
<form>
<div class="grid grid-cols-3 gap-4">
<div>
<label for="username">Benutzername</label>
<input class="w-full" required name="username" type="text" value="{{.UserName}}" />
</div>
<div>
<label for="password">Passwort</label>
<input class="w-full" required name="password" placeholder="***" type="password" />
</div>
<div>
<label for="password2">Passwort wiederholen</label>
<input class="w-full" required name="password2" placeholder="***" type="password" />
</div>
<div>
<label for="first-name">Vorname</label>
<input class="w-full" required name="first-name" type="text" value="{{.FirstName}}" />
</div>
<div>
<label for="last-name">Nachname</label>
<input class="w-full" required name="last-name" type="text" value="{{.LastName}}" />
</div>
</div>
<div class="btn-area-1">
<input class="action-btn" type="submit" value="Anlegen" hx-post="/user/add-first" hx-target="#page-content" />
</div>
</form>
{{end}}

View File

@ -2,30 +2,31 @@
<div class="flex flex-col gap-4">
<button class="btn" hx-get="/logout" hx-target="#page-content">Abmelden</button>
{{if lt . 4}}
{{if lt .Role 4}}
<div class="mb-3">
<h2>Artikel</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-2">
<button class="btn" hx-get="/article/write" hx-target="#page-content">Artikel schreiben</button>
<button class="btn" hx-get="/article/all-rejected" hx-target="#page-content">Artikel bearbeiten</button>
{{if lt . 3}}<button class="btn" hx-get="/article/all-unpublished-unrejected-and-published-rejected"
{{if lt .Role 3}}<button class="btn" hx-get="/article/all-unpublished-unrejected-and-published-rejected"
hx-target="#page-content">Artikel veröffentlichen</button>{{end}}
{{if lt . 2}}<button class="btn" hx-get="/article/all-published/delete" hx-target="#page-content">Artikel
löschen</button>{{end}}
{{if lt . 2}}<button class="btn" hx-get="/article/all-published/review-edit"
{{if lt .Role 2}}<button class="btn" hx-get="/article/all-published/delete"
hx-target="#page-content">Artikel löschen</button>{{end}}
{{if lt .Role 2}}<button class="btn" hx-get="/article/all-published/review-edit"
hx-target="#page-content">Artikel bearbeiten lassen</button>{{end}}
{{if lt . 3}}<button class="btn" hx-get="/tag/create" hx-target="#page-content">Neuer Tag</button>{{end}}
{{if lt .Role 3}}<button class="btn" hx-get="/tag/create" hx-target="#page-content">Neuer
Tag</button>{{end}}
</div>
</div>
{{end}}
{{if lt . 2}}
{{if lt .Role 2}}
<div class="mb-3">
<h2>Ausgabe</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-2">
<button class="btn" hx-get="/issue/this" hx-target="#page-content">Diese Ausgabe</button>
<form class="flex" hx-encoding="multipart/form-data">
<label class="btn cursor-pointer text-center" for="pdf-upload">PDF hochladen</label>
<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>
@ -33,16 +34,16 @@
{{end}}
</div>
{{if lt . 4}}
{{if lt .Role 4}}
<div class="mb-3">
<h2>Benutzer</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-2">
<button class="btn" hx-get="/user/edit/self" hx-target="#page-content">Mein Profil bearbeiten</button>
{{if eq . 0}}<button class="btn" hx-get="/user/create" hx-target="#page-content">Benutzer
{{if eq .Role 0}}<button class="btn" hx-get="/user/create" hx-target="#page-content">Benutzer
hinzufügen</button>{{end}}
{{if eq . 0}}<button class="btn" hx-get="/user/show-all/edit" hx-target="#page-content">Benutzer
{{if eq .Role 0}}<button class="btn" hx-get="/user/show-all/edit" hx-target="#page-content">Benutzer
bearbeiten</button>{{end}}
{{if eq . 0}}<button class="btn" hx-get="/user/show-all/delete" hx-target="#page-content">Benutzer
{{if eq .Role 0}}<button class="btn" hx-get="/user/show-all/delete" hx-target="#page-content">Benutzer
löschen</button>{{end}}
</div>
</div>

View File

@ -39,10 +39,10 @@
<footer class="text-center text-gray-500 my-8">
<p>&copy; 2024 Jason Streifling. Alle Rechte vorbehalten.</p>
<p>v0.13.0 - <strong>Alpha: Drastische Änderungen und Fehler vorbehalten.</strong></p>
<p>{{.Version}} - <strong>Alpha: Drastische Änderungen und Fehler vorbehalten.</strong></p>
</footer>
<script src="https://unpkg.com/htmx.org@2.0.2"></script>
<script src="https://unpkg.com/htmx.org@2.0.3"></script>
<script src="https://unpkg.com/easymde/dist/easymde.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {

View File

@ -3,27 +3,27 @@
<div>
<div class="w-full" id="article-banner-container">
<img src="data:image/webp;base64,{{.BannerImage}}" alt="Banner Image">
<img src="/image/serve/{{.Image}}" alt="Banner Image">
</div>
<span>Titel</span>
<h3>Titel</h3>
<div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full">
{{.Article.Title}}
</div>
<span>Beschreibung</span>
<h3>Beschreibung</h3>
<div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full">
{{.Article.Summary}}
</div>
<span>Artikel</span>
<h3>Artikel</h3>
<div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full">
<div class="prose text-slate-900 dark:text-slate-100">
{{.HTMLContent}}
</div>
</div>
<span>Tags</span>
<h3>Tags</h3>
<div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full">
{{if .Article.IsInIssue}}
<span>Orient Express</span>
@ -35,6 +35,22 @@
{{end}}
</div>
<h3>Autoren</h3>
<div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full">
{{range .Authors}}
<span>{{.FirstName}} {{.LastName}}</span>
<br>
{{end}}
</div>
<h3>Mitwirkende</h3>
<div class="border border-slate-200 dark:border-slate-800 mb-3 px-2 py-2 rounded-md w-full">
{{range .Contributors}}
<span>{{.FirstName}} {{.LastName}}</span>
<br>
{{end}}
</div>
{{if eq .Action "publish"}}
<div class="btn-area-3">
<input class="action-btn" type="submit" value="{{.ActionButton}}" hx-get="/article/{{.Action}}/{{.Article.ID}}"