Compare commits

..

57 Commits

Author SHA1 Message Date
548d2c6023 Incorporated issues 2024-03-28 07:00:37 +01:00
509e1d41db Corrected copyright 2024-03-28 06:59:39 +01:00
63d5fbd127 Disabled option to do transaction from view 2024-03-28 06:58:59 +01:00
f5c1a5e8d0 Added copyright 2024-03-17 15:29:12 +01:00
40d1104a66 Set pubDate to published time and date 2024-03-17 09:41:09 +01:00
736673c33f Now everyone only sees their own rejected articles 2024-03-17 09:15:37 +01:00
1a2e18359c Added ability to view tags when rejecting and change tags when reworking articles 2024-03-17 08:46:49 +01:00
d1d3a81565 Implemented retry logic on all transactions 2024-03-15 18:37:24 +01:00
bc2302d31f Implement retry logic for UpdateAttributes 2024-03-15 15:18:02 +01:00
1b02e3fad2 Added logout 2024-03-12 20:27:39 +01:00
dc06a6e9aa Fixed dumb routing mistake 2024-03-12 19:56:22 +01:00
63370ada01 Added ability to edit user info 2024-03-11 21:08:27 +01:00
5145e1cd94 Added ability to reject and rework article 2024-03-10 15:03:46 +01:00
28eb7e236a Split up db.go into multiple files 2024-03-09 11:06:03 +01:00
4a7883f886 Also missed rss.go 2024-03-09 10:27:55 +01:00
b69a0688ef Missed main when converting to MVC 2024-03-09 10:27:04 +01:00
0e18c7a7db Changed everything to MVC 2024-03-09 10:25:20 +01:00
3ab9a8fe10 Converted RSS feed to be DB based 2024-03-09 10:12:46 +01:00
6dfd49a5cd Reachieve basic functionality 2024-03-07 20:11:28 +01:00
5455bdb12b Articles and tags are now inserted into DB correctly 2024-03-07 15:31:00 +01:00
9eaafdc712 Converted articles and tags to DB base 2024-03-06 20:53:17 +01:00
d127be4c93 Handle rollbackError with log.Fatalf() 2024-03-06 16:58:41 +01:00
34832e397f Corrected transaction for ChangePassword() 2024-03-06 16:51:08 +01:00
2c0b4878f4 Use transaction when necessary 2024-03-06 15:37:59 +01:00
f0e078c011 Load *ArticleList, *Taglist and *Channel correctly 2024-03-06 15:37:42 +01:00
a8fc28af36 Moved main.go into cmd 2024-03-05 18:26:50 +01:00
e4f085f762 Added Tags to RSS feed as categories 2024-03-05 18:20:34 +01:00
6136f83dfc Converted RSS package to git.streifling.com/jason/rss 2024-03-05 17:13:59 +01:00
21170c9cc2 A bit of cleaning up 2024-03-05 16:38:18 +01:00
6502aa7ec1 Added partial support for tags 2024-03-03 13:56:49 +01:00
e5bdc235b6 Initial sessions implementation 2024-03-03 09:16:49 +01:00
655992c8b2 Just a bit of cleaning up 2024-03-02 09:09:55 +01:00
aa7fcd6075 Created func for minimum spec for rss and article structs, thereby crushing an annoying bug that was caused by not initializing channels but waiting for messages to go through them 2024-03-02 00:28:42 +01:00
f9cc90a948 Changed articles and rss to channels 2024-03-01 21:01:38 +01:00
7f2433c30b Implemented proper User struct 2024-03-01 12:25:53 +01:00
f34efc95dd Added ability to publish articles 2024-03-01 11:30:31 +01:00
935d0a1ca4 Added article list for written but non-published articles 2024-02-27 14:10:27 +01:00
48d4d482b2 Convert title and description to plain text 2024-02-27 09:03:21 +01:00
8dae3ca21e Implemented hub 2024-02-24 15:31:33 +01:00
6f02852212 Add ability to display feed 2024-02-24 14:49:29 +01:00
4cc2110c4b Added messages and field memory for adding user 2024-02-24 13:25:32 +01:00
04cbee097c Require all fields to be filled out when creating a new user 2024-02-24 12:10:34 +01:00
93423ae606 Implemented logging to file 2024-02-24 11:41:01 +01:00
41113b24a8 Check if user already exists and bug fix 2024-02-24 10:56:12 +01:00
2247f316a3 Added ability to add user 2024-02-24 10:28:12 +01:00
9beedf9b2b Added ability to login 2024-02-24 09:54:25 +01:00
7d6f96a185 Check user credentials before adding user 2024-02-22 20:12:09 +01:00
8d47146a7c Added ability to update Passwords 2024-02-22 19:27:41 +01:00
4853184ba1 Added ability to add user 2024-02-22 18:49:51 +01:00
50895249df Changed error messages 2024-02-22 15:23:29 +01:00
6e91253908 Added HTML sanitizer 2024-02-22 15:22:45 +01:00
9bb6010319 Added initial support for MySQL databases 2024-02-18 16:37:13 +01:00
75a0af055c Handle misssed errors for encoding and decoding feeds 2024-02-18 14:31:28 +01:00
171a0dd250 Added description and a way to save and restore the RSS feed. 2024-02-18 14:01:06 +01:00
372882a252 Create RSS from HTML 2024-02-18 12:41:49 +01:00
2d0b53a254 Show HTML on website 2024-02-18 10:48:37 +01:00
2447f50bac First implementation of web based editor to HTML pipeline 2024-02-18 10:07:49 +01:00
38 changed files with 354 additions and 1567 deletions

View File

@ -1,54 +0,0 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
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"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true

1
.gitignore vendored
View File

@ -23,4 +23,3 @@ go.work
# Custom stuff
tmp/
style.css

View File

@ -1,131 +0,0 @@
package control
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

