Compare commits

..

25 Commits

Author SHA1 Message Date
0e768c9f61 Merge branch 'devel' 2024-08-08 21:27:07 +02:00
ce43e45a6c Deleted hints of custom preview function 2024-08-08 21:26:59 +02:00
1fcd775cc5 Merge branch 'devel' 2024-08-08 21:14:24 +02:00
203a1ed147 Implemented EasyMDE 2024-08-08 21:13:25 +02:00
ef1914ee5c Implemented article preview 2024-08-08 21:13:25 +02:00
084b101e31 Register f.ArticlePreviewHtmlData in init() 2024-08-08 21:13:25 +02:00
b2db128aa9 Shorten lines by referencing frontend as f and backend as b 2024-08-08 21:13:25 +02:00
081e880fb6 Change structure of code tor frontend and backend one 2024-08-08 21:13:25 +02:00
32f11f57b5 Implemented EasyMDE 2024-08-08 21:09:38 +02:00
46a0cec6df Implemented article preview 2024-07-17 23:25:57 +02:00
4e0bce37a2 Register f.ArticlePreviewHtmlData in init() 2024-07-13 14:17:40 +02:00
85e2f8b4ad Shorten lines by referencing frontend as f and backend as b 2024-07-13 14:09:11 +02:00
d0c1e525d2 Change structure of code tor frontend and backend one 2024-07-13 13:58:36 +02:00
21fd3403b2 Added ability to delete other users 2024-04-12 08:46:34 +02:00
0f0471b84c Read config file in addition to cli arguments 2024-04-12 07:17:13 +02:00
4b90ec9652 Let admins edit other user's profiles 2024-04-09 19:06:29 +02:00
783d59805b Fixed small bug 2024-04-07 19:33:51 +02:00
b5f0fe8985 Added title, link and description to cli args. 2024-04-07 19:29:35 +02:00
d9bf79d5f8 Add space between first and last name 2024-04-07 19:01:21 +02:00
f98ab149a2 Added headings to all templates 2024-04-07 18:57:03 +02:00
822ca2b8ab Use UUID as filename and strip alt off of extension 2024-04-07 18:43:09 +02:00
af65180893 Reflect new cli arg in .air.toml 2024-04-07 11:36:34 +02:00
5615210be5 Add cli arg for domain 2024-04-07 11:32:38 +02:00
b88fb1643c Fixed bug 2024-04-07 11:12:07 +02:00
92189a4a51 Pictures are now handeled correctly 2024-04-07 10:58:07 +02:00
35 changed files with 841 additions and 295 deletions

View File

