Compare commits

..

53 Commits

Author SHA1 Message Date
e0fa31b7a1 Merge branch 'devel' 2025-01-26 08:53:36 +01:00
344864f5b2 Correct version number 2025-01-26 08:53:06 +01:00
09350bab0e Stop trying to use docker, it sucks 2025-01-25 19:47:03 +01:00
722022faec Update atom dependency 2025-01-24 23:19:18 +01:00
586b1cdef0 Update dependencies 2025-01-24 20:26:02 +01:00
1f72891896 Fix mariadb version 2025-01-24 20:24:32 +01:00
11d836dd26 Use DB_PASS for root and regular password 2025-01-24 20:10:32 +01:00
57953b2cfd Fix htmx version 2025-01-24 20:09:58 +01:00
a93603eac0 Use generic /bin/sh in docker-start.sh 2025-01-24 20:09:47 +01:00
1b72f05add Fix golang and tailwind version in Dockerfile 2025-01-24 20:09:23 +01:00
2c6b15bc6d Make docker-start.sh executable 2025-01-24 18:52:00 +01:00
cafb158323 Bug fix 2025-01-24 18:40:58 +01:00
40fbb93732 Add docker-start.sh 2025-01-24 18:28:29 +01:00
b120341d78 Correct docker-compose.yml 2025-01-24 18:28:18 +01:00
095576a234 Add tailwindcss to Dockerfile 2025-01-24 18:13:13 +01:00
9199f202be Check atom feed for nil returns 2025-01-24 17:42:05 +01:00
3d08cc7612 Make background image check way more efficient 2025-01-20 20:25:50 +01:00
a7b6fb9705 Correct probably last occurance of manually set path delimeter 2025-01-19 20:41:48 +01:00
2f4d5d4c7c Correct comment 2025-01-19 20:35:35 +01:00
5b417ef87d Change ValidateSession() to SessionIsActive() which only returns a bool 2025-01-19 20:34:12 +01:00
04283d5917 Restore old way of managing session while keeping slimmed down version 2025-01-19 20:29:10 +01:00
d882daeb01 Correct command line flag for ImgDir 2025-01-19 20:15:56 +01:00
f99358729c Create ValidateSession() to not have unwanted side effects when validating session 2025-01-19 20:10:51 +01:00
7b04149a28 Rename PicsDir to ImgDir 2025-01-19 20:10:06 +01:00
43c1cb6d9a Use proper filepaths for docker-compose 2025-01-19 20:08:58 +01:00
9feb16a8d8 Change filepaths to use filepath.Join() where possible 2025-01-19 20:07:32 +01:00
6885dfbb38 Add the ability to run cpolis from docker 2025-01-19 15:17:38 +01:00
ed614026ec Merge branch 'devel' 2025-01-19 10:16:44 +01:00
86a16629fd Change htmx version to @latest 2025-01-19 10:16:07 +01:00
6d402aca32 Update copyright notice 2025-01-19 10:13:18 +01:00
87f8786c43 Merge branch 'devel' 2025-01-19 10:04:36 +01:00
0af4a70aed Include proper image cleanup 2025-01-19 10:04:16 +01:00
cc8693ffaf Serve article clicks via uuid 2025-01-19 09:34:03 +01:00
b8fd81a86d Use proper filepaths 2025-01-19 09:31:19 +01:00
62e75eaea8 Use the correct tmpDir 2025-01-19 09:00:02 +01:00
5d251c3659 Make all files and directories in the config absolute 2025-01-19 08:59:36 +01:00
5552e6d2eb Bug fix 2025-01-18 18:18:03 +01:00
77a64d5179 Bug fix 2025-01-17 19:37:44 +01:00
fb842d203e Fix bug 2025-01-17 19:35:01 +01:00
9c0c7361a0 Serve articles via uuid 2025-01-17 19:07:55 +01:00
acae07b8f3 Merge branch 'devel' 2025-01-17 18:13:12 +01:00
52b5dbf2c7 Change the way names are given to uploaded pdfs to include the original document name 2025-01-17 18:13:04 +01:00
1b100483de Merge branch 'devel' 2025-01-17 17:52:45 +01:00
7b9438233e Comment out not ready image cleanup function 2025-01-17 17:52:35 +01:00
ff36d65cc3 Add ability to clean up unused images 2025-01-17 05:04:03 +01:00
3d3cda319d Initial version of .docx upload support 2025-01-17 05:02:31 +01:00
0e825a55e5 Fix bug not letting the first user have a session after user creation 2025-01-17 05:01:09 +01:00
88776337e4 Bug fix 2025-01-14 22:20:46 +01:00
f8691eb180 Cleanup 2025-01-14 21:48:09 +01:00
60e09175da Remove unused session.Article attribute 2025-01-14 21:31:57 +01:00
13a6039490 Bug fix 2025-01-14 21:27:24 +01:00
d953b4135c Initial version of native session management 2025-01-14 20:53:49 +01:00
4da9792b0f Minor cleanup 2025-01-12 15:29:32 +01:00
26 changed files with 951 additions and 674 deletions

View File

