Compare commits

..

11 Commits

39 changed files with 359 additions and 1120 deletions

View File

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

View File

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

View File

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

View File

@ -8,12 +8,14 @@ import (
"google.golang.org/api/option" "google.golang.org/api/option"
) )
type Client struct{ *auth.Client } type Client struct {
*auth.Client
}
func NewClient(c *Config) (*Client, error) { func NewClient(c *Config) (*Client, error) {
var err error var err error
client := new(Client) client := new(Client)
ctx := context.Background() ctx := context.Background()
opt := option.WithCredentialsFile(c.FirebaseKey) opt := option.WithCredentialsFile(c.FirebaseKey)

View File

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

View File

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

View File

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

View File

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

View File

@ -8,10 +8,8 @@ import (
func ServeRSS(c *b.Config) http.HandlerFunc { func ServeRSS(c *b.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if !tokenIsVerified(w, r, c) { if tokenIsVerified(w, r, c) {
return http.ServeFile(w, r, c.RSSFile)
} }
http.ServeFile(w, r, c.RSSFile)
} }
} }

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"html/template" "html/template"
"io" "io"
"io/fs"
"log" "log"
"net/http" "net/http"
"os" "os"
@ -29,7 +30,7 @@ func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return return
} }
type htmlData struct { type editorHTMLData struct {
Title string Title string
Description string Description string
Content string Content string
@ -37,12 +38,12 @@ func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
Tags []*b.Tag Tags []*b.Tag
Mode int Mode int
} }
var data htmlData
var data editorHTMLData
if session.Values["article"] == nil { if session.Values["article"] == nil {
data = htmlData{} data = editorHTMLData{}
} else { } else {
data = session.Values["article"].(htmlData) data = session.Values["article"].(editorHTMLData)
} }
data.Mode = EditMode data.Mode = EditMode
@ -73,13 +74,12 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
} }
article := &b.Article{ article := &b.Article{
Title: r.PostFormValue("article-title"), Title: r.PostFormValue("article-title"),
Description: r.PostFormValue("article-description"), Description: r.PostFormValue("article-description"),
Published: false, Content: r.PostFormValue("article-content"),
Rejected: false, Published: false,
AuthorID: session.Values["id"].(int64), Rejected: false,
IsInIssue: r.PostFormValue("issue") == "on", AuthorID: session.Values["id"].(int64),
AutoGenerated: false,
} }
article.ID, err = db.AddArticle(article) article.ID, err = db.AddArticle(article)
@ -89,20 +89,6 @@ func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return return
} }
articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
if err = os.WriteFile(articleAbsName, []byte(r.PostFormValue("article-content")), 0644); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
article.Link = fmt.Sprint(c.Domain, "/article/serve/", article.ID)
if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "link", Value: article.Link}); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
r.ParseForm() r.ParseForm()
tags := make([]int64, 0) tags := make([]int64, 0)
for _, tag := range r.Form["tags"] { for _, tag := range r.Form["tags"] {
@ -144,18 +130,11 @@ func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
description := r.PostFormValue("article-description") description := r.PostFormValue("article-description")
content := r.PostFormValue("article-content") content := r.PostFormValue("article-content")
link := fmt.Sprint(c.ArticleDir, "/", id, ".md")
if err = os.WriteFile(link, []byte(content), 0644); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = db.UpdateAttributes( if err = db.UpdateAttributes(
&b.Attribute{Table: "articles", ID: id, AttName: "title", Value: title}, &b.Attribute{Table: "articles", ID: id, AttName: "title", Value: title},
&b.Attribute{Table: "articles", ID: id, AttName: "description", Value: description}, &b.Attribute{Table: "articles", ID: id, AttName: "description", Value: description},
&b.Attribute{Table: "articles", ID: id, AttName: "content", Value: content},
&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false}, &b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false},
&b.Attribute{Table: "articles", ID: id, AttName: "is_in_issue", Value: r.PostFormValue("issue") == "on"},
); err != nil { ); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -211,10 +190,11 @@ func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerF
return return
} }
data := new(struct { type htmlData struct {
MyIDs map[int64]bool MyIDs map[int64]bool
RejectedArticles []*b.Article RejectedArticles []*b.Article
}) }
data := new(htmlData)
data.RejectedArticles, err = db.GetCertainArticles(false, true) data.RejectedArticles, err = db.GetCertainArticles(false, true)
if err != nil { if err != nil {
@ -242,35 +222,46 @@ func ReviewUnpublishedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Hand
return return
} }
data := new(struct { type htmlData struct {
Article *b.Article Title string
Content template.HTML Description string
Tags []*b.Tag Content template.HTML
}) Tags []*b.Tag
ID int64
}
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) var err error
data := new(htmlData)
data.ID, err = strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
data.Article, err = db.GetArticle(id) article, err := db.GetArticle(data.ID)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
articleAbsName := fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md") data.Title, err = b.ConvertToPlain(article.Title)
contentBytes, err := os.ReadFile(articleAbsName)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
content, err := b.ConvertToHTML(string(contentBytes)) data.Description, err = b.ConvertToPlain(article.Description)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
content, err := b.ConvertToHTML(article.Content)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -278,7 +269,7 @@ func ReviewUnpublishedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Hand
} }
data.Content = template.HTML(content) data.Content = template.HTML(content)
data.Tags, err = db.GetArticleTags(data.Article.ID) data.Tags, err = db.GetArticleTags(data.ID)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -297,12 +288,12 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
return return
} }
data := new(struct { type htmlData struct {
Selected map[int64]bool Selected map[int64]bool
Article *b.Article Article *b.Article
Content string
Tags []*b.Tag Tags []*b.Tag
}) }
data := new(htmlData)
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil { if err != nil {
@ -318,16 +309,6 @@ func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.Handler
return return
} }
articleAbsName := fmt.Sprint(c.ArticleDir, "/", data.Article.ID, ".md")
contentBytes, err := os.ReadFile(articleAbsName)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Content = string(contentBytes)
data.Tags, err = db.GetTagList() data.Tags, err = db.GetTagList()
if err != nil { if err != nil {
log.Println(err) log.Println(err)
@ -360,20 +341,13 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
} }
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
article, err := db.GetArticle(id)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
if err = db.AddArticleToCurrentIssue(article.ID); err != nil { if err = db.AddArticleToCurrentIssue(id); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -389,7 +363,7 @@ func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return return
} }
feed, err := b.GenerateRSS(c, db) feed, err := b.GenerateRSS(db, c.Title, c.Link, c.Description)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -453,7 +427,7 @@ func ShowCurrentArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFu
} }
} }
func UploadArticleImage(c *b.Config, s *b.CookieStore) http.HandlerFunc { func UploadImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil { if _, err := getSession(w, r, c, s); err != nil {
return return
@ -462,7 +436,7 @@ func UploadArticleImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
file, header, err := r.FormFile("article-image") file, header, err := r.FormFile("article-image")
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
defer file.Close() defer file.Close()
@ -477,6 +451,12 @@ func UploadArticleImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
return return
} }
if err = os.MkdirAll(fmt.Sprint(c.PicsDir, "/"), fs.FileMode(0755)); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
img, err := os.Create(absFilepath) img, err := os.Create(absFilepath)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
@ -491,150 +471,8 @@ func UploadArticleImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
return return
} }
url := fmt.Sprint(c.Domain, "/image/serve/", filename) url := fmt.Sprint(c.Domain, "/pics/", filename)
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(url) json.NewEncoder(w).Encode(url)
} }
} }
func ShowPublishedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil {
return
}
publishedArticles, err := db.GetCertainArticles(true, false)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
filteredArticles := make([]*b.Article, 0)
for _, article := range publishedArticles {
if !article.AutoGenerated {
filteredArticles = append(filteredArticles, article)
}
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/published-articles.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", filteredArticles)
}
}
func ReviewArticleForDeletion(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil {
return
}
var err error
data := new(struct {
Title string
Description string
Content template.HTML
Tags []*b.Tag
ID int64
})
data.ID, err = strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
article, err := db.GetArticle(data.ID)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Title, err = b.ConvertToPlain(article.Title)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Description, err = b.ConvertToPlain(article.Description)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
contentBytes, err := os.ReadFile(articleAbsName)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
content, err := b.ConvertToHTML(string(contentBytes))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Content = template.HTML(content)
data.Tags, err = db.GetArticleTags(data.ID)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/to-be-deleted.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", data)
}
}
func DeleteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s)
if err != nil {
return
}
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = db.DeleteArticle(id); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = os.Remove(fmt.Sprint(c.ArticleDir, "/", id, ".md")); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
feed, err := b.GenerateRSS(c, db)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = b.SaveRSS(c.RSSFile, feed); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int))
}
}

