Compare commits

...

10 Commits

31 changed files with 767 additions and 273 deletions

View File

@ -5,7 +5,7 @@ tmp_dir = "tmp"
[build]
args_bin = [
"-desc 'Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität'",
"-domain localhost:8080",
"-domain localhost",
"-key tmp/key.gob",
"-link https://distrikt-ni-st.de",
"-log tmp/cpolis.log",

View File

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

View File

@ -1,4 +1,4 @@
package model
package backend
import (
"context"

View File

@ -1,4 +1,4 @@
package model
package backend
import (
"fmt"

131
cmd/backend/config.go Normal file
View File

@ -0,0 +1,131 @@
package backend
import (
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/BurntSushi/toml"
)
type Config struct {
DBName string
Description string
Domain string
KeyFile string
Link string
LogFile string
PicsDir string
Port string
RSSFile string
Title string
WebDir string
}
func newConfig() *Config {
return &Config{
DBName: "cpolis",
KeyFile: "/var/www/cpolis/cpolis.key",
LogFile: "/var/log/cpolis.log",
PicsDir: "/var/www/cpolis/pics",
RSSFile: "/var/www/cpolis/cpolis.rss",
WebDir: "/var/www/cpolis/web",
}
}
func (c *Config) readFile() error {
cfgFile, err := filepath.Abs(os.Getenv("HOME") + "/.config/cpolis/config.toml")
if err != nil {
return fmt.Errorf("error getting absolute path for config file: %v", err)
}
_, err = os.Stat(cfgFile)
if os.IsNotExist(err) {
fileStrings := strings.Split(cfgFile, "/")
dir := strings.Join(fileStrings[0:len(fileStrings)-1], "/")
if err = os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("error creating config directory: %v", err)
}
fileName := fileStrings[len(fileStrings)-1]
file, err := os.Create(dir + "/" + fileName)
if err != nil {
return fmt.Errorf("error creating config file: %v", err)
}
defer file.Close()
if err = file.Chmod(0644); err != nil {
return fmt.Errorf("error setting permissions for config file: %v", err)
}
} else {
_, err = toml.DecodeFile(cfgFile, c)
if err != nil {
return fmt.Errorf("error reading config file: %v", err)
}
}
return nil
}
func (c *Config) handleCliArgs() error {
var err error
port := 8080
flag.StringVar(&c.DBName, "db", c.DBName, "DB name")
flag.StringVar(&c.Description, "desc", c.Description, "Channel description")
flag.StringVar(&c.Domain, "domain", c.Domain, "domain name")
flag.StringVar(&c.KeyFile, "key", c.KeyFile, "key file")
flag.StringVar(&c.Link, "link", c.Link, "Channel Link")
flag.StringVar(&c.LogFile, "log", c.LogFile, "log file")
flag.StringVar(&c.PicsDir, "pics", c.PicsDir, "pictures directory")
flag.StringVar(&c.RSSFile, "rss", c.RSSFile, "RSS file")
flag.StringVar(&c.Title, "title", c.Title, "Channel title")
flag.StringVar(&c.WebDir, "web", c.WebDir, "web directory")
flag.IntVar(&port, "port", port, "port")
flag.Parse()
c.KeyFile, err = filepath.Abs(c.KeyFile)
if err != nil {
return fmt.Errorf("error finding absolute path for key file: %v", err)
}
c.LogFile, err = filepath.Abs(c.LogFile)
if err != nil {
return fmt.Errorf("error finding absolute path for log file: %v", err)
}
c.PicsDir, err = filepath.Abs(c.PicsDir)
if err != nil {
return fmt.Errorf("error finding absolute path for pics dir: %v", err)
}
c.Port = fmt.Sprint(":", port)
c.RSSFile, err = filepath.Abs(c.RSSFile)
if err != nil {
return fmt.Errorf("error finding absolute path for RSS file: %v", err)
}
c.WebDir, err = filepath.Abs(c.WebDir)
if err != nil {
return fmt.Errorf("error finding absolute path for web dir: %v", err)
}
return nil
}
func HandleConfig() (*Config, error) {
config := newConfig()
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 config, nil
}

View File

@ -1,4 +1,4 @@
package model
package backend
import (
"bufio"

View File

@ -1,4 +1,4 @@
package model
package backend
import (
"context"

View File

@ -1,4 +1,4 @@
package control
package backend
import (
"bytes"

View File

@ -1,4 +1,4 @@
package control
package backend
import (
"fmt"
@ -7,10 +7,9 @@ import (
"time"
"git.streifling.com/jason/rss"
"streifling.com/jason/cpolis/cmd/model"
)
func GetChannel(db *model.DB, title, link, description string) (*rss.Channel, error) {
func GetChannel(db *DB, title, link, description string) (*rss.Channel, error) {
channel := &rss.Channel{
Title: title,
Link: link,
@ -51,7 +50,7 @@ func GetChannel(db *model.DB, title, link, description string) (*rss.Channel, er
return channel, nil
}
func GenerateRSS(db *model.DB, title, link, desc string) (*string, error) {
func GenerateRSS(db *DB, title, link, desc string) (*string, error) {
channel := &rss.Channel{
Title: title,
Link: link,
@ -121,7 +120,9 @@ func SaveRSS(filename string, feed *string) error {
return fmt.Errorf("error creating file for RSS feed: %v", err)
}
defer file.Close()
file.Chmod(0644)
if err = file.Chmod(0644); err != nil {
return fmt.Errorf("error setting permissions for RSS file: %v", err)
}
if _, err = io.WriteString(file, *feed); err != nil {
return fmt.Errorf("error writing to RSS file: %v", err)

View File

@ -1,4 +1,4 @@
package control
package backend
import (
"crypto/rand"

View File

@ -1,4 +1,4 @@
package model
package backend
import "fmt"

View File

@ -1,4 +1,4 @@
package model
package backend
import (
"context"
@ -14,6 +14,7 @@ const (
Publisher
Editor
Author
NonExistent
)
type User struct {
@ -145,7 +146,7 @@ func (db *DB) GetUser(id int64) (*User, error) {
return user, nil
}
func (db *DB) UpdateUserAttributes(id int64, user, first, last, oldPass, newPass, newPass2 string) error {
func (db *DB) UpdateOwnAttributes(id int64, user, first, last, oldPass, newPass, newPass2 string) error {
passwordEmpty := true
if len(newPass) > 0 || len(newPass2) > 0 {
if newPass != newPass2 {
@ -268,3 +269,112 @@ func (db *DB) AddFirstUser(u *User, pass string) (int64, error) {
}
return 0, fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries)
}
func (db *DB) GetAllUsers() (map[int64]*User, error) {
query := "SELECT id, username, first_name, last_name, role FROM users"
rows, err := db.Query(query)
if err != nil {
return nil, fmt.Errorf("error getting all users from DB: %v", err)
}
users := make(map[int64]*User, 0)
for rows.Next() {
user := new(User)
if err = rows.Scan(&user.ID, &user.UserName, &user.FirstName,
&user.LastName, &user.Role); err != nil {
return nil, fmt.Errorf("error getting user info: %v", err)
}
users[user.ID] = user
}
return users, nil
}
func (tx *Tx) SetPassword(id int64, newPass string) error {
hashedPass, err := bcrypt.GenerateFromPassword([]byte(newPass), bcrypt.DefaultCost)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return fmt.Errorf("error creating password hash: %v", err)
}
setQuery := "UPDATE users SET password = ? WHERE id = ?"
if _, err = tx.Exec(setQuery, string(hashedPass), id); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return fmt.Errorf("error updating password in DB: %v", err)
}
return nil
}
func (db *DB) UpdateUserAttributes(id int64, user, first, last, newPass, newPass2 string, role int) error {
passwordEmpty := true
if len(newPass) > 0 || len(newPass2) > 0 {
if newPass != newPass2 {
return fmt.Errorf("error: passwords do not match")
}
passwordEmpty = false
}
tx := new(Tx)
var err error
for i := 0; i < TxMaxRetries; i++ {
err := func() error {
tx.Tx, err = db.Begin()
if err != nil {
return fmt.Errorf("error starting transaction: %v", err)
}
if !passwordEmpty {
if err = tx.SetPassword(id, newPass); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return fmt.Errorf("error changing password: %v", err)
}
}
if err = tx.UpdateAttributes(
&Attribute{Table: "users", ID: id, AttName: "username", Value: user},
&Attribute{Table: "users", ID: id, AttName: "first_name", Value: first},
&Attribute{Table: "users", ID: id, AttName: "last_name", Value: last},
&Attribute{Table: "users", ID: id, AttName: "role", Value: role},
); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return fmt.Errorf("error updating attributes in DB: %v", err)
}
if err = tx.Commit(); err != nil {
return fmt.Errorf("error committing transaction: %v", err)
}
return nil
}()
if err == nil {
return nil
}
log.Println(err)
wait(i)
}
return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries)
}
func (db *DB) DeleteUser(id int64) error {
query := "DELETE FROM users WHERE id = ?"
_, err := db.Exec(query, id)
if err != nil {
return fmt.Errorf("error deleting user %v from DB: %v", id, err)
}
return nil
}

View File

@ -1,68 +0,0 @@
package control
import (
"flag"
"fmt"
"path/filepath"
)
type CliArgs struct {
Description string
DBName string
Domain string
KeyFile string
Link string
LogFile string
Port string
PicsDir string
RSSFile string
Title string
WebDir string
}
func HandleCliArgs() (*CliArgs, error) {
var err error
cliArgs := new(CliArgs)
flag.StringVar(&cliArgs.DBName, "db", "cpolis", "DB name")
flag.StringVar(&cliArgs.Description, "desc", "Description", "Channel description")
flag.StringVar(&cliArgs.Domain, "domain", "", "domain name")
keyFile := flag.String("key", "/var/www/cpolis/cpolis.key", "key file")
flag.StringVar(&cliArgs.Link, "link", "Link", "Channel Link")
logFile := flag.String("log", "/var/log/cpolis.log", "log file")
flag.StringVar(&cliArgs.PicsDir, "pics", "pics", "pictures directory")
port := flag.Int("port", 8080, "port")
rssFile := flag.String("rss", "/var/www/cpolis/cpolis.rss", "RSS file")
flag.StringVar(&cliArgs.Title, "title", "Title", "Channel title")
webDir := flag.String("web", "/var/www/cpolis/web", "web directory")
flag.Parse()
cliArgs.KeyFile, err = filepath.Abs(*keyFile)
if err != nil {
return nil, fmt.Errorf("error finding absolute path for KeyFile: %v", err)
}
cliArgs.LogFile, err = filepath.Abs(*logFile)
if err != nil {
return nil, fmt.Errorf("error finding absolute path for LogFile: %v", err)
}
_, err = filepath.Abs(cliArgs.PicsDir)
if err != nil {
return nil, fmt.Errorf("error finding absolute path for PicsDir: %v", err)
}
cliArgs.Port = fmt.Sprint(":", *port)
cliArgs.RSSFile, err = filepath.Abs(*rssFile)
if err != nil {
return nil, fmt.Errorf("error finding absolute path for RSSFile: %v", err)
}
cliArgs.WebDir, err = filepath.Abs(*webDir)
if err != nil {
return nil, fmt.Errorf("error finding absolute path for WebDir: %v", err)
}
return cliArgs, nil
}

View File

@ -1,9 +1,11 @@
package view
package frontend
import (
"encoding/json"
"fmt"
"html/template"
"io"
"io/fs"
"log"
"net/http"
"os"
@ -13,11 +15,15 @@ import (
"time"
"github.com/google/uuid"
"streifling.com/jason/cpolis/cmd/control"
"streifling.com/jason/cpolis/cmd/model"
b "streifling.com/jason/cpolis/cmd/backend"
)
func ShowHub(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.HandlerFunc {
const (
EditMode = iota
PreviewMode
)
func ShowHub(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := s.Get(r, "cookie")
if err != nil {
@ -26,14 +32,45 @@ func ShowHub(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.Hand
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
session.Values["article"] = nil
if err = session.Save(r, w); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", session.Values["role"].(int))
}
}
func WriteArticle(c *control.CliArgs, db *model.DB) http.HandlerFunc {
func WriteArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
tags, err := db.GetTagList()
type editorHTMLData struct {
Title string
Description string
Content string
HTMLContent template.HTML
Tags []*b.Tag
Mode int
}
session, err := s.Get(r, "cookie")
if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
var data editorHTMLData
if session.Values["article"] == nil {
data = editorHTMLData{}
} else {
data = session.Values["article"].(editorHTMLData)
}
data.Mode = EditMode
data.Tags, err = db.GetTagList()
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -41,11 +78,11 @@ func WriteArticle(c *control.CliArgs, db *model.DB) http.HandlerFunc {
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", tags)
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data)
}
}
func SubmitArticle(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.HandlerFunc {
func SubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := s.Get(r, "cookie")
if err != nil {
@ -54,7 +91,14 @@ func SubmitArticle(c *control.CliArgs, db *model.DB, s *control.CookieStore) htt
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
article := &model.Article{
session.Values["article"] = nil
if err = session.Save(r, w); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
article := &b.Article{
Title: r.PostFormValue("article-title"),
Description: r.PostFormValue("article-description"),
Content: r.PostFormValue("article-content"),
@ -93,7 +137,7 @@ func SubmitArticle(c *control.CliArgs, db *model.DB, s *control.CookieStore) htt
}
}
func ResubmitArticle(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.HandlerFunc {
func ResubmitArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
@ -107,10 +151,10 @@ func ResubmitArticle(c *control.CliArgs, db *model.DB, s *control.CookieStore) h
content := r.PostFormValue("article-content")
if err = db.UpdateAttributes(
&model.Attribute{Table: "articles", ID: id, AttName: "title", Value: title},
&model.Attribute{Table: "articles", ID: id, AttName: "description", Value: description},
&model.Attribute{Table: "articles", ID: id, AttName: "content", Value: content},
&model.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false},
&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: "content", Value: content},
&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false},
); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -147,7 +191,7 @@ func ResubmitArticle(c *control.CliArgs, db *model.DB, s *control.CookieStore) h
}
}
func ShowUnpublishedArticles(c *control.CliArgs, db *model.DB) http.HandlerFunc {
func ShowUnpublishedArticles(c *b.Config, db *b.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
unpublishedArticles, err := db.GetCertainArticles(false, false)
if err != nil {
@ -162,11 +206,11 @@ func ShowUnpublishedArticles(c *control.CliArgs, db *model.DB) http.HandlerFunc
}
}
func ShowRejectedArticles(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.HandlerFunc {
func ShowRejectedArticles(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
type htmlData struct {
MyIDs map[int64]bool
RejectedArticles []*model.Article
RejectedArticles []*b.Article
}
data := new(htmlData)
@ -197,13 +241,13 @@ func ShowRejectedArticles(c *control.CliArgs, db *model.DB, s *control.CookieSto
}
}
func ReviewUnpublishedArticle(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.HandlerFunc {
func ReviewUnpublishedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
type htmlData struct {
Title string
Description string
Content template.HTML
Tags []*model.Tag
Tags []*b.Tag
ID int64
}
@ -224,21 +268,21 @@ func ReviewUnpublishedArticle(c *control.CliArgs, db *model.DB, s *control.Cooki
return
}
data.Title, err = control.ConvertToPlain(article.Title)
data.Title, err = b.ConvertToPlain(article.Title)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Description, err = control.ConvertToPlain(article.Description)
data.Description, err = b.ConvertToPlain(article.Description)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
content, err := control.ConvertToHTML(article.Content)
content, err := b.ConvertToHTML(article.Content)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -259,12 +303,12 @@ func ReviewUnpublishedArticle(c *control.CliArgs, db *model.DB, s *control.Cooki
}
}
func ReviewRejectedArticle(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.HandlerFunc {
func ReviewRejectedArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
type htmlData struct {
Selected map[int64]bool
Article *model.Article
Tags []*model.Tag
Article *b.Article
Tags []*b.Tag
}
data := new(htmlData)
@ -306,7 +350,7 @@ func ReviewRejectedArticle(c *control.CliArgs, db *model.DB, s *control.CookieSt
}
}
func PublishArticle(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.HandlerFunc {
func PublishArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
@ -329,22 +373,22 @@ func PublishArticle(c *control.CliArgs, db *model.DB, s *control.CookieStore) ht
}
if err = db.UpdateAttributes(
&model.Attribute{Table: "articles", ID: id, AttName: "published", Value: true},
&model.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false},
&model.Attribute{Table: "articles", ID: id, AttName: "created", Value: time.Now().Format("2006-01-02 15:04:05")},
&b.Attribute{Table: "articles", ID: id, AttName: "published", Value: true},
&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: false},
&b.Attribute{Table: "articles", ID: id, AttName: "created", Value: time.Now().Format("2006-01-02 15:04:05")},
); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
feed, err := control.GenerateRSS(db, c.Title, c.Link, c.Description)
feed, err := b.GenerateRSS(db, c.Title, c.Link, c.Description)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = control.SaveRSS(c.RSSFile, feed); err != nil {
if err = b.SaveRSS(c.RSSFile, feed); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -356,7 +400,7 @@ func PublishArticle(c *control.CliArgs, db *model.DB, s *control.CookieStore) ht
}
}
func RejectArticle(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.HandlerFunc {
func RejectArticle(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
@ -373,7 +417,7 @@ func RejectArticle(c *control.CliArgs, db *model.DB, s *control.CookieStore) htt
}
if err = db.UpdateAttributes(
&model.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: true},
&b.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: true},
); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -386,7 +430,7 @@ func RejectArticle(c *control.CliArgs, db *model.DB, s *control.CookieStore) htt
}
}
func ShowCurrentArticles(c *control.CliArgs, db *model.DB) http.HandlerFunc {
func ShowCurrentArticles(c *b.Config, db *b.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
articles, err := db.GetCurrentIssueArticles()
if err != nil {
@ -400,7 +444,7 @@ func ShowCurrentArticles(c *control.CliArgs, db *model.DB) http.HandlerFunc {
}
}
func UploadImage(c *control.CliArgs) http.HandlerFunc {
func UploadImage(c *b.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
file, header, err := r.FormFile("article-image")
if err != nil {
@ -420,6 +464,12 @@ func UploadImage(c *control.CliArgs) http.HandlerFunc {
return
}
if err = os.MkdirAll(fmt.Sprint(c.PicsDir, "/"), fs.FileMode(0755)); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
img, err := os.Create(absFilepath)
if err != nil {
log.Println(err)
@ -434,9 +484,8 @@ func UploadImage(c *control.CliArgs) http.HandlerFunc {
return
}
alt := strings.Join(nameStrings[0:len(nameStrings)-1], " ")
imgMD := fmt.Sprint("![", alt, "](", c.Domain, "/pics/", filename, ")")
tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html")
template.Must(tmpl, err).ExecuteTemplate(w, "editor-images", imgMD)
url := fmt.Sprint(c.Domain, "/pics/", filename)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(url)
}
}

View File

@ -1,21 +1,20 @@
package view
package frontend
import (
"html/template"
"net/http"
"streifling.com/jason/cpolis/cmd/control"
"streifling.com/jason/cpolis/cmd/model"
b "streifling.com/jason/cpolis/cmd/backend"
)
func CreateTag(c *control.CliArgs) http.HandlerFunc {
func CreateTag(c *b.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-tag.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil)
}
}
func AddTag(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.HandlerFunc {
func AddTag(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
db.AddTag(r.PostFormValue("tag"))

View File

@ -1,14 +1,14 @@
package view
package frontend
import (
"log"
"net/http"
"path/filepath"
"streifling.com/jason/cpolis/cmd/control"
b "streifling.com/jason/cpolis/cmd/backend"
)
func ServeImage(c *control.CliArgs, s *control.CookieStore) http.HandlerFunc {
func ServeImage(c *b.Config, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
absFilepath, err := filepath.Abs(c.PicsDir)
if err != nil {

View File

@ -1,15 +1,14 @@
package view
package frontend
import (
"html/template"
"log"
"net/http"
"streifling.com/jason/cpolis/cmd/control"
"streifling.com/jason/cpolis/cmd/model"
b "streifling.com/jason/cpolis/cmd/backend"
)
func PublishLatestIssue(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.HandlerFunc {
func PublishLatestIssue(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := db.PublishLatestIssue(); err != nil {
log.Println(err)

View File

@ -1,4 +1,4 @@
package view
package frontend
import (
"fmt"
@ -6,11 +6,10 @@ import (
"log"
"net/http"
"streifling.com/jason/cpolis/cmd/control"
"streifling.com/jason/cpolis/cmd/model"
b "streifling.com/jason/cpolis/cmd/backend"
)
func saveSession(w http.ResponseWriter, r *http.Request, s *control.CookieStore, u *model.User) error {
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)
@ -27,7 +26,7 @@ func saveSession(w http.ResponseWriter, r *http.Request, s *control.CookieStore,
return nil
}
func HomePage(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.HandlerFunc {
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 {
@ -54,7 +53,7 @@ func HomePage(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.Han
}
}
func Login(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.HandlerFunc {
func Login(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userName := r.PostFormValue("username")
password := r.PostFormValue("password")
@ -89,7 +88,7 @@ func Login(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.Handle
}
}
func Logout(c *control.CliArgs, s *control.CookieStore) http.HandlerFunc {
func Logout(c *b.Config, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := s.Get(r, "cookie")
if err != nil {

View File

@ -1,4 +1,4 @@
package view
package frontend
import (
"fmt"
@ -7,16 +7,15 @@ import (
"net/http"
"strconv"
"streifling.com/jason/cpolis/cmd/control"
"streifling.com/jason/cpolis/cmd/model"
b "streifling.com/jason/cpolis/cmd/backend"
)
type UserData struct {
*model.User
*b.User
Msg string
}
func checkUserStrings(user *model.User) (string, int, bool) {
func checkUserStrings(user *b.User) (string, int, bool) {
userLen := 15
nameLen := 50
@ -31,14 +30,14 @@ func checkUserStrings(user *model.User) (string, int, bool) {
}
}
func CreateUser(c *control.CliArgs) http.HandlerFunc {
func CreateUser(c *b.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil)
}
}
func AddUser(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.HandlerFunc {
func AddUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
role, err := strconv.Atoi(r.PostFormValue("role"))
if err != nil {
@ -48,7 +47,7 @@ func AddUser(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.Hand
}
htmlData := UserData{
User: &model.User{
User: &b.User{
UserName: r.PostFormValue("username"),
FirstName: r.PostFormValue("first-name"),
LastName: r.PostFormValue("last-name"),
@ -95,12 +94,20 @@ func AddUser(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.Hand
return
}
session, err := s.Get(r, "cookie")
if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", 0)
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int))
}
}
func EditUser(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.HandlerFunc {
func EditSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := s.Get(r, "cookie")
if err != nil {
@ -116,12 +123,12 @@ func EditUser(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.Han
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html")
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-self.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", user)
}
}
func UpdateUser(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.HandlerFunc {
func UpdateSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := s.Get(r, "cookie")
if err != nil {
@ -131,7 +138,7 @@ func UpdateUser(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.H
}
userData := UserData{
User: &model.User{
User: &b.User{
ID: session.Values["id"].(int64),
UserName: r.PostFormValue("username"),
FirstName: r.PostFormValue("first-name"),
@ -172,7 +179,7 @@ func UpdateUser(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.H
}
}
if err = db.UpdateUserAttributes(
if err = db.UpdateOwnAttributes(
userData.ID,
userData.UserName,
userData.FirstName,
@ -191,16 +198,16 @@ func UpdateUser(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.H
}
}
func AddFirstUser(c *control.CliArgs, db *model.DB, s *control.CookieStore) http.HandlerFunc {
func AddFirstUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var err error
htmlData := UserData{
User: &model.User{
User: &b.User{
UserName: r.PostFormValue("username"),
FirstName: r.PostFormValue("first-name"),
LastName: r.PostFormValue("last-name"),
Role: model.Admin,
Role: b.Admin,
},
}
pass := r.PostFormValue("password")
@ -265,3 +272,165 @@ func AddFirstUser(c *control.CliArgs, db *model.DB, s *control.CookieStore) http
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", 0)
}
}
func ShowAllUsers(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var err error
type htmlData struct {
Users map[int64]*b.User
Action string
}
data := &htmlData{Action: action}
data.Users, err = db.GetAllUsers()
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session, err := s.Get(r, "cookie")
if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
delete(data.Users, session.Values["id"].(int64))
tmpl, err := template.ParseFiles(c.WebDir + "/templates/show-all-users.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", data)
}
}
func EditUser(c *b.Config, db *b.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
user, err := db.GetUser(id)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", user)
}
}
func UpdateUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
role, err := strconv.Atoi(r.PostFormValue("role"))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
userData := UserData{
User: &b.User{
ID: id,
UserName: r.PostFormValue("username"),
FirstName: r.PostFormValue("first-name"),
LastName: r.PostFormValue("last-name"),
Role: role,
},
}
newPass := r.PostFormValue("password")
newPass2 := r.PostFormValue("password2")
if len(userData.UserName) == 0 || len(userData.FirstName) == 0 ||
len(userData.LastName) == 0 {
userData.Msg = "Alle Felder mit * müssen ausgefüllt sein."
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", userData.Msg)
return
}
userString, stringLen, ok := checkUserStrings(userData.User)
if !ok {
userData.Msg = fmt.Sprint(userString, " ist zu lang. Maximal ",
stringLen, " Zeichen erlaubt.")
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", userData)
return
}
if id, ok := db.GetID(userData.UserName); ok {
if id != userData.ID {
userData.Msg = "Benutzername bereits vergeben."
userData.UserName = ""
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", userData)
return
}
}
if err = db.UpdateUserAttributes(
userData.ID,
userData.UserName,
userData.FirstName,
userData.LastName,
newPass,
newPass2,
userData.Role); err != nil {
userData.Msg = "Aktualisierung der Benutzerdaten fehlgeschlagen."
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", userData)
}
session, err := s.Get(r, "cookie")
if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int))
}
}
func DeleteUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = db.DeleteUser(id); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
session, err := s.Get(r, "cookie")
if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
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

@ -6,76 +6,78 @@ import (
"net/http"
"os"
"streifling.com/jason/cpolis/cmd/control"
"streifling.com/jason/cpolis/cmd/model"
"streifling.com/jason/cpolis/cmd/view"
b "streifling.com/jason/cpolis/cmd/backend"
f "streifling.com/jason/cpolis/cmd/frontend"
)
func init() {
gob.Register(model.User{})
gob.Register(b.User{})
}
func main() {
args, err := control.HandleCliArgs()
config, err := b.HandleConfig()
if err != nil {
log.Fatalln(err)
}
logFile, err := os.OpenFile(args.LogFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
logFile, err := os.OpenFile(config.LogFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
log.Fatalln(err)
}
defer logFile.Close()
log.SetOutput(logFile)
db, err := model.OpenDB(args.DBName)
db, err := b.OpenDB(config.DBName)
if err != nil {
log.Fatalln(err)
}
defer db.Close()
key, err := control.LoadKey(args.KeyFile)
key, err := b.LoadKey(config.KeyFile)
if err != nil {
key, err = control.NewKey()
key, err = b.NewKey()
if err != nil {
log.Fatalln(err)
}
control.SaveKey(key, args.KeyFile)
b.SaveKey(key, config.KeyFile)
}
store := control.NewCookieStore(key)
store := b.NewCookieStore(key)
mux := http.NewServeMux()
mux.Handle("/web/static/", http.StripPrefix("/web/static/",
http.FileServer(http.Dir(args.WebDir+"/static/"))))
mux.HandleFunc("/", view.HomePage(args, db, store))
http.FileServer(http.Dir(config.WebDir+"/static/"))))
mux.HandleFunc("/", f.HomePage(config, db, store))
mux.HandleFunc("GET /create-tag", view.CreateTag(args))
mux.HandleFunc("GET /create-user", view.CreateUser(args))
mux.HandleFunc("GET /edit-user", view.EditUser(args, db, store))
mux.HandleFunc("GET /hub", view.ShowHub(args, db, store))
mux.HandleFunc("GET /logout", view.Logout(args, store))
mux.HandleFunc("GET /publish-article/{id}", view.PublishArticle(args, db, store))
mux.HandleFunc("GET /publish-issue", view.PublishLatestIssue(args, db, store))
mux.HandleFunc("GET /reject-article/{id}", view.RejectArticle(args, db, store))
mux.HandleFunc("GET /rejected-articles", view.ShowRejectedArticles(args, db, store))
mux.HandleFunc("GET /review-rejected-article/{id}", view.ReviewRejectedArticle(args, db, store))
mux.HandleFunc("GET /review-unpublished-article/{id}", view.ReviewUnpublishedArticle(args, db, store))
mux.HandleFunc("GET /rss", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, args.RSSFile)
})
mux.HandleFunc("GET /pics/{pic}", view.ServeImage(args, store))
mux.HandleFunc("GET /this-issue", view.ShowCurrentArticles(args, db))
mux.HandleFunc("GET /unpublished-articles", view.ShowUnpublishedArticles(args, db))
mux.HandleFunc("GET /write-article", view.WriteArticle(args, db))
mux.HandleFunc("GET /create-tag", f.CreateTag(config))
mux.HandleFunc("GET /create-user", f.CreateUser(config))
mux.HandleFunc("GET /edit-self", f.EditSelf(config, db, store))
mux.HandleFunc("GET /edit-user/{id}", f.EditUser(config, db))
mux.HandleFunc("GET /delete-user/{id}", f.DeleteUser(config, db, store))
mux.HandleFunc("GET /hub", f.ShowHub(config, db, store))
mux.HandleFunc("GET /logout", f.Logout(config, store))
mux.HandleFunc("GET /pics/{pic}", f.ServeImage(config, store))
mux.HandleFunc("GET /publish-article/{id}", f.PublishArticle(config, db, store))
mux.HandleFunc("GET /publish-issue", f.PublishLatestIssue(config, db, store))
mux.HandleFunc("GET /reject-article/{id}", f.RejectArticle(config, db, store))
mux.HandleFunc("GET /rejected-articles", f.ShowRejectedArticles(config, db, store))
mux.HandleFunc("GET /review-rejected-article/{id}", f.ReviewRejectedArticle(config, db, store))
mux.HandleFunc("GET /review-unpublished-article/{id}", f.ReviewUnpublishedArticle(config, db, store))
mux.HandleFunc("GET /rss", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, config.RSSFile) })
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))
mux.HandleFunc("GET /unpublished-articles", f.ShowUnpublishedArticles(config, db))
mux.HandleFunc("GET /write-article", f.WriteArticle(config, db, store))
mux.HandleFunc("POST /add-first-user", view.AddFirstUser(args, db, store))
mux.HandleFunc("POST /add-tag", view.AddTag(args, db, store))
mux.HandleFunc("POST /add-user", view.AddUser(args, db, store))
mux.HandleFunc("POST /login", view.Login(args, db, store))
mux.HandleFunc("POST /resubmit-article/{id}", view.ResubmitArticle(args, db, store))
mux.HandleFunc("POST /submit-article", view.SubmitArticle(args, db, store))
mux.HandleFunc("POST /update-user", view.UpdateUser(args, db, store))
mux.HandleFunc("POST /upload-image", view.UploadImage(args))
mux.HandleFunc("POST /add-first-user", f.AddFirstUser(config, db, store))
mux.HandleFunc("POST /add-tag", f.AddTag(config, db, store))
mux.HandleFunc("POST /add-user", f.AddUser(config, db, store))
mux.HandleFunc("POST /login", f.Login(config, db, store))
mux.HandleFunc("POST /resubmit-article/{id}", f.ResubmitArticle(config, db, store))
mux.HandleFunc("POST /submit-article", f.SubmitArticle(config, db, store))
mux.HandleFunc("POST /update-self", f.UpdateSelf(config, db, store))
mux.HandleFunc("POST /update-user/{id}", f.UpdateUser(config, db, store))
mux.HandleFunc("POST /upload-image", f.UploadImage(config))
log.Fatalln(http.ListenAndServe(args.Port, mux))
log.Fatalln(http.ListenAndServe(config.Port, mux))
}

1
go.mod
View File

@ -4,6 +4,7 @@ go 1.22.0
require (
git.streifling.com/jason/rss v0.1.2
github.com/BurntSushi/toml v1.3.2
github.com/go-sql-driver/mysql v1.7.1
github.com/google/uuid v1.6.0
github.com/gorilla/sessions v1.2.2

2
go.sum
View File

@ -1,5 +1,7 @@
git.streifling.com/jason/rss v0.1.2 h1:UB3UHJXMt5WDDh9y8n0Z6nS1XortbPXjEr7QZTdovY4=
git.streifling.com/jason/rss v0.1.2/go.mod h1:gpZF0nZbQSstMpyHD9DTAvlQEG7v4pjO5c7aIMWM4Jg=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=

File diff suppressed because one or more lines are too long

View File

@ -40,7 +40,7 @@
</div>
<div>
<input required id="admin" name="role" type="radio" value="0" {{if eq .Role 0 }}checked{{end}} />
<label for="admin">Admin</label>
<label for="admin">Administrator</label>
</div>
</div>

View File

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

View File

@ -1,5 +1,5 @@
{{define "page-content"}}
<h2>Benutzerdaten bearbeiten</h2>
<h2>Profil von {{.FirstName}} {{.LastName}} bearbeiten</h2>
<form>
<div class="grid grid-cols-3 gap-4">
@ -7,30 +7,49 @@
<label for="username">Benutzername</label>
<input class="w-full" name="username" type="text" value="{{.UserName}}" />
</div>
<div>
<label for="first-name">Vorname</label>
<input class="w-full" name="first-name" type="text" value="{{.FirstName}}" />
</div>
<div>
<label for="last-name">Nachname</label>
<input class="w-full" name="last-name" type="text" value="{{.LastName}}" />
</div>
<div>
<label for="old-password">Altes Passwort</label>
<input class="w-full" name="old-password" placeholder="***" type="password" />
</div>
<div>
<label for="password">Passwort</label>
<input class="w-full" name="password" placeholder="***" type="password" />
</div>
<div>
<label for="password2">Passwort wiederholen</label>
<input class="w-full" name="password2" placeholder="***" type="password" />
</div>
</div>
<div class="flex gap-4">
<div>
<input required id="author" name="role" type="radio" value="3" {{if eq .Role 3 }}checked{{end}} />
<label for="author">Autor</label>
</div>
<div>
<input required id="editor" name="role" type="radio" value="2" {{if eq .Role 2 }}checked{{end}} />
<label for="editor">Redakteur</label>
</div>
<div>
<input required id="publisher" name="role" type="radio" value="1" {{if eq .Role 1 }}checked{{end}} />
<label for="publisher">Herausgeber</label>
</div>
<div>
<input required id="admin" name="role" type="radio" value="0" {{if eq .Role 0 }}checked{{end}} />
<label for="admin">Administrator</label>
</div>
</div>
<div class="btn-area">
<input class="action-btn" type="submit" value="Aktualisieren" hx-post="/update-user"
<input class="action-btn" type="submit" value="Aktualisieren" hx-post="/update-user/{{.ID}}"
hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
</div>

View File

@ -1,24 +1,25 @@
{{define "page-content"}}
<h2>Editor</h2>
<form>
<form id="edit-area">
<div class="flex flex-col gap-y-1">
<label for="article-title">Titel</label>
<input name="article-title" type="text" />
<input name="article-title" type="text" value="{{.Title}}" />
</div>
<div class="flex flex-col">
<label for="article-description">Beschreibung</label>
<textarea name="article-description"></textarea>
<textarea name="article-description">{{.Description}}</textarea>
</div>
<div class="flex flex-col">
<label for="article-content">Artikel</label>
<textarea name="article-content"></textarea>
<textarea id="easyMDE">{{.Content}}</textarea>
</div>
<input id="article-content" name="article-content" type="hidden" />
<div>
<span>Tags</span>
<div class="flex flex-wrap gap-x-4">
{{range .}}
{{range .Tags}}
<div>
<input id="{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" />
<label for="{{.Name}}">{{.Name}}</label>
@ -27,11 +28,6 @@
</div>
</div>
<div id="editor-images">
<input class="mb-2" name="article-image" type="file" hx-encoding="multipart/form-data" hx-post="/upload-image"
hx-swap="beforeend" hx-target="#editor-images" />
</div>
<div class="btn-area">
<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>
@ -39,32 +35,33 @@
</form>
<script>
function copyToClipboard(text) {
event.preventDefault(); // Get-Request verhindern
var easyMDE = new EasyMDE({
element: document.getElementById('easyMDE'),
hideIcons: ['image'],
imageTexts: {sbInit: ''},
showIcons: ["code", "table", "upload-image"],
uploadImage: true,
var textarea = document.createElement("textarea");
textarea.textContent = text;
document.body.appendChild(textarea);
imageUploadFunction: function (file, onSuccess, onError) {
var formData = new FormData();
formData.append('article-image', file);
textarea.select();
try {
document.execCommand('copy');
} catch (err) {
console.warn('Fehler beim Kopieren', err);
}
document.body.removeChild(textarea);
}
fetch('/upload-image', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
onSuccess(data);
})
.catch(error => {
onError(error);
});
},
});
easyMDE.codemirror.on("change", () => {
document.getElementById('article-content').value = easyMDE.value();
});
</script>
{{end}}
{{define "editor-images"}}
{{if gt (len .) 0}}
<div class="border px-2 py-1 rounded-md flex gap-4 justify-between">
<div class="self-center">{{.}}</div>
<button class="bg-slate-50 border my-2 px-3 py-2 rounded-md w-32 hover:bg-slate-100"
onclick="copyToClipboard('{{.}}')">Kopieren</button>
</div>
{{end}}
{{end}}

View File

@ -2,15 +2,17 @@
<div class="flex flex-col gap-4">
<button class="btn" hx-get="/logout" hx-target="#page-content">Abmelden</button>
{{if lt . 4}}
<div class="mb-3">
<h2>Autor</h2>
<div class="grid grid-cols-2 gap-x-4 gap-y-2">
<button class="btn" hx-get="/write-article" hx-target="#page-content">Artikel schreiben</button>
<button class="btn" hx-get="/rejected-articles" hx-target="#page-content">Abgelehnte Artikel</button>
<a class="btn text-center" href="/rss">RSS Feed</a>
<button class="btn" hx-get="/edit-user" hx-target="#page-content">Benutzer bearbeiten</button>
<button class="btn" hx-get="/edit-self" hx-target="#page-content">Profil bearbeiten</button>
</div>
</div>
{{end}}
{{if lt . 3}}
<div class="mb-3">
@ -36,8 +38,10 @@
{{if eq . 0}}
<div class="mb-3">
<h2>Administrator</h2>
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-3 gap-4">
<button class="btn" hx-get="/create-user" hx-target="#page-content">Benutzer hinzufügen</button>
<button class="btn" hx-get="/show-all-users-edit" hx-target="#page-content">Benutzer bearbeiten</button>
<button class="btn" hx-get="/show-all-users-delete" hx-target="#page-content">Benutzer löschen</button>
</div>
</div>
{{end}}

View File

@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Orient Editor</title>
<link href="/web/static/css/style.css" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/easymde/dist/easymde.min.css">
</head>
<body class="flex flex-col justify-between min-h-screen bg-slate-50">
@ -25,7 +26,8 @@
</p>
</footer>
<script src="/web/static/js/htmx.min.js"></script>
<script src="https://unpkg.com/htmx.org@2.0.1"></script>
<script src="https://unpkg.com/easymde/dist/easymde.min.js"></script>
</body>
</html>

View File

@ -14,6 +14,7 @@
<label for="article-content">Artikel</label>
<textarea name="article-content" placeholder="Artikel">{{.Article.Content}}</textarea>
</div>
<input id="article-content" name="article-content" type="hidden" />
<div>
<span>Tags</span>
@ -28,11 +29,6 @@
</div>
</div>
<div id="editor-images">
<input class="mb-2" name="article-image" type="file" hx-encoding="multipart/form-data" hx-post="/upload-image"
hx-swap="beforeend" hx-target="#editor-images" />
</div>
<div class="btn-area">
<input class="action-btn" type="submit" value="Senden" hx-post="/resubmit-article/{{.Article.ID}}"
hx-target="#page-content" />
@ -41,32 +37,33 @@
</form>
<script>
function copyToClipboard(text) {
event.preventDefault(); // Get-Request verhindern
var easyMDE = new EasyMDE({
element: document.getElementById('easyMDE'),
hideIcons: ['image'],
imageTexts: {sbInit: ''},
showIcons: ["code", "table", "upload-image"],
uploadImage: true,
var textarea = document.createElement("textarea");
textarea.textContent = text;
document.body.appendChild(textarea);
imageUploadFunction: function (file, onSuccess, onError) {
var formData = new FormData();
formData.append('article-image', file);
textarea.select();
try {
document.execCommand('copy');
} catch (err) {
console.warn('Fehler beim Kopieren', err);
}
document.body.removeChild(textarea);
}
fetch('/upload-image', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
onSuccess(data);
})
.catch(error => {
onError(error);
});
},
});
easyMDE.codemirror.on("change", () => {
document.getElementById('article-content').value = easyMDE.value();
});
</script>
{{end}}
{{define "editor-images"}}
{{if gt (len .) 0}}
<div class="border px-2 py-1 rounded-md flex gap-4 justify-between">
<div class="self-center">{{.}}</div>
<button class="bg-slate-50 border my-2 px-3 py-2 rounded-md w-32 hover:bg-slate-100"
onclick="copyToClipboard('{{.}}')">Kopieren</button>
</div>
{{end}}
{{end}}

View File

@ -0,0 +1,24 @@
{{define "page-content"}}
<h2>Alle Benutzer</h2>
<div class="flex flex-col gap-4">
{{range .Users}}
<button class="btn" hx-get="/{{$.Action}}/{{.ID}}" hx-target="#page-content">
<h1 class="font-bold text-2xl">
{{.UserName}}
({{if eq .Role 0}}
Administrator
{{else if eq .Role 1}}
Herausgeber
{{else if eq .Role 2}}
Redakteur
{{else}}
Autor
{{end}})
</h1>
<p>{{.FirstName}} {{.LastName}}</p>
</button>
{{end}}
<button class="action-btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
</div>
{{end}}