@ -2,8 +2,6 @@ package control
import (
"fmt"
"io"
"os"
"time"
"git.streifling.com/jason/rss"
@ -50,84 +48,3 @@ 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) {
channel := &rss.Channel{
Title: title,
Link: link,
Description: desc,
Items: make([]*rss.Item, 0),
}
articles, err := db.GetCertainArticles(true, false)
if err != nil {
return nil, fmt.Errorf("error getting published articles for RSS feed: %v", err)
}
for _, article := range articles {
tags, err := db.GetArticleTags(article.ID)
if err != nil {
return nil, fmt.Errorf("error getting tags for articles for RSS feed: %v", err)
}
tagNames := make([]string, 0)
for _, tag := range tags {
tagNames = append(tagNames, tag.Name)
}
tagNames = append(tagNames, fmt.Sprint("Orient Express ", article.IssueID))
user, err := db.GetUser(article.AuthorID)
if err != nil {
return nil, fmt.Errorf("error getting user user info for RSS feed: %v", err)
}
articleTitle, err := ConvertToPlain(article.Title)
if err != nil {
return nil, fmt.Errorf("error converting title to plain text for RSS feed: %v", err)
}
articleDescription, err := ConvertToPlain(article.Description)
if err != nil {
return nil, fmt.Errorf("error converting description to plain text for RSS feed: %v", err)
}
articleContent, err := ConvertToHTML(article.Content)
if err != nil {
return nil, fmt.Errorf("error converting content to HTML for RSS feed: %v", err)
}
channel.Items = append(channel.Items, &rss.Item{
Title: articleTitle,
Author: user.FirstName + " " + user.LastName,
PubDate: article.Created.Format(time.RFC1123Z),
Description: articleDescription,
Content: &rss.Content{Value: articleContent},
Categories: tagNames,
})
}
feed := rss.NewFeed()
feed.Channels = append(feed.Channels, channel)
rss, err := feed.ToXML("UTF-8")
if err != nil {
return nil, fmt.Errorf("error converting RSS feed to XML: %v", err)
}
return &rss, nil
}
func SaveRSS(filename string, feed *string) error {
file, err := os.Create(filename)
if err != nil {
return fmt.Errorf("error creating file for RSS feed: %v", err)
}
defer file.Close()
if err = file.Chmod(0644); err != nil {
return fmt.Errorf("error setting permissions for RSS file: %v", err)
}
if _, err = io.WriteString(file, *feed); err != nil {
return fmt.Errorf("error writing to RSS file: %v", err)
}
return nil
}

View File

@ -16,71 +16,62 @@ func init() {
}
func main() {
config, err := control.HandleConfig()
if err != nil {
log.Fatalln(err)
}
logFile, err := os.OpenFile(config.LogFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
logFile, err := os.OpenFile("tmp/cpolis.log",
os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
log.Fatalln(err)
}
defer logFile.Close()
log.SetOutput(logFile)
// log.SetOutput(logFile)
db, err := model.OpenDB(config.DBName)
db, err := model.OpenDB("cpolis")
if err != nil {
log.Fatalln(err)
}
defer db.Close()
key, err := control.LoadKey(config.KeyFile)
key, err := control.LoadKey("tmp/key.gob")
if err != nil {
key, err = control.NewKey()
if err != nil {
log.Fatalln(err)
}
control.SaveKey(key, config.KeyFile)
control.SaveKey(key, "tmp/key.gob")
}
store := control.NewCookieStore(key)
mux := http.NewServeMux()
mux.Handle("/web/static/", http.StripPrefix("/web/static/",
http.FileServer(http.Dir(config.WebDir+"/static/"))))
mux.HandleFunc("/", view.HomePage(config, db, store))
http.FileServer(http.Dir("web/static/"))))
mux.HandleFunc("/", view.HomePage(db, store))
mux.HandleFunc("GET /create-tag", view.CreateTag(config))
mux.HandleFunc("GET /create-user", view.CreateUser(config))
mux.HandleFunc("GET /edit-self", view.EditSelf(config, db, store))
mux.HandleFunc("GET /edit-user/{id}", view.EditUser(config, db))
mux.HandleFunc("GET /delete-user/{id}", view.DeleteUser(config, db, store))
mux.HandleFunc("GET /hub", view.ShowHub(config, db, store))
mux.HandleFunc("GET /logout", view.Logout(config, store))
mux.HandleFunc("GET /pics/{pic}", view.ServeImage(config, store))
mux.HandleFunc("GET /publish-article/{id}", view.PublishArticle(config, db, store))
mux.HandleFunc("GET /publish-issue", view.PublishLatestIssue(config, db, store))
mux.HandleFunc("GET /reject-article/{id}", view.RejectArticle(config, db, store))
mux.HandleFunc("GET /rejected-articles", view.ShowRejectedArticles(config, db, store))
mux.HandleFunc("GET /review-rejected-article/{id}", view.ReviewRejectedArticle(config, db, store))
mux.HandleFunc("GET /review-unpublished-article/{id}", view.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", view.ShowAllUsers(config, db, store, "edit-user"))
mux.HandleFunc("GET /show-all-users-delete", view.ShowAllUsers(config, db, store, "delete-user"))
mux.HandleFunc("GET /this-issue", view.ShowCurrentArticles(config, db))
mux.HandleFunc("GET /unpublished-articles", view.ShowUnpublishedArticles(config, db))
mux.HandleFunc("GET /write-article", view.WriteArticle(config, db))
mux.HandleFunc("GET /create-tag/", view.CreateTag)
mux.HandleFunc("GET /create-user/", view.CreateUser)
mux.HandleFunc("GET /edit-user/", view.EditUser(db, store))
mux.HandleFunc("GET /hub/", view.ShowHub(db, store))
mux.HandleFunc("GET /logout/", view.Logout(store))
mux.HandleFunc("GET /publish-issue/", view.PublishLatestIssue(db))
mux.HandleFunc("GET /rejected-articles/", view.ShowRejectedArticles(db, store))
mux.HandleFunc("GET /rss/", view.ShowRSS(
db,
"Freimaurer Distrikt Niedersachsen und Sachsen-Anhalt",
"https://distrikt-ni-st.de",
"Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität",
))
mux.HandleFunc("GET /this-issue/", view.ShowCurrentArticles(db))
mux.HandleFunc("GET /unpublished-articles/", view.ShowUnpublishedArticles(db))
mux.HandleFunc("GET /write-article/", view.WriteArticle(db))
mux.HandleFunc("POST /add-first-user", view.AddFirstUser(config, db, store))
mux.HandleFunc("POST /add-tag", view.AddTag(config, db, store))
mux.HandleFunc("POST /add-user", view.AddUser(config, db, store))
mux.HandleFunc("POST /login", view.Login(config, db, store))
mux.HandleFunc("POST /resubmit-article/{id}", view.ResubmitArticle(config, db, store))
mux.HandleFunc("POST /submit-article", view.SubmitArticle(config, db, store))
mux.HandleFunc("POST /update-self", view.UpdateSelf(config, db, store))
mux.HandleFunc("POST /update-user/{id}", view.UpdateUser(config, db, store))
mux.HandleFunc("POST /upload-image", view.UploadImage(config))
mux.HandleFunc("POST /add-tag/", view.AddTag(db, store))
mux.HandleFunc("POST /add-user/", view.AddUser(db, store))
mux.HandleFunc("POST /login/", view.Login(db, store))
mux.HandleFunc("POST /publish-article/", view.PublishArticle(db, store))
mux.HandleFunc("POST /reject-article/", view.RejectArticle(db, store))
mux.HandleFunc("POST /resubmit-article/", view.ResubmitArticle(db, store))
mux.HandleFunc("POST /review-rejected-article/", view.ReviewRejectedArticle(db, store))
mux.HandleFunc("POST /review-unpublished-article/", view.ReviewUnpublishedArticle(db, store))
mux.HandleFunc("POST /submit-article/", view.SubmitArticle(db, store))
mux.HandleFunc("POST /update-user/", view.UpdateUser(db, store))
log.Fatalln(http.ListenAndServe(config.Port, mux))
log.Fatalln(http.ListenAndServe(":8080", mux))
}