View File

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

View File

@ -1,18 +1,10 @@
package frontend package frontend
import ( import (
"fmt"
"html/template" "html/template"
"io"
"log" "log"
"mime"
"net/http" "net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/google/uuid"
b "streifling.com/jason/cpolis/cmd/backend" b "streifling.com/jason/cpolis/cmd/backend"
) )
@ -23,169 +15,14 @@ func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFun
return return
} }
session.Values["article"] = nil
if err = session.Save(r, w); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if session.Values["issue-image"] == nil {
err := "error: Image required"
log.Println(err)
http.Error(w, err, http.StatusBadRequest)
return
}
imgFileName := session.Values["issue-image"].(string)
imgAbsName := fmt.Sprint(c.PicsDir, "/", imgFileName)
imgFile, err := os.Open(imgAbsName)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer imgFile.Close()
imgInfo, err := imgFile.Stat()
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
imgSize := imgInfo.Size()
mimeType := mime.TypeByExtension(filepath.Ext(imgAbsName))
article := &b.Article{
Title: r.PostFormValue("issue-title"),
EncURL: fmt.Sprint(c.Domain, "/image/serve/", imgFileName),
EncLength: int(imgSize),
EncType: mimeType,
Published: true,
Rejected: false,
Created: time.Now(),
AuthorID: session.Values["id"].(int64),
AutoGenerated: true,
}
article.ID, err = db.AddArticle(article)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
articleAbsName := fmt.Sprint(c.ArticleDir, "/", article.ID, ".md")
if err = os.WriteFile(articleAbsName, []byte(r.PostFormValue("article-content")), 0644); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
article.Link = fmt.Sprint(c.Domain, "/article/serve/", article.ID)
if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "link", Value: article.Link}); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = db.UpdateAttributes(&b.Attribute{Table: "articles", ID: article.ID, AttName: "link", Value: article.Link}); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = db.AddArticleToCurrentIssue(article.ID); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := db.PublishLatestIssue(); err != nil { if err := db.PublishLatestIssue(); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
feed, err := b.GenerateRSS(c, db)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = b.SaveRSS(c.RSSFile, feed); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session.Values["issue-image"] = nil
if err = session.Save(r, w); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html") tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err) tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"]) tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
} }
} }
func UploadIssueImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := getSession(w, r, c, s)
if err != nil {
return
}
if err := r.ParseMultipartForm(10 << 20); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
file, header, err := r.FormFile("issue-image")
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer file.Close()
nameStrings := strings.Split(header.Filename, ".")
extension := "." + nameStrings[len(nameStrings)-1]
filename := fmt.Sprint(uuid.New(), extension)
absFilepath, err := filepath.Abs(fmt.Sprint(c.PicsDir, "/", filename))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
img, err := os.Create(absFilepath)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer img.Close()
if _, err = io.Copy(img, file); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session.Values["issue-image"] = filename
if err = session.Save(r, w); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
}