@ -3,42 +3,52 @@ testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main -key tmp/key.gob -log tmp/cpolis.log -pics tmp/pics -rss tmp/orientexpress_alle.rss -web web"
cmd = "go build -o ./tmp/main ./cmd/main.go"
delay = 0
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html", "css"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
args_bin = [
"-desc 'Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität'",
"-domain localhost",
"-key tmp/key.gob",
"-link https://distrikt-ni-st.de",
"-log tmp/cpolis.log",
"-pics tmp/pics",
"-rss tmp/orientexpress_alle.rss",
"-title 'Freimaurer Distrikt Niedersachsen und Sachsen-Anhalt'",
"-web web"
]
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ./cmd/main.go"
delay = 0
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html", "css"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
main_only = false
time = false
[misc]
clean_on_exit = false
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true
clear_on_rebuild = false
keep_scroll = true

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,
@ -97,7 +96,7 @@ func GenerateRSS(db *model.DB, title, link, desc string) (*string, error) {
channel.Items = append(channel.Items, &rss.Item{
Title: articleTitle,
Author: user.FirstName + user.LastName,
Author: user.FirstName + " " + user.LastName,
PubDate: article.Created.Format(time.RFC1123Z),
Description: articleDescription,
Content: &rss.Content{Value: articleContent},
@ -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,60 +0,0 @@
package control
import (
"flag"
"fmt"
"path/filepath"
)
type CliArgs struct {
DBName string
KeyFile string
LogFile string
Port string
PicsDir string
RSSFile string
WebDir string
}
func HandleCliArgs() (*CliArgs, error) {
var err error
cliArgs := new(CliArgs)
keyFile := flag.String("key", "/var/www/cpolis/cpolis.key", "key file")
logFile := flag.String("log", "/var/log/cpolis.log", "log file")
picsDir := flag.String("pics", "/var/www/cpolis/pics", "pictures directory")
port := flag.Int("port", 8080, "port")
rssFile := flag.String("rss", "/var/www/cpolis/cpolis.rss", "RSS file")
webDir := flag.String("web", "/var/www/cpolis/web", "web directory")
flag.StringVar(&cliArgs.DBName, "db", "cpolis", "DB name")
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)
}
cliArgs.PicsDir, err = filepath.Abs(*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,20 +1,29 @@
package view
package frontend
import (
"encoding/json"
"fmt"
"html/template"
"io"
"io/fs"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"streifling.com/jason/cpolis/cmd/control"
"streifling.com/jason/cpolis/cmd/model"
"github.com/google/uuid"
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 {
@ -23,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)
@ -38,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 {
@ -51,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"),
@ -90,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 {
@ -104,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)
@ -144,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 {
@ -159,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)
@ -194,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
}
@ -221,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)
@ -256,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)
@ -303,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 {
@ -326,27 +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,
"Freimaurer Distrikt Niedersachsen und Sachsen-Anhalt",
"https://distrikt-ni-st.de",
"Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität",
)
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
@ -358,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 {
@ -375,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)
@ -388,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 {
@ -402,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 {
@ -412,8 +454,23 @@ func UploadImage(c *control.CliArgs) http.HandlerFunc {
}
defer file.Close()
filename := fmt.Sprint(c.PicsDir, time.Now().Format("2006-01-02_15:04:05"), "-", header.Filename)
img, err := os.Create(filename)
nameStrings := strings.Split(header.Filename, ".")
extension := "." + nameStrings[len(nameStrings)-1]
filename := fmt.Sprint(uuid.New(), extension)
absFilepath, err := filepath.Abs(fmt.Sprint(c.PicsDir, "/", filename))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err = os.MkdirAll(fmt.Sprint(c.PicsDir, "/"), fs.FileMode(0755)); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
img, err := os.Create(absFilepath)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -427,7 +484,8 @@ func UploadImage(c *control.CliArgs) http.HandlerFunc {
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html")
template.Must(tmpl, err).ExecuteTemplate(w, "editor-images", fmt.Sprint("![", header.Filename, "](", filename, ")"))
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"))

22
cmd/frontend/images.go Normal file
View File

@ -0,0 +1,22 @@
package frontend
import (
"log"
"net/http"
"path/filepath"
b "streifling.com/jason/cpolis/cmd/backend"
)
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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.ServeFile(w, r, absFilepath+"/"+r.PathValue("pic"))
}
}

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,73 +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 /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))
}

2
go.mod
View File

@ -4,7 +4,9 @@ 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
github.com/microcosm-cc/bluemonday v1.0.26
github.com/yuin/goldmark v1.7.0

4
go.sum
View File

@ -1,11 +1,15 @@
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=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=

View File

@ -1,5 +1,6 @@
{{define "page-content"}}
<h2>Neuer Benutzer</h2>
<form>
<div class="grid grid-cols-3 gap-4">
<div>
@ -39,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

@ -1,4 +1,6 @@
{{define "page-content"}}
<h2>Aktuelle Artikel</h2>
<div class="flex flex-col gap-4">
{{range .}}
<div class="border px-2 py-1 rounded-md">

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,34 +1,55 @@
{{define "page-content"}}
<h2>Profil von {{.FirstName}} {{.LastName}} 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="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,23 +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>
@ -26,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>
@ -38,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

@ -1,3 +0,0 @@
{{define "page-content"}}
{{.}}
{{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

@ -1,5 +1,6 @@
{{define "page-content"}}
<h2>Anmeldung</h2>
<form>
<div class="btn-area">
<input class="w-full" name="username" placeholder="Benutzername" type="text" />

View File

@ -1,4 +1,6 @@
{{define "page-content"}}
<h2>Abgelehnte Artikel</h2>
<div class="flex flex-col gap-4">
{{range .RejectedArticles}}
{{if index $.MyIDs .ID}}

View File

@ -1,5 +1,6 @@
{{define "page-content"}}
<h2>Editor</h2>
<form>
<div class="flex flex-col gap-y-1">
<label for="article-title">Titel</label>
@ -13,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>
@ -27,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" />
@ -40,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}}

View File

@ -1,4 +1,6 @@
{{define "page-content"}}
<h2>Artikel veröffentlichen</h2>
<div>
<span>Titel</span>
<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">

View File

@ -1,4 +1,6 @@
{{define "page-content"}}
<h2>Unveröffentlichte Artikel</h2>
<div class="flex flex-col gap-4">
{{range .}}
<button class="btn" hx-get="/review-unpublished-article/{{.ID}}" hx-target="#page-content">