@ -12,12 +12,11 @@ args_bin = [
"-domain localhost",
"-feed tmp/cpolis.atom",
"-firebase tmp/firebase.json",
"-gob tmp/cpolis.gob",
"-images tmp/pics",
"-img-width 256",
"-link https://distrikt-ni-st.de",
"-log tmp/cpolis.log",
"-pdfs tmp/pdfs",
"-pics tmp/pics",
"-port 8080",
"-title 'Freimaurer Distrikt Niedersachsen und Sachsen-Anhalt'",
"-web web",

View File

@ -6,7 +6,10 @@ import (
"fmt"
"log"
"os"
"path/filepath"
"time"
"github.com/google/uuid"
)
type Article struct {
@ -14,6 +17,7 @@ type Article struct {
Title string
BannerLink string
Summary string
UUID uuid.UUID
ID int64
CreatorID int64
IssueID int64
@ -31,8 +35,8 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
selectQuery := "SELECT id FROM issues WHERE published = false"
insertQuery := `
INSERT INTO articles
(title, banner_link, summary, published, rejected, creator_id, issue_id, edited_id, clicks, is_in_issue, auto_generated)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
(title, banner_link, summary, published, rejected, creator_id, issue_id, edited_id, clicks, is_in_issue, auto_generated, uuid)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`
for i := 0; i < TxMaxRetries; i++ {
@ -49,7 +53,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.Published, a.Rejected, a.CreatorID, id, a.EditedID, 0, 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, a.UUID.String())
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
@ -83,7 +87,7 @@ func (db *DB) AddArticle(a *Article) (int64, error) {
func (db *DB) GetArticle(id int64) (*Article, error) {
query := `
SELECT title, created, banner_link, summary, published, creator_id, issue_id, edited_id, clicks, is_in_issue, auto_generated
SELECT title, created, banner_link, summary, published, creator_id, issue_id, edited_id, clicks, is_in_issue, auto_generated, uuid
FROM articles
WHERE id = ?
`
@ -91,9 +95,10 @@ func (db *DB) GetArticle(id int64) (*Article, error) {
article := new(Article)
var created []byte
var uuidString string
var err error
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 {
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, &uuidString); err != nil {
return nil, fmt.Errorf("error scanning article row: %v", err)
}
@ -103,12 +108,42 @@ func (db *DB) GetArticle(id int64) (*Article, error) {
return nil, fmt.Errorf("error parsing created: %v", err)
}
article.UUID, err = uuid.Parse(uuidString)
if err != nil {
return nil, fmt.Errorf("error parsing uuid: %v", err)
}
return article, nil
}
func (db *DB) GetArticleByUUID(u uuid.UUID) (*Article, error) {
query := `
SELECT id, title, created, banner_link, summary, published, creator_id, issue_id, edited_id, clicks, is_in_issue, auto_generated
FROM articles
WHERE uuid = ?
`
row := db.QueryRow(query, u.String())
article := new(Article)
var created []byte
var err error
if err := row.Scan(&article.ID, &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)
}
article.UUID = u
article.Created, err = time.Parse("2006-01-02 15:04:05", string(created))
if err != nil {
return nil, fmt.Errorf("error parsing created: %v", err)
}
return article, nil
}
func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, error) {
query := fmt.Sprintf(`
SELECT id, title, created, banner_link, summary, creator_id, issue_id, clicks, 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, uuid
FROM articles
WHERE %s = ?
`, attribute)
@ -121,8 +156,9 @@ func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, erro
for rows.Next() {
article := new(Article)
var created []byte
var uuidString string
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 {
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, &uuidString); err != nil {
return nil, fmt.Errorf("error scanning article row: %v", err)
}
@ -131,6 +167,11 @@ func (db *DB) GetCertainArticles(attribute string, value bool) ([]*Article, erro
return nil, fmt.Errorf("error parsing created: %v", err)
}
article.UUID, err = uuid.Parse(uuidString)
if err != nil {
return nil, fmt.Errorf("error parsing uuid: %v", err)
}
articleList = append(articleList, article)
}
@ -142,7 +183,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, clicks, auto_generated
SELECT id, title, created, banner_link, summary, clicks, auto_generated, uuid
FROM articles
WHERE issue_id = ? AND published = true AND is_in_issue = true
`
@ -174,8 +215,9 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) {
for rows.Next() {
article := new(Article)
var created []byte
var uuidString string
if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.Clicks, &article.AutoGenerated); err != nil {
if err = rows.Scan(&article.ID, &article.Title, &created, &article.BannerLink, &article.Summary, &article.Clicks, &article.AutoGenerated, &uuidString); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
@ -190,6 +232,14 @@ func (db *DB) GetCurrentIssueArticles() ([]*Article, error) {
return nil, fmt.Errorf("error parsing created: %v", err)
}
article.UUID, err = uuid.Parse(uuidString)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return nil, fmt.Errorf("error parsing uuid: %v", err)
}
articleList = append(articleList, article)
}
@ -284,11 +334,11 @@ 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")
func WriteArticleToFile(c *Config, articleUUID uuid.UUID, content []byte) error {
articlePath := filepath.Join(c.ArticleDir, fmt.Sprint(articleUUID, ".md"))
if err := os.WriteFile(articleAbsName, content, 0644); err != nil {
return fmt.Errorf("error writing article %v to file: %v", articleID, err)
if err := os.WriteFile(articlePath, content, 0644); err != nil {
return fmt.Errorf("error writing article %v to file: %v", articleUUID, err)
}
return nil

View File

@ -1,6 +1,7 @@
package backend
import (
"errors"
"fmt"
"io"
"os"
@ -12,6 +13,9 @@ 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)
if feed.Subtitle == nil {
return nil, errors.New("feed subtitle was not created")
}
linkID := feed.AddLink(atom.NewLink(c.Link))
feed.Links[linkID].Rel = "self"
@ -33,16 +37,25 @@ func GenerateAtomFeed(c *Config, db *DB) (*string, error) {
entry := atom.NewEntry(articleTitle)
entry.ID = atom.NewID(fmt.Sprint("urn:entry:", article.ID))
entry.Published = atom.NewDate(article.Created)
entry.Content = atom.NewContent(atom.OutOfLine, "text/hmtl", fmt.Sprint(c.Domain, "/article/serve/", article.ID))
entry.Content = atom.NewContent(atom.OutOfLine, "text/html", fmt.Sprint(c.Domain, "/article/serve/", article.UUID))
if entry.Content == nil {
return nil, errors.New("entry content was not created")
}
if article.AutoGenerated {
entry.Summary = atom.NewText("text", "automatically generated")
if entry.Summary == nil {
return nil, errors.New("entry summary was not created")
}
} 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 entry.Summary == nil {
return nil, errors.New("entry summary was not created")
}
}
if len(article.BannerLink) > 0 {

View File

@ -14,21 +14,21 @@ import (
type Config struct {
AESKeyFile string
ArticleDir string
AtomFile string
ConfigFile string
DBName string
Description string
Domain string
AtomFile string
FirebaseKey string
GOBKeyFile string
ImgDir string
Link string
LogFile string
PDFDir string
PicsDir string
Port string
Title string
Version string
WebDir string
CookieExpiryHours int
MaxBannerHeight int
MaxBannerWidth int
MaxImgHeight int
@ -41,29 +41,26 @@ func newConfig() *Config {
ArticleDir: "/var/www/cpolis/articles",
AtomFile: "/var/www/cpolis/cpolis.atom",
ConfigFile: "/etc/cpolis/config.toml",
CookieExpiryHours: 24 * 30,
DBName: "cpolis",
FirebaseKey: "/var/www/cpolis/serviceAccountKey.json",
GOBKeyFile: "/var/www/cpolis/gob.key",
FirebaseKey: "/etc/cpolis/serviceAccountKey.json",
ImgDir: "/var/www/cpolis/images",
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",
Port: ":1664",
Version: "v0.15.4",
WebDir: "/var/www/cpolis/web",
}
}
func mkDir(path string, perm fs.FileMode) (string, error) {
var err error
name := filepath.Base(path)
stringSlice := strings.Split(path, "/")
name := stringSlice[len(stringSlice)-1]
path, err = filepath.Abs(path)
path, err := filepath.Abs(path)
if err != nil {
return "", fmt.Errorf("error finding absolute path for %v directory: %v", name, err)
}
@ -82,20 +79,20 @@ func mkFile(path string, filePerm, dirPerm fs.FileMode) (string, error) {
return "", fmt.Errorf("error finding absolute path for %v: %v", path, err)
}
stringSlice := strings.Split(path, "/")
_, err = os.Stat(path)
if os.IsNotExist(err) {
dir := strings.Join(stringSlice[:len(stringSlice)-1], "/")
dir := filepath.Dir(path)
if err = os.MkdirAll(dir, dirPerm); err != nil {
return "", fmt.Errorf("error creating %v: %v", dir, err)
}
fileName := stringSlice[len(stringSlice)-1]
file, err := os.Create(dir + "/" + fileName)
fileName := filepath.Base(path)
file, err := os.Create(filepath.Join(dir, fileName))
if err != nil {
return "", fmt.Errorf("error creating %v: %v", fileName, err)
}
defer file.Close()
if err = file.Chmod(filePerm); err != nil {
return "", fmt.Errorf("error setting permissions for %v: %v", fileName, err)
}
@ -116,13 +113,13 @@ func (c *Config) handleCliArgs() error {
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.GOBKeyFile, "gob", c.GOBKeyFile, "gob key file")
flag.StringVar(&c.ImgDir, "images", c.ImgDir, "images directory")
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.Title, "title", c.Title, "channel title")
flag.StringVar(&c.WebDir, "web", c.WebDir, "web directory")
flag.IntVar(&c.CookieExpiryHours, "cookie-expiry-hours", c.CookieExpiryHours, "cookies expire after this amount of hours")
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")
@ -158,6 +155,10 @@ func (c *Config) setupConfig(cliConfig *Config) error {
if cliConfig.AESKeyFile != defaultConfig.AESKeyFile {
c.AESKeyFile = cliConfig.AESKeyFile
}
c.AESKeyFile, err = filepath.Abs(c.AESKeyFile)
if err != nil {
return fmt.Errorf("error setting absolute filepath for AESKeyFile: %v", err)
}
c.AESKeyFile, err = mkFile(c.AESKeyFile, 0600, 0700)
if err != nil {
return fmt.Errorf("error setting up file: %v", err)
@ -166,11 +167,31 @@ func (c *Config) setupConfig(cliConfig *Config) error {
if cliConfig.ArticleDir != defaultConfig.ArticleDir {
c.ArticleDir = cliConfig.ArticleDir
}
c.ArticleDir, err = filepath.Abs(c.ArticleDir)
if err != nil {
return fmt.Errorf("error setting absolute filepath for ArticleDir: %v", err)
}
c.ArticleDir, err = mkDir(c.ArticleDir, 0700)
if err != nil {
return fmt.Errorf("error setting up directory: %v", err)
}
if cliConfig.AtomFile != defaultConfig.AtomFile {
c.AtomFile = cliConfig.AtomFile
}
c.AtomFile, err = filepath.Abs(c.AtomFile)
if err != nil {
return fmt.Errorf("error setting absolute filepath for AtomFile: %v", err)
}
c.AtomFile, err = mkFile(c.AtomFile, 0644, 0744)
if err != nil {
return fmt.Errorf("error setting up file: %v", err)
}
if cliConfig.CookieExpiryHours != defaultConfig.CookieExpiryHours {
c.CookieExpiryHours = cliConfig.CookieExpiryHours
}
if cliConfig.DBName != defaultConfig.DBName {
c.DBName = cliConfig.DBName
}
@ -182,33 +203,33 @@ func (c *Config) setupConfig(cliConfig *Config) error {
if cliConfig.Domain != defaultConfig.Domain {
c.Domain = cliConfig.Domain
}
domainStrings := strings.Split(c.Domain, "/")
if domainStrings[0] != "http:" && domainStrings[0] != "https:" {
domainStrings := strings.Split(c.Domain, ":")
if domainStrings[0] != "http" && domainStrings[0] != "https" {
c.Domain = "https://" + c.Domain
}
if cliConfig.AtomFile != defaultConfig.AtomFile {
c.AtomFile = cliConfig.AtomFile
}
c.AtomFile, err = mkFile(c.AtomFile, 0644, 0744)
if err != nil {
return fmt.Errorf("error setting up file: %v", err)
}
if cliConfig.FirebaseKey != defaultConfig.FirebaseKey {
c.FirebaseKey = cliConfig.FirebaseKey
}
c.FirebaseKey, err = filepath.Abs(c.FirebaseKey)
if err != nil {
return fmt.Errorf("error setting absolute filepath for FirebaseKey: %v", err)
}
c.FirebaseKey, err = mkFile(c.FirebaseKey, 0600, 0700)
if err != nil {
return fmt.Errorf("error setting up file: %v", err)
}
if cliConfig.GOBKeyFile != defaultConfig.GOBKeyFile {
c.GOBKeyFile = cliConfig.GOBKeyFile
if cliConfig.ImgDir != defaultConfig.ImgDir {
c.ImgDir = cliConfig.ImgDir
}
c.GOBKeyFile, err = mkFile(c.GOBKeyFile, 0600, 0700)
c.ImgDir, err = filepath.Abs(c.ImgDir)
if err != nil {
return fmt.Errorf("error setting up file: %v", err)
return fmt.Errorf("error setting absolute filepath for PicsDir: %v", err)
}
c.ImgDir, err = mkDir(c.ImgDir, 0700)
if err != nil {
return fmt.Errorf("error setting up directory: %v", err)
}
if cliConfig.Link != defaultConfig.Link {
@ -218,6 +239,10 @@ func (c *Config) setupConfig(cliConfig *Config) error {
if cliConfig.LogFile != defaultConfig.LogFile {
c.LogFile = cliConfig.LogFile
}
c.LogFile, err = filepath.Abs(c.LogFile)
if err != nil {
return fmt.Errorf("error setting absolute filepath for LogFile: %v", err)
}
c.LogFile, err = mkFile(c.LogFile, 0600, 0700)
if err != nil {
return fmt.Errorf("error setting up file: %v", err)
@ -242,15 +267,11 @@ func (c *Config) setupConfig(cliConfig *Config) error {
if cliConfig.PDFDir != defaultConfig.PDFDir {
c.PDFDir = cliConfig.PDFDir
}
c.PDFDir, err = mkDir(c.PDFDir, 0700)
c.PDFDir, err = filepath.Abs(c.PDFDir)
if err != nil {
return fmt.Errorf("error setting up directory: %v", err)
return fmt.Errorf("error setting absolute filepath for PDFDir: %v", err)
}
if cliConfig.PicsDir != defaultConfig.PicsDir {
c.PicsDir = cliConfig.PicsDir
}
c.PicsDir, err = mkDir(c.PicsDir, 0700)
c.PDFDir, err = mkDir(c.PDFDir, 0700)
if err != nil {
return fmt.Errorf("error setting up directory: %v", err)
}
@ -266,6 +287,10 @@ func (c *Config) setupConfig(cliConfig *Config) error {
if cliConfig.WebDir != defaultConfig.WebDir {
c.WebDir = cliConfig.WebDir
}
c.WebDir, err = filepath.Abs(c.WebDir)
if err != nil {
return fmt.Errorf("error setting absolute filepath for WebDir: %v", err)
}
c.WebDir, err = mkDir(c.WebDir, 0700)
if err != nil {
return fmt.Errorf("error setting up directory: %v", err)

57
cmd/backend/docx.go Normal file
View File

@ -0,0 +1,57 @@
package backend
import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"github.com/google/uuid"
)
func ConvertToMarkdown(c *Config, filename string) ([]byte, error) {
var stderr bytes.Buffer
tmpDir, err := os.MkdirTemp(os.TempDir(), "cpolis_images")
if err != nil {
return nil, fmt.Errorf("error creating temporary directory: %v", err)
}
defer os.RemoveAll(tmpDir)
articleFileName := filepath.Join(os.TempDir(), fmt.Sprint(uuid.New(), ".md"))
cmd := exec.Command("pandoc", "-s", "-f", "docx", "-t", "commonmark_x", "-o", articleFileName, "--extract-media", tmpDir, filename) // TODO: Is writing to a file necessary?
cmd.Stderr = &stderr
if err = cmd.Run(); err != nil {
return nil, fmt.Errorf("error converting docx to markdown: %v: %v", err, stderr.String())
}
defer os.Remove(articleFileName)
articleContent, err := os.ReadFile(articleFileName)
if err != nil {
return nil, fmt.Errorf("error reading markdown file: %v", err)
}
imageNames, err := filepath.Glob(filepath.Join(tmpDir, "media", "*"))
if err != nil {
return nil, fmt.Errorf("error getting docx images from temporary directory: %v", err)
}
for _, name := range imageNames {
image, err := os.Open(name)
if err != nil {
return nil, fmt.Errorf("error opening image file %v: %v", name, err)
}
defer image.Close()
newImageName, err := SaveImage(image, c.MaxImgHeight, c.MaxImgWidth, c.ImgDir)
if err != nil {
return nil, fmt.Errorf("error saving image %v: %v", name, err)
}
articleContent = regexp.MustCompile(name).ReplaceAll(articleContent, []byte(c.Domain+"/image/serve/"+newImageName))
}
return articleContent, nil
}

View File

@ -1,10 +1,14 @@
package backend
import (
"bufio"
"fmt"
"image"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/chai2010/webp"
"github.com/disintegration/imaging"
@ -13,6 +17,53 @@ import (
var ErrUnsupportedFormat error = image.ErrFormat // used internally by imaging
func checkImageUsage(c *Config, db *DB, name string) (bool, error) {
imageWasFound := false
if err := filepath.Walk(c.ArticleDir, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return fmt.Errorf("error walking articles filepath: %v", err)
}
if !info.IsDir() {
mdFile, err := os.Open(path)
if err != nil {
return fmt.Errorf("error opening article %v: %v", info.Name(), err)
}
defer mdFile.Close()
scanner := bufio.NewScanner(mdFile)
for scanner.Scan() {
if strings.Contains(scanner.Text(), name) {
imageWasFound = true
return nil
}
}
return scanner.Err()
}
return nil
}); err != nil {
return false, fmt.Errorf("error walking articles filepath: %v", err)
}
if !imageWasFound {
users, err := db.GetAllUsers(c)
if err != nil {
return false, fmt.Errorf("error getting all users: %v", err)
}
for _, user := range users {
if name == user.ProfilePicLink {
return true, nil
}
}
}
return imageWasFound, nil
}
func SaveImage(src io.Reader, maxHeight, maxWidth int, path string) (string, error) {
img, err := imaging.Decode(src, imaging.AutoOrientation(true))
if err != nil {
@ -30,7 +81,7 @@ func SaveImage(src io.Reader, maxHeight, maxWidth int, path string) (string, err
}
filename := fmt.Sprint(uuid.New(), ".webp")
file, err := os.Create(path + filename)
file, err := os.Create(filepath.Join(path, filename))
if err != nil {
return "", fmt.Errorf("error creating new image file: %v", err)
}
@ -42,3 +93,33 @@ func SaveImage(src io.Reader, maxHeight, maxWidth int, path string) (string, err
return filename, nil
}
func CleanUpImages(c *Config, db *DB) error {
if err := filepath.Walk(c.ImgDir, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return fmt.Errorf("error walking images filepath: %v", err)
}
if !info.IsDir() {
imageName := info.Name()
imagePath := path
imageWasFound, err := checkImageUsage(c, db, imageName)
if err != nil {
return fmt.Errorf("error checking image usage: %v", err)
}
if !imageWasFound {
if err = os.Remove(imagePath); err != nil {
return fmt.Errorf("error removing unused image: %v", err)
}
}
}
return nil
}); err != nil {
return fmt.Errorf("error cleaning up: %v", err)
}
return nil
}

View File

@ -1,63 +0,0 @@
package backend
import (
"crypto/rand"
"encoding/gob"
"fmt"
"io"
"os"
"github.com/gorilla/sessions"
)
type (
CookieStore struct{ sessions.CookieStore }
Session struct{ sessions.Session }
)
func NewKey() ([]byte, error) {
key := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, key); err != nil {
return nil, fmt.Errorf("error generating key: %v", err)
}
return key, nil
}
func SaveKey(key []byte, filename string) error {
file, err := os.Create(filename)
if err != nil {
return fmt.Errorf("error creating key file: %v", err)
}
defer file.Close()
file.Chmod(0600)
if err = gob.NewEncoder(file).Encode(key); err != nil {
return fmt.Errorf("error ecoding key: %v", err)
}
return nil
}
func LoadKey(filename string) ([]byte, error) {
file, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("error opening key file: %v", err)
}
defer file.Close()
key := make([]byte, 32)
if err = gob.NewDecoder(file).Decode(&key); err != nil {
return nil, fmt.Errorf("error decoding key: %v", err)
}
return key, nil
}
func NewCookieStore(key []byte) *CookieStore {
store := sessions.NewCookieStore(key)
store.Options.Secure = true
store.Options.HttpOnly = true
return &CookieStore{*store}
}

View File

@ -5,8 +5,9 @@ import (
"log"
"net/http"
"os"
"strconv"
"path/filepath"
"github.com/google/uuid"
b "streifling.com/jason/cpolis/cmd/backend"
)
@ -37,15 +38,15 @@ func ServeArticle(c *b.Config, db *b.DB) http.HandlerFunc {
return
}
idString := r.PathValue("id")
id, err := strconv.ParseInt(idString, 10, 64)
uuidString := r.PathValue("uuid")
uuid, err := uuid.Parse(uuidString)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
article, err := db.GetArticle(id)
article, err := db.GetArticleByUUID(uuid)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -56,8 +57,8 @@ func ServeArticle(c *b.Config, db *b.DB) http.HandlerFunc {
return
}
articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
contentBytes, err := os.ReadFile(articleAbsName)
articlePath := filepath.Join(c.ArticleDir, fmt.Sprint(article.UUID, ".md"))
contentBytes, err := os.ReadFile(articlePath)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -87,15 +88,14 @@ func ServeArticle(c *b.Config, db *b.DB) http.HandlerFunc {
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)
uuid, err := uuid.Parse(r.PathValue("uuid"))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
article, err := db.GetArticle(id)
article, err := db.GetArticleByUUID(uuid)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)

View File

@ -1,9 +1,7 @@
package calls
import (
"log"
"net/http"
"path/filepath"
b "streifling.com/jason/cpolis/cmd/backend"
)
@ -14,13 +12,6 @@ func ServeAtomFeed(c *b.Config) http.HandlerFunc {
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)
http.ServeFile(w, r, c.AtomFile)
}
}

View File

@ -1,7 +1,6 @@
package calls
import (
"log"
"net/http"
"path/filepath"
@ -9,21 +8,14 @@ import (
f "streifling.com/jason/cpolis/cmd/frontend"
)
func ServeImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
func ServeImage(c *b.Config, s map[string]*f.Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := f.GetSession(w, r, c, s); err != nil {
if !f.SessionIsActive(r, s) {
if !tokenIsVerified(w, r, c) {
return
}
}
absFilepath, err := filepath.Abs(c.PicsDir)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.ServeFile(w, r, absFilepath+"/"+r.PathValue("pic"))
http.ServeFile(w, r, filepath.Join(c.ImgDir, r.PathValue("pic")))
}
}

View File

@ -5,6 +5,7 @@ import (
"log"
"net/http"
"os"
"path/filepath"
b "streifling.com/jason/cpolis/cmd/backend"
)
@ -42,6 +43,6 @@ func ServePDF(c *b.Config) http.HandlerFunc {
return
}
http.ServeFile(w, r, c.PDFDir+"/"+r.PathValue("id"))
http.ServeFile(w, r, filepath.Join(c.PDFDir, r.PathValue("id")))
}
}

View File

@ -6,10 +6,12 @@ import (
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/google/uuid"
b "streifling.com/jason/cpolis/cmd/backend"
)
@ -45,33 +47,27 @@ type EditorHTMLData struct {
Contributors []*b.User
}
func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func WriteArticle(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := GetSession(w, r, c, s)
session, err := ManageSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
var data *EditorHTMLData
if session.Values["article"] == nil {
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
}
data := &EditorHTMLData{Action: "submit", Article: new(b.Article), ArticleUsers: make(map[string]*ArticleUser)}
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))
creator, err := db.GetUser(c, session.User.ID)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -88,7 +84,7 @@ func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html")
tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "editor.html"))
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -97,19 +93,11 @@ func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
}
func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func SubmitArticle(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := GetSession(w, r, c, s)
session, err := ManageSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session.Values["article"] = nil
if err = session.Save(r, w); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
@ -117,12 +105,13 @@ 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),
CreatorID: session.User.ID,
Published: false,
Rejected: false,
IsInIssue: r.PostFormValue("issue") == "on",
AutoGenerated: false,
EditedID: 0,
UUID: uuid.New(),
}
if len(article.Title) == 0 {
@ -178,7 +167,7 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
http.Error(w, "Bitte den Artikel eingeben.", http.StatusBadRequest)
return
}
if err := b.WriteArticleToFile(c, article.ID, content); err != nil {
if err := b.WriteArticleToFile(c, article.UUID, content); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -215,9 +204,9 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
data := new(struct{ Role int })
data.Role = session.Values["role"].(int)
data.Role = session.User.Role
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html"))
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
@ -227,23 +216,34 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
}
func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func ResubmitArticle(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := GetSession(w, r, c, s)
session, err := ManageSession(w, r, c, s)
if err != nil {
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
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",
article, err := db.GetArticle(id)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
article.Title = r.PostFormValue("article-title")
article.BannerLink = r.PostFormValue("article-banner-url")
article.Summary = r.PostFormValue("article-summary")
article.CreatorID = session.User.ID
article.IsInIssue = r.PostFormValue("issue") == "on"
if len(article.Title) == 0 {
http.Error(w, "Bitte den Titel eingeben.", http.StatusBadRequest)
return
@ -285,20 +285,13 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
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, "/", article.ID, ".md")
if err = os.WriteFile(contentLink, []byte(content), 0644); err != nil {
if err = b.WriteArticleToFile(c, article.UUID, []byte(content)); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -346,9 +339,9 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
data := new(struct{ Role int })
data.Role = session.Values["role"].(int)
data.Role = session.User.Role
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html"))
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
@ -358,11 +351,10 @@ 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 {
func ShowUnpublishedUnrejectedAndPublishedRejectedArticles(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
if _, err := ManageSession(w, r, c, s); err != nil {
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
@ -393,7 +385,7 @@ func ShowUnpublishedUnrejectedAndPublishedRejectedArticles(c *b.Config, db *b.DB
}
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/unpublished-articles.html")
tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "unpublished-articles.html"))
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", articles); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -402,12 +394,11 @@ func ShowUnpublishedUnrejectedAndPublishedRejectedArticles(c *b.Config, db *b.DB
}
}
func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func ShowRejectedArticles(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := GetSession(w, r, c, s)
session, err := ManageSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
@ -425,12 +416,12 @@ 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.CreatorID == session.Values["id"].(int64) {
if article.CreatorID == session.User.ID {
data.MyIDs[article.ID] = true
}
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/rejected-articles.html")
tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "rejected-articles.html"))
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
@ -440,12 +431,11 @@ func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerF
}
}
func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func ReviewRejectedArticle(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := GetSession(w, r, c, s)
session, err := ManageSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
@ -466,8 +456,8 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
data.Image = data.Article.BannerLink
articleAbsName := fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md")
content, err := os.ReadFile(articleAbsName)
articlePath := filepath.Join(c.ArticleDir, fmt.Sprint(data.Article.UUID, ".md"))
content, err := os.ReadFile(articlePath)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -513,7 +503,7 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
data.ArticleUsers[fmt.Sprint(contributor.LastName, contributor.FirstName, contributor.ID)].ArticleRole = Contributor
}
creator, err := db.GetUser(c, session.Values["id"].(int64))
creator, err := db.GetUser(c, session.User.ID)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -535,7 +525,7 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
data.Action = fmt.Sprint("resubmit/", data.Article.ID)
tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html")
tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "editor.html"))
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
@ -545,12 +535,11 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
}
}
func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func PublishArticle(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := GetSession(w, r, c, s)
session, err := ManageSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
@ -598,7 +587,7 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return
}
if err = os.Remove(fmt.Sprint(c.ArticleDir, "/", oldArticle.ID, ".md")); err != nil {
if err = os.Remove(filepath.Join(c.ArticleDir, fmt.Sprint(oldArticle.UUID, ".md"))); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -624,9 +613,9 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
data := new(struct{ Role int })
data.Role = session.Values["role"].(int)
data.Role = session.User.Role
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html"))
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
@ -636,12 +625,11 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
}
func RejectArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func RejectArticle(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := GetSession(w, r, c, s)
session, err := ManageSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
@ -659,9 +647,9 @@ func RejectArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
data := new(struct{ Role int })
data.Role = session.Values["role"].(int)
data.Role = session.User.Role
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html"))
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
@ -671,11 +659,10 @@ 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 {
func ShowCurrentIssue(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
if _, err := ManageSession(w, r, c, s); err != nil {
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
@ -686,7 +673,7 @@ func ShowCurrentIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/current-issue.html")
tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "current-issue.html"))
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", articles); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -695,11 +682,10 @@ 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 {
func ShowPublishedArticles(c *b.Config, db *b.DB, s map[string]*Session, action string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
if _, err := ManageSession(w, r, c, s); err != nil {
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
@ -722,7 +708,7 @@ func ShowPublishedArticles(c *b.Config, db *b.DB, s *b.CookieStore, action strin
}
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/published-articles.html")
tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "published-articles.html"))
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
@ -732,11 +718,10 @@ 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 {
func ReviewArticle(c *b.Config, db *b.DB, s map[string]*Session, action, title, button string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
if _, err := ManageSession(w, r, c, s); err != nil {
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
@ -780,8 +765,8 @@ func ReviewArticle(c *b.Config, db *b.DB, s *b.CookieStore, action, title, butto
return
}
articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
content, err := os.ReadFile(articleAbsName)
articlePath := filepath.Join(c.ArticleDir, fmt.Sprint(article.UUID, ".md"))
content, err := os.ReadFile(articlePath)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -818,7 +803,7 @@ func ReviewArticle(c *b.Config, db *b.DB, s *b.CookieStore, action, title, butto
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/review-article.html")
tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "review-article.html"))
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -827,16 +812,22 @@ 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 {
func DeleteArticle(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := GetSession(w, r, c, s)
session, err := ManageSession(w, r, c, s)
if err != nil {
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
article, err := db.GetArticle(id)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -849,12 +840,19 @@ func DeleteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return
}
if err = os.Remove(fmt.Sprint(c.ArticleDir, "/", id, ".md")); err != nil {
if err = os.Remove(filepath.Join(c.ArticleDir, fmt.Sprint(article.UUID, ".md"))); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
go func(c *b.Config, db *b.DB) {
if err = b.CleanUpImages(c, db); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}(c, db)
feed, err := b.GenerateAtomFeed(c, db)
if err != nil {
log.Println(err)
@ -868,9 +866,9 @@ func DeleteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
data := new(struct{ Role int })
data.Role = session.Values["role"].(int)
data.Role = session.User.Role
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html"))
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
@ -880,12 +878,11 @@ 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 {
func AllowEditArticle(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := GetSession(w, r, c, s)
session, err := ManageSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
@ -921,8 +918,8 @@ func AllowEditArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc
return
}
src := fmt.Sprint(c.ArticleDir, "/", oldArticle.ID, ".md")
dst := fmt.Sprint(c.ArticleDir, "/", newArticle.ID, ".md")
src := filepath.Join(c.ArticleDir, fmt.Sprint(oldArticle.UUID, ".md"))
dst := filepath.Join(c.ArticleDir, fmt.Sprint(newArticle.UUID, ".md"))
if err = b.CopyFile(src, dst); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -962,9 +959,9 @@ func AllowEditArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc
}
data := new(struct{ Role int })
data.Role = session.Values["role"].(int)
data.Role = session.User.Role
tmpl := template.Must(template.ParseFiles(c.WebDir + "/templates/hub.html"))
tmpl := template.Must(template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html")))
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -973,11 +970,10 @@ 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 {
func EditArticle(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
if _, err := ManageSession(w, r, c, s); err != nil {
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
@ -999,7 +995,7 @@ func EditArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
data.Image = data.Article.BannerLink
content, err := os.ReadFile(fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md"))
content, err := os.ReadFile(filepath.Join(c.ArticleDir, fmt.Sprint(data.Article.UUID, ".md")))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -1027,7 +1023,7 @@ func EditArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
data.Action = fmt.Sprint("save/", data.Article.ID)
tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html")
tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "editor.html"))
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)

93
cmd/frontend/docx.go Normal file
View File

@ -0,0 +1,93 @@
package frontend
import (
"bytes"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"time"
"github.com/gabriel-vasile/mimetype"
"github.com/google/uuid"
b "streifling.com/jason/cpolis/cmd/backend"
)
func UploadDocx(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := ManageSession(w, r, c, s)
if err != nil {
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
file, fileHeader, err := r.FormFile("docx-upload")
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer file.Close()
var buf bytes.Buffer
if _, err = io.Copy(&buf, file); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
mime := mimetype.Detect(buf.Bytes())
if !mime.Is("application/vnd.openxmlformats-officedocument.wordprocessingml.document") {
http.Error(w, "Die Datei ist kein DOCX Worddokument.", http.StatusBadRequest)
return
}
docxFilepath := filepath.Join(os.TempDir(), fmt.Sprint(uuid.New(), ".docx"))
if err = os.WriteFile(docxFilepath, buf.Bytes(), 0644); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer os.Remove(docxFilepath)
mdString, err := b.ConvertToMarkdown(c, docxFilepath)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
uuidName := uuid.New()
mdFilepath := filepath.Join(c.ArticleDir, fmt.Sprint(uuidName, ".md"))
if err = os.WriteFile(mdFilepath, mdString, 0644); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
article := &b.Article{
Created: time.Now(),
UUID: uuidName,
CreatorID: session.User.ID,
Rejected: true,
}
article.Title = fmt.Sprint(fileHeader.Filename, "-", article.UUID)
id, err := db.AddArticle(article)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = db.WriteArticleAuthors(id, []int64{session.User.ID}); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
}

100
cmd/frontend/homepage.go Normal file
View File

@ -0,0 +1,100 @@
package frontend
import (
"html/template"
"log"
"net/http"
"path/filepath"
"time"
b "streifling.com/jason/cpolis/cmd/backend"
)
func HomePage(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
numRows, err := db.CountEntries("users")
if err != nil {
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] = filepath.Join(c.WebDir, "templates", "index.html")
if numRows == 0 {
data.Role = b.NonExistent
data.Title = "Erster Benutzer (Administrator)"
data.ButtonText = "Anlegen"
data.URL = "/user/add-first"
files[1] = filepath.Join(c.WebDir, "templates", "edit-user.html")
tmpl, err := template.ParseFiles(files...)
if err = template.Must(tmpl, err).Execute(w, data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
} else {
cookie, err := r.Cookie("cpolis_session")
if err != nil {
files[1] = filepath.Join(c.WebDir, "templates", "login.html")
tmpl, err := template.ParseFiles(files...)
if err = template.Must(tmpl, err).Execute(w, data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
return
}
session, ok := s[cookie.Value]
if !ok {
cookie.Expires = time.Now()
http.SetCookie(w, cookie)
files[1] = filepath.Join(c.WebDir, "templates", "login.html")
tmpl, err := template.ParseFiles(files...)
if err = template.Must(tmpl, err).Execute(w, data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
return
}
data.Role = session.User.Role
files[1] = filepath.Join(c.WebDir, "templates", "hub.html")
tmpl, err := template.ParseFiles(files...)
if err = template.Must(tmpl, err).Execute(w, data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
}
func ShowHub(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := ManageSession(w, r, c, s)
if err != nil {
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
data := new(struct{ Role int })
data.Role = session.User.Role
tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.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
}
}
}

View File

@ -5,15 +5,15 @@ import (
"html/template"
"log"
"net/http"
"path/filepath"
b "streifling.com/jason/cpolis/cmd/backend"
)
func UploadEasyMDEImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
func UploadEasyMDEImage(c *b.Config, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
if _, err := ManageSession(w, r, c, s); err != nil {
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
@ -25,7 +25,7 @@ func UploadEasyMDEImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
}
defer file.Close()
filename, err := b.SaveImage(file, c.MaxImgHeight, c.MaxImgWidth, c.PicsDir+"/")
filename, err := b.SaveImage(file, c.MaxImgHeight, c.MaxImgWidth, c.ImgDir)
if err != nil {
if err == b.ErrUnsupportedFormat {
http.Error(w, "Das Dateiformat wird nicht unterstützt.", http.StatusBadRequest)
@ -42,11 +42,10 @@ func UploadEasyMDEImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
}
}
func UploadImage(c *b.Config, s *b.CookieStore, fileKey, htmlFile, htmlTemplate string) http.HandlerFunc {
func UploadImage(c *b.Config, s map[string]*Session, fileKey, htmlFile, htmlTemplate string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
if _, err := ManageSession(w, r, c, s); err != nil {
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
@ -58,7 +57,7 @@ func UploadImage(c *b.Config, s *b.CookieStore, fileKey, htmlFile, htmlTemplate
}
defer file.Close()
filename, err := b.SaveImage(file, c.MaxBannerHeight, c.MaxBannerWidth, c.PicsDir+"/")
filename, err := b.SaveImage(file, c.MaxBannerHeight, c.MaxBannerWidth, c.ImgDir)
if err != nil {
if err == b.ErrUnsupportedFormat {
http.Error(w, "Das Dateiformat wird nicht unterstützt.", http.StatusBadRequest)
@ -72,7 +71,7 @@ func UploadImage(c *b.Config, s *b.CookieStore, fileKey, htmlFile, htmlTemplate
data := new(struct{ Image string })
data.Image = filename
tmpl, err := template.ParseFiles(c.WebDir + "/templates/" + htmlFile)
tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", htmlFile))
if err = template.Must(tmpl, err).ExecuteTemplate(w, htmlTemplate, data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)

View File

@ -6,24 +6,18 @@ import (
"log"
"net/http"
"os"
"path/filepath"
"time"
"github.com/google/uuid"
b "streifling.com/jason/cpolis/cmd/backend"
)
func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func PublishLatestIssue(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := GetSession(w, r, c, s)
session, err := ManageSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session.Values["article"] = nil
if err = session.Save(r, w); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
@ -34,6 +28,7 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
Rejected: false,
Created: time.Now(),
AutoGenerated: true,
UUID: uuid.New(),
}
if len(article.Title) == 0 {
@ -49,14 +44,7 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
}
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
}
authorIDs[0] = session.User.ID
if err = db.WriteArticleAuthors(article.ID, authorIDs); err != nil {
log.Println(err)
@ -70,8 +58,8 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
return
}
articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
if err = os.WriteFile(articleAbsName, content, 0644); err != nil {
articlePath := filepath.Join(c.ArticleDir, fmt.Sprint(article.UUID, ".md"))
if err = os.WriteFile(articlePath, content, 0644); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -101,17 +89,10 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
return
}
session.Values["issue-image"] = nil
if err = session.Save(r, w); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data := new(struct{ Role int })
data.Role = session.Values["role"].(int)
data.Role = session.User.Role
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html"))
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)

View File

@ -5,20 +5,20 @@ import (
"log"
"net/http"
"path/filepath"
"strings"
"github.com/google/uuid"
b "streifling.com/jason/cpolis/cmd/backend"
)
func UploadPDF(c *b.Config, s *b.CookieStore) http.HandlerFunc {
func UploadPDF(c *b.Config, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
if _, err := ManageSession(w, r, c, s); err != nil {
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
file, _, err := r.FormFile("pdf-upload")
file, header, err := r.FormFile("pdf-upload")
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -44,15 +44,11 @@ func UploadPDF(c *b.Config, s *b.CookieStore) http.HandlerFunc {
return
}
filename := fmt.Sprint(uuid.New(), ".pdf")
absFilepath, err := filepath.Abs(c.PDFDir + "/" + filename)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
oldFilename := strings.Join(strings.Split(header.Filename, ".")[:len(header.Filename)-1], ".")
filename := fmt.Sprint(oldFilename, ".", uuid.New(), ".pdf")
filepath := filepath.Join(c.PDFDir, filename)
if err = b.WriteFile(absFilepath, file); err != nil {
if err = b.WriteFile(filepath, file); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return

View File

@ -1,115 +1,115 @@
package frontend
import (
"context"
"errors"
"fmt"
"html/template"
"log"
"net/http"
"path/filepath"
"time"
"github.com/google/uuid"
b "streifling.com/jason/cpolis/cmd/backend"
)
func saveSession(w http.ResponseWriter, r *http.Request, s *b.CookieStore, u *b.User) error {
session, err := s.Get(r, "cookie")
if err != nil {
return fmt.Errorf("error getting session: %v", err)
}
session.Values["authenticated"] = true
session.Values["id"] = u.ID
session.Values["name"] = u.FirstName + u.LastName
session.Values["role"] = u.Role
if err := session.Save(r, w); err != nil {
return fmt.Errorf("error saving session: %v", err)
}
return nil
type Session struct {
ctx context.Context
cancel context.CancelFunc
cookie *http.Cookie
User *b.User
}
// 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")
func newSession(w http.ResponseWriter, c *b.Config, sessionExpiryChan chan<- string, user *b.User) *Session {
sessionID := uuid.New().String()
expires := time.Now().Add(time.Hour * time.Duration(c.CookieExpiryHours))
ctx, cancel := context.WithDeadline(context.Background(), expires)
tmpSession, err := s.Get(r, "cookie")
session := &Session{
ctx: ctx,
cancel: cancel,
cookie: &http.Cookie{
Name: "cpolis_session",
Value: sessionID,
Expires: expires,
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
},
User: user,
}
go func() {
<-session.ctx.Done()
sessionExpiryChan <- session.cookie.Value
session.cookie.Expires = time.Now()
http.SetCookie(w, session.cookie)
}()
return session
}
func StartSessions() (map[string]*Session, chan string) {
sessions := make(map[string]*Session)
sessionExpiryChan := make(chan string)
go func() {
for sessionID := range sessionExpiryChan {
delete(sessions, sessionID)
}
}()
return sessions, sessionExpiryChan
}
// SessionIsActive is used for verifying that the user is logged in and returns
// a bool.
func SessionIsActive(r *http.Request, s map[string]*Session) bool {
cookie, err := r.Cookie("cpolis_session")
if err != nil {
if err = template.Must(tmpl, tmplErr).ExecuteTemplate(w, "page-content", msg); err != nil {
return nil, fmt.Errorf("error executing template: %v", err)
}
return nil, fmt.Errorf("error getting session: %v", err)
return false
}
session := &b.Session{Session: *tmpSession}
if session.IsNew {
if err = template.Must(tmpl, tmplErr).ExecuteTemplate(w, "page-content", msg); err != nil {
_, ok := s[cookie.Value]
return ok
}
// ManageSession is used for verifying that the user is logged in and returns
// their session and an error. It also handles cases where the user is not
// logged in.
func ManageSession(w http.ResponseWriter, r *http.Request, c *b.Config, s map[string]*Session) (*Session, error) {
tmpl, tmplErr := template.ParseFiles(filepath.Join(c.WebDir, "templates", "index.html"), filepath.Join(c.WebDir, "templates", "login.html"))
cookie, err := r.Cookie("cpolis_session")
if err != nil {
if err = template.Must(tmpl, tmplErr).ExecuteTemplate(w, "page-content", nil); err != nil {
return nil, fmt.Errorf("error executing template: %v", err)
}
return nil, errors.New("no cookie set")
}
session, ok := s[cookie.Value]
if !ok {
cookie.Expires = time.Now()
http.SetCookie(w, cookie)
if err = template.Must(tmpl, tmplErr).ExecuteTemplate(w, "page-content", nil); err != nil {
return nil, fmt.Errorf("error executing template: %v", err)
}
return nil, errors.New("session does not exist")
}
session.cookie.Expires = time.Now().Add(time.Hour * time.Duration(c.CookieExpiryHours))
http.SetCookie(w, session.cookie)
return session, nil
}
func HomePage(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
numRows, err := db.CountEntries("users")
if err != nil {
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 {
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, data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
} else {
session, err := s.Get(r, "cookie")
if err != nil {
log.Println(err)
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, 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, data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
}
}
func Login(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func Login(c *b.Config, db *b.DB, s map[string]*Session, sessionExpiryChan chan string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userName := r.PostFormValue("username")
password := r.PostFormValue("password")
@ -133,13 +133,11 @@ func Login(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return
}
if err := saveSession(w, r, s, user); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session := newSession(w, c, sessionExpiryChan, user)
s[session.cookie.Value] = session
http.SetCookie(w, session.cookie)
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html"))
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", user); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -148,52 +146,32 @@ func Login(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
}
func Logout(c *b.Config, s *b.CookieStore) http.HandlerFunc {
func Logout(c *b.Config, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := GetSession(w, r, c, s)
tmpl, tmplErr := template.ParseFiles(filepath.Join(c.WebDir, "templates", "login.html"))
cookie, err := r.Cookie("cpolis_session")
if err != nil {
if err = template.Must(tmpl, tmplErr).ExecuteTemplate(w, "page-content", nil); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
cookie.Expires = time.Now()
http.SetCookie(w, cookie)
session.Options.MaxAge = -1
if err = session.Save(r, w); err != nil {
session, ok := s[cookie.Value]
if !ok {
if err = template.Must(tmpl, tmplErr).ExecuteTemplate(w, "page-content", nil); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
session.cancel()
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
func ShowHub(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := GetSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session.Values["article"] = nil
if err = session.Save(r, w); 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")
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil {
if err = template.Must(tmpl, tmplErr).ExecuteTemplate(w, "page-content", nil); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return

View File

@ -4,19 +4,19 @@ import (
"html/template"
"log"
"net/http"
"path/filepath"
b "streifling.com/jason/cpolis/cmd/backend"
)
func CreateTag(c *b.Config, s *b.CookieStore) http.HandlerFunc {
func CreateTag(c *b.Config, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
if _, err := ManageSession(w, r, c, s); err != nil {
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-tag.html")
tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "add-tag.html"))
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -25,12 +25,11 @@ func CreateTag(c *b.Config, s *b.CookieStore) http.HandlerFunc {
}
}
func AddTag(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func AddTag(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := GetSession(w, r, c, s)
session, err := ManageSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
@ -42,9 +41,9 @@ 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)
data.Role = session.User.Role
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html"))
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)

View File

@ -5,6 +5,7 @@ import (
"html/template"
"log"
"net/http"
"path/filepath"
"sort"
"strconv"
@ -43,11 +44,10 @@ func sortUsersByName(users []*b.User) {
})
}
func CreateUser(c *b.Config, s *b.CookieStore) http.HandlerFunc {
func CreateUser(c *b.Config, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
if _, err := ManageSession(w, r, c, s); err != nil {
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
@ -58,7 +58,7 @@ func CreateUser(c *b.Config, s *b.CookieStore) http.HandlerFunc {
URL: "/user/add",
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html")
tmpl, err := template.ParseFiles(filepath.Join(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)
@ -67,12 +67,11 @@ func CreateUser(c *b.Config, s *b.CookieStore) http.HandlerFunc {
}
}
func AddUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func AddUser(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := GetSession(w, r, c, s)
session, err := ManageSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
@ -134,9 +133,9 @@ func AddUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
data := new(struct{ Role int })
data.Role = session.Values["role"].(int)
data.Role = session.User.Role
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html"))
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
@ -146,16 +145,15 @@ func AddUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
}
func EditSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func EditSelf(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := GetSession(w, r, c, s)
session, err := ManageSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
user, err := db.GetUser(c, session.Values["id"].(int64))
user, err := db.GetUser(c, session.User.ID)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -170,7 +168,7 @@ func EditSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
Image: user.ProfilePicLink,
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html")
tmpl, err := template.ParseFiles(filepath.Join(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)
@ -179,17 +177,16 @@ func EditSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
}
func UpdateSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func UpdateSelf(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := GetSession(w, r, c, s)
session, err := ManageSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
user := &b.User{
ID: session.Values["id"].(int64),
ID: session.User.ID,
UserName: r.PostFormValue("username"),
FirstName: r.PostFormValue("first-name"),
LastName: r.PostFormValue("last-name"),
@ -244,9 +241,9 @@ func UpdateSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
data := new(struct{ Role int })
data.Role = session.Values["role"].(int)
data.Role = session.User.Role
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html"))
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
@ -256,7 +253,7 @@ func UpdateSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
}
func AddFirstUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func AddFirstUser(c *b.Config, db *b.DB, s map[string]*Session, sessionExpiryChan chan string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var err error
user := &b.User{
@ -303,22 +300,20 @@ func AddFirstUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return
}
if err := saveSession(w, r, s, user); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if _, err := db.AddIssue(); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data := new(struct{ Role int })
data.Role = 0
session := newSession(w, c, sessionExpiryChan, user)
s[session.cookie.Value] = session
http.SetCookie(w, session.cookie)
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
data := new(struct{ Role int })
data.Role = user.Role
tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html"))
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -327,12 +322,11 @@ func AddFirstUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
}
func ShowAllUsers(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.HandlerFunc {
func ShowAllUsers(c *b.Config, db *b.DB, s map[string]*Session, action string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := GetSession(w, r, c, s)
session, err := ManageSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
@ -348,9 +342,9 @@ func ShowAllUsers(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.H
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
delete(data.Users, session.Values["id"].(int64))
delete(data.Users, session.User.ID)
tmpl, err := template.ParseFiles(c.WebDir + "/templates/show-all-users.html")
tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "show-all-users.html"))
if err = template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -359,11 +353,10 @@ 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 {
func EditUser(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := GetSession(w, r, c, s); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
if _, err := ManageSession(w, r, c, s); err != nil {
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
@ -389,7 +382,7 @@ func EditUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
Image: user.ProfilePicLink,
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html")
tmpl, err := template.ParseFiles(filepath.Join(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)
@ -398,12 +391,11 @@ func EditUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
}
func UpdateUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func UpdateUser(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := GetSession(w, r, c, s)
session, err := ManageSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
@ -473,9 +465,9 @@ func UpdateUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
data := new(struct{ Role int })
data.Role = session.Values["role"].(int)
data.Role = session.User.Role
tmpl := template.Must(template.ParseFiles(c.WebDir + "/templates/hub.html"))
tmpl := template.Must(template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html")))
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -484,12 +476,11 @@ func UpdateUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
}
func DeleteUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func DeleteUser(c *b.Config, db *b.DB, s map[string]*Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := GetSession(w, r, c, s)
session, err := ManageSession(w, r, c, s)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
http.Error(w, "Die Session ist abgelaufen. Bitte erneut anmelden.", http.StatusUnauthorized)
return
}
@ -507,9 +498,9 @@ func DeleteUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
}
data := new(struct{ Role int })
data.Role = session.Values["role"].(int)
data.Role = session.User.Role
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl, err := template.ParseFiles(filepath.Join(c.WebDir, "templates", "hub.html"))
tmpl = template.Must(tmpl, err)
if err = tmpl.ExecuteTemplate(w, "page-content", data); err != nil {
log.Println(err)

View File

@ -1,20 +1,16 @@
package main
import (
"encoding/gob"
"log"
"net/http"
"os"
"time"
b "streifling.com/jason/cpolis/cmd/backend"
c "streifling.com/jason/cpolis/cmd/calls"
f "streifling.com/jason/cpolis/cmd/frontend"
)
func init() {
gob.Register(b.User{})
}
func main() {
config, err := b.HandleConfig()
if err != nil {
@ -34,68 +30,69 @@ func main() {
}
defer db.Close()
key, err := b.LoadKey(config.GOBKeyFile)
if err != nil {
key, err = b.NewKey()
if err != nil {
log.Fatalln(err)
sessions, sessionExpiryChan := f.StartSessions()
defer close(sessionExpiryChan)
go func(c *b.Config, db *b.DB) {
for {
if err = b.CleanUpImages(c, db); err != nil {
log.Println(err)
}
if err = b.SaveKey(key, config.GOBKeyFile); err != nil {
log.Fatalln(err)
time.Sleep(time.Hour * 24)
}
}
store := b.NewCookieStore(key)
}(config, db)
mux := http.NewServeMux()
mux.Handle("/web/static/", http.StripPrefix("/web/static/",
http.FileServer(http.Dir(config.WebDir+"/static/"))))
mux.HandleFunc("/", f.HomePage(config, db, store))
mux.HandleFunc("/", f.HomePage(config, db, sessions))
mux.HandleFunc("GET /article/allow-edit/{id}", f.AllowEditArticle(config, db, store))
mux.HandleFunc("GET /article/all-published/review-edit", f.ShowPublishedArticles(config, db, store, "review-edit"))
mux.HandleFunc("GET /article/all-published/delete", f.ShowPublishedArticles(config, db, store, "review-delete"))
mux.HandleFunc("GET /article/all-rejected", f.ShowRejectedArticles(config, db, store))
mux.HandleFunc("GET /article/all-unpublished-unrejected-and-published-rejected", f.ShowUnpublishedUnrejectedAndPublishedRejectedArticles(config, db, store))
mux.HandleFunc("GET /article/delete/{id}", f.DeleteArticle(config, db, store))
mux.HandleFunc("GET /article/edit/{id}", f.EditArticle(config, db, store))
mux.HandleFunc("GET /article/publish/{id}", f.PublishArticle(config, db, store))
mux.HandleFunc("GET /article/reject/{id}", f.RejectArticle(config, db, store))
mux.HandleFunc("GET /article/review-delete/{id}", f.ReviewArticle(config, db, store, "delete", "Artikel löschen", "Löschen"))
mux.HandleFunc("GET /article/review-edit/{id}", f.ReviewArticle(config, db, store, "allow-edit", "Artikel bearbeiten", "Bearbeiten erlauben"))
mux.HandleFunc("GET /article/review-rejected/{id}", f.ReviewRejectedArticle(config, db, store))
mux.HandleFunc("GET /article/review-unpublished/{id}", f.ReviewArticle(config, db, store, "publish", "Artikel veröffentlichen", "Veröffentlichen"))
mux.HandleFunc("GET /article/serve/{id}", c.ServeArticle(config, db))
mux.HandleFunc("GET /article/serve/{id}/clicks", c.ServeClicks(db))
mux.HandleFunc("GET /article/write", f.WriteArticle(config, db, store))
mux.HandleFunc("GET /article/allow-edit/{id}", f.AllowEditArticle(config, db, sessions))
mux.HandleFunc("GET /article/all-published/review-edit", f.ShowPublishedArticles(config, db, sessions, "review-edit"))
mux.HandleFunc("GET /article/all-published/delete", f.ShowPublishedArticles(config, db, sessions, "review-delete"))
mux.HandleFunc("GET /article/all-rejected", f.ShowRejectedArticles(config, db, sessions))
mux.HandleFunc("GET /article/all-unpublished-unrejected-and-published-rejected", f.ShowUnpublishedUnrejectedAndPublishedRejectedArticles(config, db, sessions))
mux.HandleFunc("GET /article/delete/{id}", f.DeleteArticle(config, db, sessions))
mux.HandleFunc("GET /article/edit/{id}", f.EditArticle(config, db, sessions))
mux.HandleFunc("GET /article/publish/{id}", f.PublishArticle(config, db, sessions))
mux.HandleFunc("GET /article/reject/{id}", f.RejectArticle(config, db, sessions))
mux.HandleFunc("GET /article/review-delete/{id}", f.ReviewArticle(config, db, sessions, "delete", "Artikel löschen", "Löschen"))
mux.HandleFunc("GET /article/review-edit/{id}", f.ReviewArticle(config, db, sessions, "allow-edit", "Artikel bearbeiten", "Bearbeiten erlauben"))
mux.HandleFunc("GET /article/review-rejected/{id}", f.ReviewRejectedArticle(config, db, sessions))
mux.HandleFunc("GET /article/review-unpublished/{id}", f.ReviewArticle(config, db, sessions, "publish", "Artikel veröffentlichen", "Veröffentlichen"))
mux.HandleFunc("GET /article/serve/{uuid}", c.ServeArticle(config, db))
mux.HandleFunc("GET /article/serve/{uuid}/clicks", c.ServeClicks(db))
mux.HandleFunc("GET /article/write", f.WriteArticle(config, db, sessions))
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 /hub", f.ShowHub(config, db, sessions))
mux.HandleFunc("GET /image/serve/{pic}", c.ServeImage(config, sessions))
mux.HandleFunc("GET /issue/this", f.ShowCurrentIssue(config, db, sessions))
mux.HandleFunc("GET /logout", f.Logout(config, sessions))
mux.HandleFunc("GET /pdf/get-list", c.ServePDFList(config))
mux.HandleFunc("GET /pdf/serve/{id}", c.ServePDF(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))
mux.HandleFunc("GET /user/edit/{id}", f.EditUser(config, db, store))
mux.HandleFunc("GET /user/edit/self", f.EditSelf(config, db, store))
mux.HandleFunc("GET /user/show-all/delete", f.ShowAllUsers(config, db, store, "delete"))
mux.HandleFunc("GET /user/show-all/edit", f.ShowAllUsers(config, db, store, "edit"))
mux.HandleFunc("GET /tag/create", f.CreateTag(config, sessions))
mux.HandleFunc("GET /user/create", f.CreateUser(config, sessions))
mux.HandleFunc("GET /user/delete/{id}", f.DeleteUser(config, db, sessions))
mux.HandleFunc("GET /user/edit/{id}", f.EditUser(config, db, sessions))
mux.HandleFunc("GET /user/edit/self", f.EditSelf(config, db, sessions))
mux.HandleFunc("GET /user/show-all/delete", f.ShowAllUsers(config, db, sessions, "delete"))
mux.HandleFunc("GET /user/show-all/edit", f.ShowAllUsers(config, db, sessions, "edit"))
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.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.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))
mux.HandleFunc("POST /user/add", f.AddUser(config, db, store))
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"))
mux.HandleFunc("POST /article/resubmit/{id}", f.ResubmitArticle(config, db, sessions))
mux.HandleFunc("POST /article/submit", f.SubmitArticle(config, db, sessions))
mux.HandleFunc("POST /article/upload-banner", f.UploadImage(config, sessions, "article-banner", "editor.html", "article-banner-template"))
mux.HandleFunc("POST /article/upload-image", f.UploadEasyMDEImage(config, sessions))
mux.HandleFunc("POST /docx/upload", f.UploadDocx(config, db, sessions))
mux.HandleFunc("POST /issue/publish", f.PublishLatestIssue(config, db, sessions))
mux.HandleFunc("POST /issue/upload-banner", f.UploadImage(config, sessions, "issue-banner", "current-issue.html", "issue-banner-template"))
mux.HandleFunc("POST /login", f.Login(config, db, sessions, sessionExpiryChan))
mux.HandleFunc("POST /pdf/upload", f.UploadPDF(config, sessions))
mux.HandleFunc("POST /tag/add", f.AddTag(config, db, sessions))
mux.HandleFunc("POST /user/add", f.AddUser(config, db, sessions))
mux.HandleFunc("POST /user/add-first", f.AddFirstUser(config, db, sessions, sessionExpiryChan))
mux.HandleFunc("POST /user/update/{id}", f.UpdateUser(config, db, sessions))
mux.HandleFunc("POST /user/update/self", f.UpdateSelf(config, db, sessions))
mux.HandleFunc("POST /user/upload-profile-pic", f.UploadImage(config, sessions, "upload-profile-pic", "edit-user.html", "profile-pic-template"))
log.Fatalln(http.ListenAndServe(config.Port, mux))
}

View File

@ -38,6 +38,7 @@ CREATE TABLE articles (
clicks INT NOT NULL,
is_in_issue BOOL NOT NULL,
auto_generated BOOL NOT NULL,
uuid VARCHAR(36) NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (creator_id) REFERENCES users (id),
FOREIGN KEY (issue_id) REFERENCES issues (id)

39
go.mod
View File

@ -4,18 +4,18 @@ go 1.23.2
require (
firebase.google.com/go/v4 v4.15.1
git.streifling.com/jason/atom v1.0.0
git.streifling.com/jason/atom v1.0.1
github.com/BurntSushi/toml v1.4.0
github.com/chai2010/webp v1.1.1
github.com/disintegration/imaging v1.6.2
github.com/gabriel-vasile/mimetype v1.4.8
github.com/go-sql-driver/mysql v1.8.1
github.com/google/uuid v1.6.0
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.32.0
golang.org/x/term v0.28.0
google.golang.org/api v0.216.0
google.golang.org/api v0.218.0
)
require (
@ -27,7 +27,7 @@ require (
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/monitoring v1.23.0 // 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.25.0 // indirect
@ -36,9 +36,9 @@ require (
github.com/MicahParks/keyfunc v1.9.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 // indirect
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.32.3 // indirect
github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.2.1 // 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
@ -49,17 +49,16 @@ require (
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // 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.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
go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
go.opentelemetry.io/otel v1.34.0 // indirect
go.opentelemetry.io/otel/metric v1.34.0 // indirect
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect
go.opentelemetry.io/otel/trace v1.34.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
@ -68,9 +67,9 @@ require (
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-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
google.golang.org/genproto v0.0.0-20250124145028-65684f501c47 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250124145028-65684f501c47 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47 // indirect
google.golang.org/grpc v1.70.0 // indirect
google.golang.org/protobuf v1.36.4 // indirect
)

80
go.sum
View File

@ -16,8 +16,8 @@ cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXH
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/monitoring v1.23.0 h1:M3nXww2gn9oZ/qWN2bZ35CjolnVHM3qnSbu6srCPgjk=
cloud.google.com/go/monitoring v1.23.0/go.mod h1:034NnlQPDzrQ64G2Gavhl0LUHZs9H3rRmhtnp7jiJgg=
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=
@ -26,8 +26,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
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/atom v1.0.1 h1:G1PtNt1+qlzxpwjlD6iDeseFmzoac1IYxdq9twofTFY=
git.streifling.com/jason/atom v1.0.1/go.mod h1:FNTYJfatYaIOQn4OKy8y+Mtohqm3MeyEGZUu4bMtZ9E=
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.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw=
@ -46,8 +46,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
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/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/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk=
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/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=
@ -58,10 +58,12 @@ github.com/envoyproxy/go-control-plane/envoy v1.32.3 h1:hVEaommgvzTjTd4xCaFd+kEQ
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/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@ -80,8 +82,6 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
@ -94,10 +94,6 @@ github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrk
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=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
@ -113,24 +109,24 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
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/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao=
go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
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=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
@ -174,21 +170,21 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
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.216.0 h1:xnEHy+xWFrtYInWPy8OdGFsyIfWJjtVnO39g7pz2BFY=
google.golang.org/api v0.216.0/go.mod h1:K9wzQMvWi47Z9IU7OgdOofvZuw75Ge3PPITImZR/UyI=
google.golang.org/api v0.218.0 h1:x6JCjEWeZ9PFCRe9z0FBrNwj7pB7DOAqT35N+IPnAUA=
google.golang.org/api v0.218.0/go.mod h1:5VGHBAkxrA/8EFjLVEYmMUJ8/8+gWWQ3s4cFH0FxG2M=
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-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/genproto v0.0.0-20250124145028-65684f501c47 h1:SI8Hf7K4+uVYchXqZiMfP44PZ83xomMWovbcFfm0P8Q=
google.golang.org/genproto v0.0.0-20250124145028-65684f501c47/go.mod h1:qbZzneIOXSq+KFAFut9krLfRLZiFLzZL5u2t8SV83EE=
google.golang.org/genproto/googleapis/api v0.0.0-20250124145028-65684f501c47 h1:5iw9XJTD4thFidQmFVvx0wi4g5yOHk76rNRUxz1ZG5g=
google.golang.org/genproto/googleapis/api v0.0.0-20250124145028-65684f501c47/go.mod h1:AfA77qWLcidQWywD0YgqfpJzf50w2VjzBml3TybHeJU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47 h1:91mG8dNTpkC0uChJUQ9zCiRqx3GEEFOWaRZ0mI6Oj2I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
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.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/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=

View File

@ -7,6 +7,11 @@
<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>
<form class="flex" hx-encoding="multipart/form-data">
<label class="btn text-center" for="docx-upload">Word-Dokument hochladen</label>
<input accept=".docx" class="hidden" id="docx-upload" name="docx-upload" type="file"
hx-post="/docx/upload" />
</form>
<button class="btn" hx-get="/article/all-rejected" hx-target="#page-content">Artikel bearbeiten</button>
{{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}}

View File

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