View File

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

View File

@ -60,7 +60,7 @@ func Login(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
id, ok := db.GetID(userName) id, ok := db.GetID(userName)
if !ok { if !ok {
http.Error(w, fmt.Sprintf("no such user: %v", userName), http.StatusBadRequest) http.Error(w, fmt.Sprintf("no such user: %v", userName), http.StatusInternalServerError)
return return
} }
@ -96,6 +96,7 @@ func Logout(c *b.Config, s *b.CookieStore) http.HandlerFunc {
} }
session.Options.MaxAge = -1 session.Options.MaxAge = -1
if err = session.Save(r, w); err != nil { if err = session.Save(r, w); err != nil {
log.Println(err) log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)

View File

@ -198,6 +198,10 @@ func UpdateSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
func AddFirstUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { func AddFirstUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if _, err := getSession(w, r, c, s); err != nil {
return
}
var err error var err error
htmlData := UserData{ htmlData := UserData{
User: &b.User{ User: &b.User{
@ -277,12 +281,12 @@ func ShowAllUsers(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.H
return return
} }
data := new(struct { type htmlData struct {
Users map[int64]*b.User Users map[int64]*b.User
Action string Action string
}) }
data.Action = action data := &htmlData{Action: action}
data.Users, err = db.GetAllUsers() data.Users, err = db.GetAllUsers()
if err != nil { if err != nil {
log.Println(err) log.Println(err)

View File

@ -49,44 +49,38 @@ func main() {
http.FileServer(http.Dir(config.WebDir+"/static/")))) http.FileServer(http.Dir(config.WebDir+"/static/"))))
mux.HandleFunc("/", f.HomePage(config, db, store)) mux.HandleFunc("/", f.HomePage(config, db, store))
mux.HandleFunc("GET /article/all-published", f.ShowPublishedArticles(config, db, store)) mux.HandleFunc("GET /create-tag", f.CreateTag(config, store))
mux.HandleFunc("GET /article/all-rejected", f.ShowRejectedArticles(config, db, store)) mux.HandleFunc("GET /create-user", f.CreateUser(config, store))
mux.HandleFunc("GET /article/all-unpublished", f.ShowUnpublishedArticles(config, db, store)) mux.HandleFunc("GET /edit-self", f.EditSelf(config, db, store))
mux.HandleFunc("GET /article/delete/{id}", f.DeleteArticle(config, db, store)) mux.HandleFunc("GET /edit-user/{id}", f.EditUser(config, db, store))
mux.HandleFunc("GET /article/publish/{id}", f.PublishArticle(config, db, store)) mux.HandleFunc("GET /delete-user/{id}", f.DeleteUser(config, db, store))
mux.HandleFunc("GET /article/reject/{id}", f.RejectArticle(config, db, store))
mux.HandleFunc("GET /article/review-deletion/{id}", f.ReviewArticleForDeletion(config, db, store))
mux.HandleFunc("GET /article/review-rejected/{id}", f.ReviewRejectedArticle(config, db, store))
mux.HandleFunc("GET /article/review-unpublished/{id}", f.ReviewUnpublishedArticle(config, db, store))
mux.HandleFunc("GET /article/serve/{id}", c.ServeArticle(config, db))
mux.HandleFunc("GET /article/write", f.WriteArticle(config, db, store))
mux.HandleFunc("GET /hub", f.ShowHub(config, db, store)) mux.HandleFunc("GET /hub", f.ShowHub(config, db, store))
mux.HandleFunc("GET /image/serve/{pic}", c.ServeImage(config, store))
mux.HandleFunc("GET /issue/this", f.ShowCurrentArticles(config, db, store))
mux.HandleFunc("GET /logout", f.Logout(config, store)) mux.HandleFunc("GET /logout", f.Logout(config, store))
mux.HandleFunc("GET /pdf/get-list", c.ServePDFList(config)) mux.HandleFunc("GET /pdf/get-list", c.ServePDFList(config))
mux.HandleFunc("GET /pdf/serve/{id}", c.ServePDF(config)) mux.HandleFunc("GET /pdf/{id}", c.ServePDF(config))
mux.HandleFunc("GET /rss/serve", c.ServeRSS(config)) mux.HandleFunc("GET /pics/{pic}", f.ServeImage(config, store))
mux.HandleFunc("GET /tag/create", f.CreateTag(config, store)) mux.HandleFunc("GET /publish-article/{id}", f.PublishArticle(config, db, store))
mux.HandleFunc("GET /user/create", f.CreateUser(config, store)) mux.HandleFunc("GET /publish-issue", f.PublishLatestIssue(config, db, store))
mux.HandleFunc("GET /user/delete/{id}", f.DeleteUser(config, db, store)) mux.HandleFunc("GET /reject-article/{id}", f.RejectArticle(config, db, store))
mux.HandleFunc("GET /user/edit/{id}", f.EditUser(config, db, store)) mux.HandleFunc("GET /rejected-articles", f.ShowRejectedArticles(config, db, store))
mux.HandleFunc("GET /user/edit/self", f.EditSelf(config, db, store)) mux.HandleFunc("GET /review-rejected-article/{id}", f.ReviewRejectedArticle(config, db, store))
mux.HandleFunc("GET /user/show-all/delete", f.ShowAllUsers(config, db, store, "delete")) mux.HandleFunc("GET /review-unpublished-article/{id}", f.ReviewUnpublishedArticle(config, db, store))
mux.HandleFunc("GET /user/show-all/edit", f.ShowAllUsers(config, db, store, "edit")) mux.HandleFunc("GET /rss", c.ServeRSS(config))
mux.HandleFunc("GET /show-all-users-edit", f.ShowAllUsers(config, db, store, "edit-user"))
mux.HandleFunc("GET /show-all-users-delete", f.ShowAllUsers(config, db, store, "delete-user"))
mux.HandleFunc("GET /this-issue", f.ShowCurrentArticles(config, db, store))
mux.HandleFunc("GET /unpublished-articles", f.ShowUnpublishedArticles(config, db, store))
mux.HandleFunc("GET /write-article", f.WriteArticle(config, db, store))
mux.HandleFunc("POST /article/resubmit/{id}", f.ResubmitArticle(config, db, store)) mux.HandleFunc("POST /add-first-user", f.AddFirstUser(config, db, store))
mux.HandleFunc("POST /article/submit", f.SubmitArticle(config, db, store)) mux.HandleFunc("POST /add-tag", f.AddTag(config, db, store))
mux.HandleFunc("POST /article/upload-image", f.UploadArticleImage(config, store)) mux.HandleFunc("POST /add-user", f.AddUser(config, db, store))
mux.HandleFunc("POST /issue/publish", f.PublishLatestIssue(config, db, store))
mux.HandleFunc("POST /issue/upload-image", f.UploadIssueImage(config, store))
mux.HandleFunc("POST /login", f.Login(config, db, store)) mux.HandleFunc("POST /login", f.Login(config, db, store))
mux.HandleFunc("POST /pdf/upload", f.UploadPDF(config, store)) mux.HandleFunc("POST /resubmit-article/{id}", f.ResubmitArticle(config, db, store))
mux.HandleFunc("POST /tag/add", f.AddTag(config, db, store)) mux.HandleFunc("POST /submit-article", f.SubmitArticle(config, db, store))
mux.HandleFunc("POST /user/add", f.AddUser(config, db, store)) mux.HandleFunc("POST /update-self", f.UpdateSelf(config, db, store))
mux.HandleFunc("POST /user/add-first", f.AddFirstUser(config, db, store)) mux.HandleFunc("POST /update-user/{id}", f.UpdateUser(config, db, store))
mux.HandleFunc("POST /user/update/{id}", f.UpdateUser(config, db, store)) mux.HandleFunc("POST /upload-image", f.UploadImage(config, store))
mux.HandleFunc("POST /user/update/self", f.UpdateSelf(config, db, store))
log.Fatalln(http.ListenAndServe(config.Port, mux)) log.Fatalln(http.ListenAndServe(config.Port, mux))
} }

View File

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

2
go.mod
View File

@ -4,7 +4,7 @@ go 1.22.0
require ( require (
firebase.google.com/go/v4 v4.14.1 firebase.google.com/go/v4 v4.14.1
git.streifling.com/jason/rss v0.1.3 git.streifling.com/jason/rss v0.1.2
github.com/BurntSushi/toml v1.3.2 github.com/BurntSushi/toml v1.3.2
github.com/go-sql-driver/mysql v1.7.1 github.com/go-sql-driver/mysql v1.7.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0

4
go.sum
View File

@ -15,8 +15,8 @@ cloud.google.com/go/storage v1.40.0 h1:VEpDQV5CJxFmJ6ueWNsKxcr1QAYOXEgxDa+sBbJah
cloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g= cloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g=
firebase.google.com/go/v4 v4.14.1 h1:4qiUETaFRWoFGE1XP5VbcEdtPX93Qs+8B/7KvP2825g= firebase.google.com/go/v4 v4.14.1 h1:4qiUETaFRWoFGE1XP5VbcEdtPX93Qs+8B/7KvP2825g=
firebase.google.com/go/v4 v4.14.1/go.mod h1:fgk2XshgNDEKaioKco+AouiegSI9oTWVqRaBdTTGBoM= firebase.google.com/go/v4 v4.14.1/go.mod h1:fgk2XshgNDEKaioKco+AouiegSI9oTWVqRaBdTTGBoM=
git.streifling.com/jason/rss v0.1.3 h1:fd3j4ZtcLehapcmmroo3AP3X34gRHC4xzpfV6bDV1ZU= git.streifling.com/jason/rss v0.1.2 h1:UB3UHJXMt5WDDh9y8n0Z6nS1XortbPXjEr7QZTdovY4=
git.streifling.com/jason/rss v0.1.3/go.mod h1:gpZF0nZbQSstMpyHD9DTAvlQEG7v4pjO5c7aIMWM4Jg= git.streifling.com/jason/rss v0.1.2/go.mod h1:gpZF0nZbQSstMpyHD9DTAvlQEG7v4pjO5c7aIMWM4Jg=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=

View File

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

View File

@ -43,15 +43,13 @@ chmod +x $CPOLIS_DIR/tailwindcss
$CPOLIS_DIR/tailwindcss -i $CPOLIS_DIR/web/static/css/input.css -o $CPOLIS_DIR/web/static/css/style.css $CPOLIS_DIR/tailwindcss -i $CPOLIS_DIR/web/static/css/input.css -o $CPOLIS_DIR/web/static/css/style.css
echo '\nBuilding cpolis...' >&2 echo '\nBuilding cpolis...' >&2
cd $CPOLIS_DIR go build -o $TMP_DIR/cpolis $CPOLIS_DIR/cmd/main.go
go build -o $BIN_DIR/cpolis cmd/main.go
cd
echo '\nSetting up system files...' >&2 echo '\nSetting system files up...' >&2
sudo mv $TMP_DIR/cpolis $BIN_DIR/cpolis
sudo chown root:root $BIN_DIR/cpolis sudo chown root:root $BIN_DIR/cpolis
chmod +x $BIN_DIR/cpolis chmod +x $BIN_DIR/cpolis
echo '\nSetting up service...' >&2
sudo mv $CPOLIS_DIR/cpolis.service $SYSTEMD_DIR sudo mv $CPOLIS_DIR/cpolis.service $SYSTEMD_DIR
sudo chown root:root $SYSTEMD_DIR/cpolis.service sudo chown root:root $SYSTEMD_DIR/cpolis.service
sudo systemctl daemon-reload sudo systemctl daemon-reload

View File

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

View File

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

View File

@ -2,7 +2,7 @@
<h2>Neuer Benutzer</h2> <h2>Neuer Benutzer</h2>
<form> <form>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"> <div class="grid grid-cols-3 gap-4">
<div> <div>
<label for="username">Benutzername</label> <label for="username">Benutzername</label>
<input class="w-full" required name="username" type="text" value="{{.UserName}}" /> <input class="w-full" required name="username" type="text" value="{{.UserName}}" />
@ -25,7 +25,7 @@
</div> </div>
</div> </div>
<div class="flex flex-wrap gap-4"> <div class="flex gap-4">
<div> <div>
<input required id="author" name="role" type="radio" value="3" {{if eq .Role 3 }}checked{{end}} /> <input required id="author" name="role" type="radio" value="3" {{if eq .Role 3 }}checked{{end}} />
<label for="author">Autor</label> <label for="author">Autor</label>
@ -45,7 +45,7 @@
</div> </div>
<div class="btn-area"> <div class="btn-area">
<input class="action-btn" type="submit" value="Anlegen" hx-post="/user/add" hx-target="#page-content" /> <input class="action-btn" type="submit" value="Anlegen" hx-post="/add-user" hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button> <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
</div> </div>
</form> </form>

View File

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

View File

@ -2,7 +2,7 @@
<h2>Profil bearbeiten</h2> <h2>Profil bearbeiten</h2>
<form> <form>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"> <div class="grid grid-cols-3 gap-4">
<div> <div>
<label for="username">Benutzername</label> <label for="username">Benutzername</label>
<input class="w-full" name="username" type="text" value="{{.UserName}}" /> <input class="w-full" name="username" type="text" value="{{.UserName}}" />
@ -35,7 +35,7 @@
</div> </div>
<div class="btn-area"> <div class="btn-area">
<input class="action-btn" type="submit" value="Aktualisieren" hx-post="/user/update/self" <input class="action-btn" type="submit" value="Aktualisieren" hx-post="/update-self"
hx-target="#page-content" /> hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button> <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
</div> </div>

View File

@ -2,7 +2,7 @@
<h2>Profil von {{.FirstName}} {{.LastName}} bearbeiten</h2> <h2>Profil von {{.FirstName}} {{.LastName}} bearbeiten</h2>
<form> <form>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"> <div class="grid grid-cols-3 gap-4">
<div> <div>
<label for="username">Benutzername</label> <label for="username">Benutzername</label>
<input class="w-full" name="username" type="text" value="{{.UserName}}" /> <input class="w-full" name="username" type="text" value="{{.UserName}}" />
@ -29,7 +29,7 @@
</div> </div>
</div> </div>
<div class="flex flex-wrap gap-4"> <div class="flex gap-4">
<div> <div>
<input required id="author" name="role" type="radio" value="3" {{if eq .Role 3 }}checked{{end}} /> <input required id="author" name="role" type="radio" value="3" {{if eq .Role 3 }}checked{{end}} />
<label for="author">Autor</label> <label for="author">Autor</label>
@ -49,7 +49,7 @@
</div> </div>
<div class="btn-area"> <div class="btn-area">
<input class="action-btn" type="submit" value="Aktualisieren" hx-post="/user/update/{{.ID}}" <input class="action-btn" type="submit" value="Aktualisieren" hx-post="/update-user/{{.ID}}"
hx-target="#page-content" /> hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button> <button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
</div> </div>

View File

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

View File

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

View File

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

View File

@ -5,26 +5,13 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Orient Editor</title> <title>Orient Editor</title>
<link href="/web/static/css/style.css" rel="stylesheet"> <link href="/web/static/css/style.css" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/easymde/dist/easymde.min.css"> <link rel="stylesheet" href="https://unpkg.com/easymde/dist/easymde.min.css">
</head> </head>
<body <body class="flex flex-col justify-between min-h-screen bg-slate-50">
class="bg-slate-50 dark:bg-slate-950 container flex flex-col justify-between max-w-screen-lg min-h-screen mx-auto text-slate-900 dark:text-slate-100">
<header class="my-8"> <header class="my-8">
<h1 class="font-bold text-4xl text-center">Orient Editor</h1> <h1 class="font-bold text-4xl text-center">Orient Editor</h1>
<div class="ml-4">
<label class="cursor-pointer flex items-center relative" for="theme-toggle">
<div class="bg-slate-200 dark:bg-slate-800 block h-6 w-12 rounded-full"></div>
<div
class="absolute bg-slate-800 dark:bg-slate-50 dot left-1 top-1 h-4 w-4 rounded-full transform transition">
</div>
<span class="ml-2">Dunkel</span>
</label>
<input type="checkbox" id="theme-toggle" class="sr-only">
</div>
</header> </header>
<main class="mx-4"> <main class="mx-4">
@ -33,44 +20,14 @@
</div> </div>
</main> </main>
<footer class="text-center text-gray-500 my-8"> <footer class="my-8">
<p> <p class="text-center text-gray-500 dark:text-gray-400">
&copy; 2024 Jason Streifling. Alle Rechte vorbehalten. &copy; 2024 Jason Streifling. Alle Rechte vorbehalten.
</p> </p>
<p>
v0.10.3 - <strong>Hinweis:</strong> Diese Software befindet sich noch in der Entwicklung und kann Fehler
enthalten.
</p>
</footer> </footer>
<script src="https://unpkg.com/htmx.org@2.0.2"></script> <script src="https://unpkg.com/htmx.org@2.0.1"></script>
<script src="https://unpkg.com/easymde/dist/easymde.min.js"></script> <script src="https://unpkg.com/easymde/dist/easymde.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const toggleSwitch = document.getElementById('theme-toggle');
const dot = document.querySelector('.dot');
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
toggleSwitch.checked = true;
dot.classList.add('translate-x-6');
} else {
document.documentElement.classList.remove('dark')
}
toggleSwitch.addEventListener('change', () => {
if (toggleSwitch.checked) {
document.documentElement.classList.add('dark');
localStorage.theme = 'dark';
dot.classList.add('translate-x-6');
} else {
document.documentElement.classList.remove('dark');
localStorage.theme = 'light';
dot.classList.remove('translate-x-6');
}
});
});
</script>
</body> </body>
</html> </html>

View File

@ -2,13 +2,11 @@
<h2>Anmeldung</h2> <h2>Anmeldung</h2>
<form> <form>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 my-1"> <div class="btn-area">
<input class="w-full" name="username" placeholder="Benutzername" type="text" /> <input class="w-full" name="username" placeholder="Benutzername" type="text" />
<input class="w-full" name="password" placeholder="Passwort" type="password" /> <input class="w-full" name="password" placeholder="Passwort" type="password" />
</div> </div>
<div class="mt-2"> <input class="action-btn" type="submit" value="Anmelden" hx-post="/login" hx-target="#page-content" />
<input class="action-btn" type="submit" value="Anmelden" hx-post="/login" hx-target="#page-content" />
</div>
</form> </form>
{{end}} {{end}}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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