View File

@ -105,7 +105,7 @@ func (db *DB) GetArticle(id int64) (*Article, error) {
func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) {
query := `
SELECT id, title, created, description, content, author_id, issue_id
SELECT id, title, created, description, content, author_id
FROM articles
WHERE published = ?
AND rejected = ?
@ -121,8 +121,7 @@ func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) {
var created []byte
if err = rows.Scan(&article.ID, &article.Title, &created,
&article.Description, &article.Content, &article.AuthorID,
&article.IssueID); err != nil {
&article.Description, &article.Content, &article.AuthorID); err != nil {
return nil, fmt.Errorf("error scanning article row: %v", err)
}

View File

@ -6,7 +6,10 @@ import (
)
func (db *DB) WriteArticleTags(articleID int64, tagIDs []int64) error {
query := "INSERT INTO articles_tags (article_id, tag_id) VALUES (?, ?)"
query := `
INSERT INTO articles_tags (article_id, tag_id)
VALUES (?, ?)
`
for i := 0; i < TxMaxRetries; i++ {
err := func() error {
@ -65,8 +68,8 @@ func (db *DB) GetArticleTags(articleID int64) ([]*Tag, error) {
}
func (db *DB) UpdateArticleTags(articleID int64, tagIDs []int64) error {
deleteQuery := "DELETE FROM articles_tags WHERE article_id = ?"
insertQuery := "INSERT INTO articles_tags (article_id, tag_id) VALUES (?, ?)"
query := `
`
for i := 0; i < TxMaxRetries; i++ {
err := func() error {
@ -75,22 +78,6 @@ func (db *DB) UpdateArticleTags(articleID int64, tagIDs []int64) error {
return fmt.Errorf("error starting transaction: %v", err)
}
if _, err := tx.Exec(deleteQuery, articleID); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return fmt.Errorf("error deleting entries from articles_tags before inserting new ones: %v", err)
}
for _, tagID := range tagIDs {
if _, err := tx.Exec(insertQuery, articleID, tagID); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return fmt.Errorf("error inserting new entries into articles_tags: %v", err)
}
}
if err = tx.Commit(); err != nil {
return fmt.Errorf("error committing transaction: %v", err)
}

View File

@ -23,6 +23,7 @@ func (db *DB) AddIssue() (int64, error) {
}
func (db *DB) PublishLatestIssue() error {
var id int64
txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable}
updateQuery := "UPDATE issues SET published = true WHERE published = false"
insertQuery := "INSERT INTO issues (published) VALUES (?)"
@ -34,7 +35,7 @@ func (db *DB) PublishLatestIssue() error {
return fmt.Errorf("error starting transaction: %v", err)
}
if _, err := tx.Exec(updateQuery); err != nil {
if _, err := tx.Exec(updateQuery, id); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}

View File

@ -1,8 +1,6 @@
package model
import (
"context"
"database/sql"
"fmt"
"log"
@ -14,7 +12,6 @@ const (
Publisher
Editor
Author
NonExistent
)
type User struct {
@ -146,7 +143,7 @@ func (db *DB) GetUser(id int64) (*User, error) {
return user, nil
}
func (db *DB) UpdateOwnAttributes(id int64, user, first, last, oldPass, newPass, newPass2 string) error {
func (db *DB) UpdateUserAttributes(id int64, user, first, last, oldPass, newPass, newPass2 string) error {
passwordEmpty := true
if len(newPass) > 0 || len(newPass2) > 0 {
if newPass != newPass2 {
@ -201,180 +198,3 @@ func (db *DB) UpdateOwnAttributes(id int64, user, first, last, oldPass, newPass,
return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries)
}
func (db *DB) AddFirstUser(u *User, pass string) (int64, error) {
var numUsers int64
txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable}
selectQuery := "SELECT COUNT(*) FROM users"
insertQuery := `
INSERT INTO users (username, password, first_name, last_name, role)
VALUES (?, ?, ?, ?, ?)
`
for i := 0; i < TxMaxRetries; i++ {
id, err := func() (int64, error) {
tx, err := db.BeginTx(context.Background(), txOptions)
if err != nil {
return 0, fmt.Errorf("error starting transaction: %v", err)
}
if err := tx.QueryRow(selectQuery).Scan(&numUsers); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return 0, fmt.Errorf("error getting ID of %v: %v", u.UserName, err)
}
if numUsers != 0 {
if err = tx.Commit(); err != nil {
return 0, fmt.Errorf("error committing transaction: %v", err)
}
return 2, nil
}
hashedPass, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return 0, fmt.Errorf("error creating password hash: %v", err)
}
result, err := tx.Exec(insertQuery, u.UserName, string(hashedPass), u.FirstName, u.LastName, u.Role)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return 0, fmt.Errorf("error inserting new user %v into DB: %v", u.UserName, err)
}
id, err := result.LastInsertId()
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr)
}
return 0, fmt.Errorf("error inserting user into DB: %v", err)
}
if err = tx.Commit(); err != nil {
return 0, fmt.Errorf("error committing transaction: %v", err)
}
return id, nil
}()
if err == nil {
return id, nil
}
log.Println(err)
wait(i)
}
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,37 +1,31 @@
package view
import (
"fmt"
"html/template"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/google/uuid"
"streifling.com/jason/cpolis/cmd/control"
"streifling.com/jason/cpolis/cmd/model"
)
func ShowHub(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
func ShowHub(db *model.DB, s *control.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := s.Get(r, "cookie")
if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
tmpl, err := template.ParseFiles("web/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, err := template.ParseFiles("web/templates/hub.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", session.Values["role"].(int))
}
}
func WriteArticle(c *control.Config, db *model.DB) http.HandlerFunc {
func WriteArticle(db *model.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
tags, err := db.GetTagList()
if err != nil {
@ -40,16 +34,16 @@ func WriteArticle(c *control.Config, db *model.DB) http.HandlerFunc {
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/editor.html")
tmpl, err := template.ParseFiles("web/templates/editor.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", tags)
}
}
func SubmitArticle(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
func SubmitArticle(db *model.DB, s *control.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := s.Get(r, "cookie")
if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
tmpl, err := template.ParseFiles("web/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
@ -87,15 +81,15 @@ func SubmitArticle(c *control.Config, db *model.DB, s *control.CookieStore) http
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl, err := template.ParseFiles("web/templates/hub.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
}
}
func ResubmitArticle(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
func ResubmitArticle(db *model.DB, s *control.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
id, err := strconv.ParseInt(r.PostFormValue("article-id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -106,7 +100,7 @@ func ResubmitArticle(c *control.Config, db *model.DB, s *control.CookieStore) ht
description := r.PostFormValue("article-description")
content := r.PostFormValue("article-content")
if err = db.UpdateAttributes(
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},
@ -117,37 +111,20 @@ func ResubmitArticle(c *control.Config, db *model.DB, s *control.CookieStore) ht
return
}
r.ParseForm()
tags := make([]int64, 0)
for _, tag := range r.Form["tags"] {
tagID, err := strconv.ParseInt(tag, 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tags = append(tags, tagID)
}
if err = db.UpdateArticleTags(id, tags); 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")
tmpl, err := template.ParseFiles("web/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, err := template.ParseFiles("web/templates/hub.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
}
}
func ShowUnpublishedArticles(c *control.Config, db *model.DB) http.HandlerFunc {
func ShowUnpublishedArticles(db *model.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
unpublishedArticles, err := db.GetCertainArticles(false, false)
if err != nil {
@ -156,13 +133,13 @@ func ShowUnpublishedArticles(c *control.Config, db *model.DB) http.HandlerFunc {
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/unpublished-articles.html")
tmpl, err := template.ParseFiles("web/templates/unpublished-articles.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", unpublishedArticles)
}
}
func ShowRejectedArticles(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
func ShowRejectedArticles(db *model.DB, s *control.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
type htmlData struct {
MyIDs map[int64]bool
@ -191,75 +168,48 @@ func ShowRejectedArticles(c *control.Config, db *model.DB, s *control.CookieStor
}
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/rejected-articles.html")
tmpl, err := template.ParseFiles("web/templates/rejected-articles.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", data)
}
}
func ReviewUnpublishedArticle(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
func ReviewUnpublishedArticle(db *model.DB, s *control.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
type htmlData struct {
Title string
Description string
Content template.HTML
Article *model.Article
Tags []*model.Tag
ID int64
}
var err error
data := new(htmlData)
data.ID, err = strconv.ParseInt(r.PathValue("id"), 10, 64)
id, err := strconv.ParseInt(r.PostFormValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
article, err := db.GetArticle(data.ID)
data.Article, err = db.GetArticle(id)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Title, err = control.ConvertToPlain(article.Title)
data.Tags, err = db.GetArticleTags(id)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Description, err = control.ConvertToPlain(article.Description)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
content, err := control.ConvertToHTML(article.Content)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Content = template.HTML(content)
data.Tags, err = db.GetArticleTags(data.ID)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/to-be-published.html")
tmpl, err := template.ParseFiles("web/templates/to-be-published.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", data)
}
}
func ReviewRejectedArticle(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
func ReviewRejectedArticle(db *model.DB, s *control.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
type htmlData struct {
Selected map[int64]bool
@ -268,7 +218,7 @@ func ReviewRejectedArticle(c *control.Config, db *model.DB, s *control.CookieSto
}
data := new(htmlData)
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
id, err := strconv.ParseInt(r.PostFormValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -300,15 +250,15 @@ func ReviewRejectedArticle(c *control.Config, db *model.DB, s *control.CookieSto
data.Selected[tag.ID] = true
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/rework-article.html")
tmpl, err := template.ParseFiles("web/templates/rework-article.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", data)
}
}
func PublishArticle(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
func PublishArticle(db *model.DB, s *control.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
id, err := strconv.ParseInt(r.PostFormValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -317,7 +267,7 @@ func PublishArticle(c *control.Config, db *model.DB, s *control.CookieStore) htt
session, err := s.Get(r, "cookie")
if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
tmpl, err := template.ParseFiles("web/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
@ -338,27 +288,15 @@ func PublishArticle(c *control.Config, db *model.DB, s *control.CookieStore) htt
return
}
feed, err := control.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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl, err := template.ParseFiles("web/templates/hub.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
}
}
func RejectArticle(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
func RejectArticle(db *model.DB, s *control.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
id, err := strconv.ParseInt(r.PostFormValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -367,7 +305,7 @@ func RejectArticle(c *control.Config, db *model.DB, s *control.CookieStore) http
session, err := s.Get(r, "cookie")
if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
tmpl, err := template.ParseFiles("web/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
@ -380,13 +318,13 @@ func RejectArticle(c *control.Config, db *model.DB, s *control.CookieStore) http
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl, err := template.ParseFiles("web/templates/hub.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
}
}
func ShowCurrentArticles(c *control.Config, db *model.DB) http.HandlerFunc {
func ShowCurrentArticles(db *model.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
articles, err := db.GetCurrentIssueArticles()
if err != nil {
@ -395,48 +333,20 @@ func ShowCurrentArticles(c *control.Config, db *model.DB) http.HandlerFunc {
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/current-articles.html")
tmpl, err := template.ParseFiles("web/templates/current-articles.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", articles)
}
}
func UploadImage(c *control.Config) http.HandlerFunc {
func PublishLatestIssue(db *model.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
file, header, err := r.FormFile("article-image")
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer file.Close()
nameStrings := strings.Split(header.Filename, ".")
extension := "." + nameStrings[len(nameStrings)-1]
filename := fmt.Sprint(uuid.New(), extension)
absFilepath, err := filepath.Abs(fmt.Sprint(c.PicsDir, "/", filename))
if err != nil {
if err := db.PublishLatestIssue(); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
img, err := os.Create(absFilepath)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer img.Close()
if _, err = io.Copy(img, file); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
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)
tmpl, err := template.ParseFiles("web/templates/hub.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil)
}
}

View File

@ -8,25 +8,23 @@ import (
"streifling.com/jason/cpolis/cmd/model"
)
func CreateTag(c *control.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-tag.html")
func CreateTag(w http.ResponseWriter, r *http.Request) {
tmpl, err := template.ParseFiles("web/templates/add-tag.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil)
}
}
func AddTag(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
func AddTag(db *model.DB, s *control.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
db.AddTag(r.PostFormValue("tag"))
session, err := s.Get(r, "cookie")
if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
tmpl, err := template.ParseFiles("web/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, err := template.ParseFiles("web/templates/hub.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
}

View File

@ -1,22 +0,0 @@
package view
import (
"log"
"net/http"
"path/filepath"
"streifling.com/jason/cpolis/cmd/control"
)
func ServeImage(c *control.Config, s *control.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,31 +0,0 @@
package view
import (
"html/template"
"log"
"net/http"
"streifling.com/jason/cpolis/cmd/control"
"streifling.com/jason/cpolis/cmd/model"
)
func PublishLatestIssue(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := db.PublishLatestIssue(); 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"])
}
}

95
cmd/view/rss.go Normal file
View File

@ -0,0 +1,95 @@
package view
import (
"fmt"
"html/template"
"log"
"net/http"
"time"
"git.streifling.com/jason/rss"
"streifling.com/jason/cpolis/cmd/control"
"streifling.com/jason/cpolis/cmd/model"
)
func ShowRSS(db *model.DB, title, link, desc string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
channel := &rss.Channel{
Title: title,
Link: link,
Description: desc,
Items: make([]*rss.Item, 0),
}
articles, err := db.GetCertainArticles(true, false)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for _, article := range articles {
tags, err := db.GetArticleTags(article.ID)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tagNames := make([]string, 0)
for _, tag := range tags {
tagNames = append(tagNames, tag.Name)
}
tagNames = append(tagNames, fmt.Sprint("Orient Express ", article.IssueID))
user, err := db.GetUser(article.AuthorID)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
articleTitle, err := control.ConvertToPlain(article.Title)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
articleDescription, err := control.ConvertToPlain(article.Description)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
articleContent, err := control.ConvertToHTML(article.Content)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
channel.Items = append(channel.Items, &rss.Item{
Title: articleTitle,
Author: user.FirstName + user.LastName,
PubDate: article.Created.Format(time.RFC1123Z),
Description: articleDescription,
Content: &rss.Content{Value: articleContent},
Categories: tagNames,
})
}
feed := rss.NewFeed()
feed.Channels = append(feed.Channels, channel)
rss, err := feed.ToXML()
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
files := []string{"web/templates/index.html", "web/templates/feed.rss"}
tmpl, err := template.ParseFiles(files...)
template.Must(tmpl, err).Execute(w, rss)
}
}

View File

@ -27,26 +27,26 @@ func saveSession(w http.ResponseWriter, r *http.Request, s *control.CookieStore,
return nil
}
func HomePage(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
func HomePage(db *model.DB, s *control.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
numRows, err := db.CountEntries("users")
if err != nil {
log.Fatalln(err)
}
files := []string{c.WebDir + "/templates/index.html"}
files := []string{"web/templates/index.html"}
if numRows == 0 {
files = append(files, c.WebDir+"/templates/first-user.html")
files = append(files, "web/templates/add-user.html")
tmpl, err := template.ParseFiles(files...)
template.Must(tmpl, err).Execute(w, nil)
} else {
session, _ := s.Get(r, "cookie")
if auth, ok := session.Values["authenticated"].(bool); auth && ok {
files = append(files, c.WebDir+"/templates/hub.html")
files = append(files, "web/templates/hub.html")
tmpl, err := template.ParseFiles(files...)
template.Must(tmpl, err).Execute(w, session.Values["role"])
} else {
files = append(files, c.WebDir+"/templates/login.html")
files = append(files, "web/templates/login.html")
tmpl, err := template.ParseFiles(files...)
template.Must(tmpl, err).Execute(w, nil)
}
@ -54,7 +54,7 @@ func HomePage(c *control.Config, db *model.DB, s *control.CookieStore) http.Hand
}
}
func Login(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
func Login(db *model.DB, s *control.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userName := r.PostFormValue("username")
password := r.PostFormValue("password")
@ -84,16 +84,16 @@ func Login(c *control.Config, db *model.DB, s *control.CookieStore) http.Handler
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl, err := template.ParseFiles("web/templates/hub.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", user.Role)
}
}
func Logout(c *control.Config, s *control.CookieStore) http.HandlerFunc {
func Logout(s *control.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := s.Get(r, "cookie")
if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
tmpl, err := template.ParseFiles("web/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
@ -106,7 +106,7 @@ func Logout(c *control.Config, s *control.CookieStore) http.HandlerFunc {
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
tmpl, err := template.ParseFiles("web/templates/login.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil)
}
}

View File

@ -31,14 +31,12 @@ func checkUserStrings(user *model.User) (string, int, bool) {
}
}
func CreateUser(c *control.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html")
func CreateUser(w http.ResponseWriter, r *http.Request) {
tmpl, err := template.ParseFiles("web/templates/add-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil)
}
}
func AddUser(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
func AddUser(db *model.DB, s *control.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
role, err := strconv.Atoi(r.PostFormValue("role"))
if err != nil {
@ -61,7 +59,7 @@ func AddUser(c *control.Config, db *model.DB, s *control.CookieStore) http.Handl
if len(htmlData.UserName) == 0 || len(htmlData.FirstName) == 0 ||
len(htmlData.LastName) == 0 || len(pass) == 0 || len(pass2) == 0 {
htmlData.Msg = "Alle Felder müssen ausgefüllt werden."
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html")
tmpl, err := template.ParseFiles("web/templates/add-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData)
return
}
@ -69,7 +67,7 @@ func AddUser(c *control.Config, db *model.DB, s *control.CookieStore) http.Handl
if !ok {
htmlData.Msg = fmt.Sprint(userString, " ist zu lang. Maximal ",
stringLen, " Zeichen erlaubt.")
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html")
tmpl, err := template.ParseFiles("web/templates/add-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData)
return
}
@ -77,42 +75,58 @@ func AddUser(c *control.Config, db *model.DB, s *control.CookieStore) http.Handl
if id != 0 {
htmlData.Msg = fmt.Sprint(htmlData.UserName,
" ist bereits vergeben. Bitte anderen Benutzernamen wählen.")
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html")
tmpl, err := template.ParseFiles("web/templates/add-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData)
return
}
if pass != pass2 {
htmlData.Msg = "Die Passwörter stimmen nicht überein."
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html")
tmpl, err := template.ParseFiles("web/templates/add-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData)
return
}
_, err = db.AddUser(htmlData.User, pass)
htmlData.ID, err = db.AddUser(htmlData.User, pass)
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)
if htmlData.ID == 1 {
htmlData.Role = model.Admin
if err = db.UpdateAttributes(
&model.Attribute{Table: "users", ID: id, AttName: "role", Value: htmlData.Role},
); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int))
if err := saveSession(w, r, s, htmlData.User); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if _, err := db.AddIssue(); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
tmpl, err := template.ParseFiles("web/templates/hub.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", 0)
}
}
func EditSelf(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
func EditUser(db *model.DB, s *control.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := s.Get(r, "cookie")
if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
tmpl, err := template.ParseFiles("web/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
@ -124,16 +138,16 @@ func EditSelf(c *control.Config, db *model.DB, s *control.CookieStore) http.Hand
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-self.html")
tmpl, err := template.ParseFiles("web/templates/edit-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", user)
}
}
func UpdateSelf(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
func UpdateUser(db *model.DB, s *control.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := s.Get(r, "cookie")
if err != nil {
tmpl, err := template.ParseFiles(c.WebDir + "/templates/login.html")
tmpl, err := template.ParseFiles("web/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
@ -153,7 +167,7 @@ func UpdateSelf(c *control.Config, db *model.DB, s *control.CookieStore) http.Ha
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, err := template.ParseFiles("web/templates/edit-user.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", userData.Msg)
return
@ -163,7 +177,7 @@ func UpdateSelf(c *control.Config, db *model.DB, s *control.CookieStore) http.Ha
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, err := template.ParseFiles("web/templates/edit-user.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", userData)
return
@ -173,209 +187,7 @@ func UpdateSelf(c *control.Config, db *model.DB, s *control.CookieStore) http.Ha
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.UpdateOwnAttributes(
userData.ID,
userData.UserName,
userData.FirstName,
userData.LastName,
oldPass,
newPass,
newPass2); 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)
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int))
}
}
func AddFirstUser(c *control.Config, db *model.DB, s *control.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var err error
htmlData := UserData{
User: &model.User{
UserName: r.PostFormValue("username"),
FirstName: r.PostFormValue("first-name"),
LastName: r.PostFormValue("last-name"),
Role: model.Admin,
},
}
pass := r.PostFormValue("password")
pass2 := r.PostFormValue("password2")
if len(htmlData.UserName) == 0 || len(htmlData.FirstName) == 0 ||
len(htmlData.LastName) == 0 || len(pass) == 0 || len(pass2) == 0 {
htmlData.Msg = "Alle Felder müssen ausgefüllt werden."
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData)
return
}
userString, stringLen, ok := checkUserStrings(htmlData.User)
if !ok {
htmlData.Msg = fmt.Sprint(userString, " ist zu lang. Maximal ",
stringLen, " Zeichen erlaubt.")
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData)
return
}
id, _ := db.GetID(htmlData.UserName)
if id != 0 {
htmlData.Msg = fmt.Sprint(htmlData.UserName,
" ist bereits vergeben. Bitte anderen Benutzernamen wählen.")
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData)
return
}
if pass != pass2 {
htmlData.Msg = "Die Passwörter stimmen nicht überein."
tmpl, err := template.ParseFiles(c.WebDir + "/templates/add-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData)
return
}
htmlData.ID, err = db.AddFirstUser(htmlData.User, pass)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if htmlData.ID > 1 {
errString := "error: there is already a first user"
log.Println(errString)
http.Error(w, errString, http.StatusInternalServerError)
return
}
if err := saveSession(w, r, s, htmlData.User); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if _, err := db.AddIssue(); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles(c.WebDir + "/templates/hub.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", 0)
}
}
func ShowAllUsers(c *control.Config, db *model.DB, s *control.CookieStore, action string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var err error
type htmlData struct {
Users map[int64]*model.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 *control.Config, db *model.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 *control.Config, db *model.DB, s *control.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: &model.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, err := template.ParseFiles("web/templates/edit-user.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", userData)
return
@ -387,50 +199,15 @@ func UpdateUser(c *control.Config, db *model.DB, s *control.CookieStore) http.Ha
userData.UserName,
userData.FirstName,
userData.LastName,
oldPass,
newPass,
newPass2,
userData.Role); err != nil {
newPass2); err != nil {
userData.Msg = "Aktualisierung der Benutzerdaten fehlgeschlagen."
tmpl, err := template.ParseFiles(c.WebDir + "/templates/edit-user.html")
tmpl, err := template.ParseFiles("web/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 *control.Config, db *model.DB, s *control.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, err := template.ParseFiles("web/templates/hub.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int))
}

View File

@ -1,50 +0,0 @@
DROP TABLE IF EXISTS articles_tags;
DROP TABLE IF EXISTS tags;
DROP TABLE IF EXISTS articles;
DROP TABLE IF EXISTS issues;
DROP TABLE IF EXISTS users;
CREATE TABLE users (
id INT AUTO_INCREMENT,
username VARCHAR(15) NOT NULL UNIQUE,
password VARCHAR(60) NOT NULL,
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL,
role INT NOT NULL,
PRIMARY KEY(id)
);
CREATE TABLE issues (
id INT AUTO_INCREMENT,
published BOOL NOT NULL,
PRIMARY KEY(id)
);
CREATE TABLE articles (
id INT AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
description TEXT NOT NULL,
content TEXT NOT NULL,
published BOOL NOT NULL,
rejected BOOL NOT NULL,
author_id INT NOT NULL,
issue_id INT NOT NULL,
PRIMARY KEY(id),
FOREIGN KEY(author_id) REFERENCES users(id),
FOREIGN KEY(issue_id) REFERENCES issues(id)
);
CREATE TABLE tags (
id INT AUTO_INCREMENT,
name VARCHAR(50) NOT NULL UNIQUE,
PRIMARY KEY(id)
);
CREATE TABLE articles_tags (
article_id INT,
tag_id INT,
PRIMARY KEY(article_id, tag_id),
FOREIGN KEY(article_id) REFERENCES articles(id),
FOREIGN KEY(tag_id) REFERENCES tags(id)
);

4
go.mod
View File

@ -3,10 +3,8 @@ module streifling.com/jason/cpolis
go 1.22.0
require (
git.streifling.com/jason/rss v0.1.2
github.com/BurntSushi/toml v1.3.2
git.streifling.com/jason/rss v0.0.0-20240305164907-524bf9676188
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

8
go.sum
View File

@ -1,15 +1,11 @@
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=
git.streifling.com/jason/rss v0.0.0-20240305164907-524bf9676188 h1:C8M/j3f+cl5Y7YfGpU/ynb/SC/4tTYMDsyGFt3rswM8=
git.streifling.com/jason/rss v0.0.0-20240305164907-524bf9676188/go.mod h1:gpZF0nZbQSstMpyHD9DTAvlQEG7v4pjO5c7aIMWM4Jg=
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,10 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./web/templates/*.html"],
theme: {
extend: {}
},
plugins: [
require('@tailwindcss/typography')
],
}

View File

@ -1,41 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
width: 800px;
@apply mx-auto text-slate-900;
}
h2 {
@apply font-bold mb-2 text-2xl;
}
form {
@apply flex flex-col gap-y-3;
}
input[type="file"] {
@apply border rounded-md w-full;
}
input[type="password"],
input[type="text"] {
@apply border h-8 rounded-md;
}
textarea {
@apply border h-32 rounded-md;
}
.btn-area {
@apply flex gap-4 mt-4;
}
.btn {
@apply bg-slate-200 border my-2 px-3 py-2 rounded-md w-full hover:bg-slate-100;
}
.action-btn {
@apply bg-slate-800 border my-2 px-3 py-2 rounded-md text-slate-50 w-full hover:bg-slate-700;
}

0
web/static/css/style.css Normal file
View File

View File

@ -1,12 +1,8 @@
{{define "page-content"}}
<h2>Neuer Tag</h2>
<h2>Neuer Benutzer</h2>
<form>
<input required name="tag" placeholder="Tag eingeben" type="text" />
<div class="btn-area">
<input class="action-btn" type="submit" value="Anlegen" hx-post="/add-tag" hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
</div>
<input required name="tag" placeholder="Tag" type="text" />
<input type="submit" value="Anlegen" hx-post="/add-tag/" hx-target="#page-content" />
</form>
<button hx-get="/hub/" hx-target="#page-content">Abbrechen</button>
{{end}}

View File

@ -1,54 +1,25 @@
{{define "page-content"}}
<h2>Neuer Benutzer</h2>
<form>
<div class="grid grid-cols-3 gap-4">
<div>
<label for="username">Benutzername</label>
<input class="w-full" required name="username" type="text" value="{{.UserName}}" />
</div>
<div>
<label for="password">Passwort</label>
<input class="w-full" required name="password" placeholder="***" type="password" />
</div>
<div>
<label for="password2">Passwort wiederholen</label>
<input class="w-full" required name="password2" placeholder="***" type="password" />
</div>
<div>
<label for="first-name">Vorname</label>
<input class="w-full" required name="first-name" type="text" value="{{.FirstName}}" />
</div>
<div>
<label for="last-name">Nachname</label>
<input class="w-full" required name="last-name" type="text" value="{{.LastName}}" />
</div>
</div>
<input required name="username" placeholder="Benutzername" type="text" value="{{.UserName}}" />
<input required name="password" placeholder="Passwort" type="password" />
<input required name="password2" placeholder="Passwort wiederholen" type="password" />
<input required name="first-name" placeholder="Vorname" type="text" value="{{.FirstName}}" />
<input required name="last-name" placeholder="Nachname" type="text" value="{{.LastName}}" />
<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>
<label for="admin">Admin</label>
<div class="btn-area">
<input class="action-btn" type="submit" value="Anlegen" hx-post="/add-user" hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
</div>
<input type="submit" value="Anlegen" hx-post="/add-user/" hx-target="#page-content" />
</form>
<button hx-get="/hub/" hx-target="#page-content">Abbrechen</button>
<script>
var msg = "{{.Msg}}";

View File

@ -1,17 +1,10 @@
{{define "page-content"}}
<h2>Aktuelle Artikel</h2>
<div class="flex flex-col gap-4">
{{range .}}
<div class="border px-2 py-1 rounded-md">
<h1 class="font-bold text-2xl">{{.Title}}</h1>
{{range .}}
<div>
<h1>{{.Title}}</h1>
<p>{{.Description}}</p>
</div>
{{end}}
</div>
<div class="btn-area">
<button class="action-btn" hx-get="/publish-issue" hx-target="#page-content">Ausgabe publizieren</button>
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
</div>
{{end}}
<button hx-get="/publish-issue/" hx-target="#page-content">Ausgabe publizieren</button>
<button hx-get="/hub/" hx-target="#page-content">Abbrechen</button>
{{end}}

View File

@ -1,43 +0,0 @@
{{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,57 +1,13 @@
{{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>
<input name="username" type="text" value="{{.UserName}}" />
<input name="first-name" type="text" value="{{.FirstName}}" />
<input name="last-name" type="text" value="{{.LastName}}" />
<div>
<label for="first-name">Vorname</label>
<input class="w-full" name="first-name" type="text" value="{{.FirstName}}" />
</div>
<input name="old-password" placeholder="Altes Passwort" type="password" />
<input name="password" placeholder="Neues Passwort" type="password" />
<input name="password2" placeholder="Wiederholen" type="password" />
<div>
<label for="last-name">Nachname</label>
<input class="w-full" name="last-name" type="text" value="{{.LastName}}" />
</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/{{.ID}}"
hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Abbrechen</button>
</div>
<input type="submit" value="Aktualisieren" hx-post="/update-user/" hx-target="#page-content" />
</form>
{{end}}

View File

@ -1,70 +1,13 @@
{{define "page-content"}}
<h2>Editor</h2>
<form>
<div class="flex flex-col gap-y-1">
<label for="article-title">Titel</label>
<input name="article-title" type="text" />
</div>
<div class="flex flex-col">
<label for="article-description">Beschreibung</label>
<textarea name="article-description"></textarea>
</div>
<div class="flex flex-col">
<label for="article-content">Artikel</label>
<textarea name="article-content"></textarea>
</div>
<div>
<span>Tags</span>
<div class="flex flex-wrap gap-x-4">
<input name="article-title" placeholder="Titel" type="text" />
<textarea name="article-description" placeholder="Beschreibung"></textarea>
<textarea name="article-content" placeholder="Artikel"></textarea>
{{range .}}
<div>
<input id="{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" />
<label for="{{.Name}}">{{.Name}}</label>
</div>
{{end}}
</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>
</div>
<input type="submit" value="Senden" hx-post="/submit-article/" hx-target="#page-content" />
</form>
<script>
function copyToClipboard(text) {
event.preventDefault(); // Get-Request verhindern
var textarea = document.createElement("textarea");
textarea.textContent = text;
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
} catch (err) {
console.warn('Fehler beim Kopieren', err);
}
document.body.removeChild(textarea);
}
</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}}

3
web/templates/feed.rss Normal file
View File

@ -0,0 +1,3 @@
{{define "page-content"}}
{{.}}
{{end}}

View File

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

View File

@ -1,49 +1,17 @@
{{define "page-content"}}
<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-self" hx-target="#page-content">Profil bearbeiten</button>
</div>
</div>
{{end}}
{{if lt . 3}}
<div class="mb-3">
<h2>Redakteur</h2>
<div class="grid grid-cols-2 gap-4">
<button class="btn" hx-get="/unpublished-articles" hx-target="#page-content">
Unveröffentlichte Artikel
</button>
<button class="btn" hx-get="/create-tag" hx-target="#page-content">Neuer Tag</button>
</div>
</div>
{{end}}
{{if lt . 2}}
<div class="mb-3">
<h2>Herausgeber</h2>
<div class="grid grid-cols-2 gap-4">
<button class="btn" hx-get="/this-issue" hx-target="#page-content">Diese Ausgabe</button>
</div>
</div>
{{end}}
{{if eq . 0}}
<div class="mb-3">
<h2>Administrator</h2>
<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}}
</div>
<h2>Hub</h2>
<button hx-get="/write-article/" hx-target="#page-content">Artikel schreiben</button>
<button hx-get="/rejected-articles/" hx-target="#page-content">Abgelehnte Artikel</button>
<button hx-get="/rss/" hx-target="#page-content">RSS Feed</button>
<button hx-get="/edit-user/" hx-target="#page-content">Benutzer bearbeiten</button>
{{if lt . 3}}
<button hx-get="/unpublished-articles/" hx-target="#page-content">Unveröffentlichte Artikel</button>
<button hx-get="/create-tag/" hx-target="#page-content">Neuer Tag</button>
{{end}}
{{if lt . 2}}
<button hx-get="/this-issue/" hx-target="#page-content">Diese Ausgabe</button>
{{end}}
{{if eq . 0}}
<button hx-get="/create-user/" hx-target="#page-content">Benutzer hinzufügen</button>
{{end}}
{{end}}

View File

@ -5,27 +5,26 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Orient Editor</title>
<link href="/web/static/css/style.css" rel="stylesheet">
<link href="web/static/css/style.css" rel="stylesheet">
</head>
<body class="flex flex-col justify-between min-h-screen bg-slate-50">
<header class="my-8">
<h1 class="font-bold text-4xl text-center">Orient Editor</h1>
<body>
<header>
<h1>Orient Editor</h1>
<button hx-get="logout" hx-target="#page-content">Abmelden</button>
</header>
<main class="mx-4">
<main>
<div id="page-content">
{{template "page-content" .}}
</div>
<script src="web/static/js/htmx.min.js"></script>
</main>
<footer class="my-8">
<p class="text-center text-gray-500 dark:text-gray-400">
&copy; 2024 Jason Streifling. Alle Rechte vorbehalten.
</p>
<footer>
<p>&copy; 2024 Jason Streifling. Alle Rechte vorbehalten.</p>
</footer>
<script src="/web/static/js/htmx.min.js"></script>
</body>
</html>

View File

@ -1,12 +1,8 @@
{{define "page-content"}}
<h2>Anmeldung</h2>
<form>
<div class="btn-area">
<input class="w-full" name="username" placeholder="Benutzername" type="text" />
<input class="w-full" name="password" placeholder="Passwort" type="password" />
</div>
<input class="action-btn" type="submit" value="Anmelden" hx-post="/login" hx-target="#page-content" />
<input name="username" placeholder="Benutzername" type="text" />
<input name="password" placeholder="Passwort" type="password" />
<input type="submit" value="Anmelden" hx-post="/login/" hx-target="#page-content" />
</form>
{{end}}

View File

@ -1,16 +1,12 @@
{{define "page-content"}}
<h2>Abgelehnte Artikel</h2>
<div class="flex flex-col gap-4">
<form>
{{range .RejectedArticles}}
{{if index $.MyIDs .ID}}
<button class="btn" hx-get="/review-rejected-article/{{.ID}}" hx-target="#page-content">
<h1 class="font-bold text-2xl">{{.Title}}</h1>
<p>{{.Description}}</p>
</button>
<input required id="{{.ID}}" name="id" type="radio" value="{{.ID}}" />
<label for="{{.ID}}">{{.Title}}</label>
{{end}}
{{end}}
<button class="action-btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
</div>
<input type="submit" value="Auswählen" hx-post="/review-rejected-article/" hx-target="#page-content" />
</form>
<button hx-get="/hub/" hx-target="#page-content">Zurück</button>
{{end}}

View File

@ -1,72 +1,16 @@
{{define "page-content"}}
<h2>Editor</h2>
<form>
<div class="flex flex-col gap-y-1">
<label for="article-title">Titel</label>
<input name="article-title" type="text" value="{{.Article.Title}}" />
</div>
<div class="flex flex-col">
<label for="article-description">Beschreibung</label>
<textarea name="article-description">{{.Article.Description}}</textarea>
</div>
<div class="flex flex-col">
<label for="article-content">Artikel</label>
<input name="article-title" placeholder="Titel" type="text" value="{{.Article.Title}}" />
<textarea name="article-description" placeholder="Beschreibung">{{.Article.Description}}</textarea>
<textarea name="article-content" placeholder="Artikel">{{.Article.Content}}</textarea>
</div>
<input name="article-id" type="hidden" value="{{.Article.ID}}" />
<div>
<span>Tags</span>
<div class="flex flex-wrap gap-x-4">
{{range .Tags}}
<div>
<input id="tag-{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" {{if index $.Selected
.ID}}checked{{end}} />
<input id="tag-{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" {{if index $.Selected .ID}}checked{{end}} />
<label for="tag-{{.Name}}">{{.Name}}</label>
</div>
{{end}}
</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" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
</div>
<input type="submit" value="Senden" hx-post="/resubmit-article/" hx-target="#page-content" />
</form>
<script>
function copyToClipboard(text) {
event.preventDefault(); // Get-Request verhindern
var textarea = document.createElement("textarea");
textarea.textContent = text;
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
} catch (err) {
console.warn('Fehler beim Kopieren', err);
}
document.body.removeChild(textarea);
}
</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,24 +0,0 @@
{{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,37 +1,19 @@
{{define "page-content"}}
<h2>Artikel veröffentlichen</h2>
<form>
<h2>{{.Article.Title}}</h2>
<p>{{.Article.Description}}</p>
{{.Article.Content}}
<div>
<span>Titel</span>
<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
{{.Title}}
</div>
<span>Beschreibung</span>
<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
{{.Description}}
</div>
<span>Artikel</span>
<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
<div class="prose">
{{.Content}}
</div>
</div>
<span>Tags</span>
<div class="bg-white border mb-3 px-2 py-2 rounded-md w-full">
<p>
{{range .Tags}}
{{.Name}}
<br>
{{end}}
</div>
</p>
<div class="btn-area">
<input class="action-btn" type="submit" value="Veröffentlichen" hx-get="/publish-article/{{.ID}}"
hx-target="#page-content" />
<input class="btn" type="submit" value="Ablehnen" hx-get="/reject-article/{{.ID}}" hx-target="#page-content" />
<button class="btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
</div>
</div>
<input name="id" type="hidden" value="{{.Article.ID}}" />
<input type="submit" value="Veröffentlichen" hx-post="/publish-article/" hx-target="#page-content" />
<input type="submit" value="Ablehnen" hx-post="/reject-article/" hx-target="#page-content" />
</form>
<button hx-get="/hub/" hx-target="#page-content">Zurück</button>
{{end}}

View File

@ -1,13 +1,10 @@
{{define "page-content"}}
<h2>Unveröffentlichte Artikel</h2>
<div class="flex flex-col gap-4">
<form>
{{range .}}
<button class="btn" hx-get="/review-unpublished-article/{{.ID}}" hx-target="#page-content">
<h1 class="font-bold text-2xl">{{.Title}}</h1>
<p>{{.Description}}</p>
</button>
<input required id="{{.ID}}" name="id" type="radio" value="{{.ID}}" />
<label for="{{.ID}}">{{.Title}}</label>
{{end}}
<button class="action-btn" hx-get="/hub" hx-target="#page-content">Zurück</button>
</div>
<input type="submit" value="Auswählen" hx-post="/review-unpublished-article/" hx-target="#page-content" />
</form>
<button hx-get="/hub/" hx-target="#page-content">Zurück</button>
{{end}}