Compare commits

..

27 Commits

Author SHA1 Message Date
45036fe286 Initial sessions implementation 2024-03-03 09:16:49 +01:00
8f7ac979a3 Just a bit of cleaning up 2024-03-02 09:09:55 +01:00
2da17014e4 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
4e2cae74bb Changed articles and rss to channels 2024-03-01 21:01:38 +01:00
4b5929911e Implemented proper User struct 2024-03-01 12:25:53 +01:00
f59321b9c6 Added ability to publish articles 2024-03-01 11:30:31 +01:00
cba3c663c9 Added article list for written but non-published articles 2024-02-27 14:10:27 +01:00
59029c86a9 Convert title and description to plain text 2024-02-27 09:03:21 +01:00
8f5739fb68 Implemented hub 2024-02-24 15:31:33 +01:00
49988edd82 Add ability to display feed 2024-02-24 14:49:29 +01:00
36f7a92a06 Added messages and field memory for adding user 2024-02-24 13:25:32 +01:00
f716e9f0b5 Require all fields to be filled out when creating a new user 2024-02-24 12:10:34 +01:00
f3c8cd6fa5 Implemented logging to file 2024-02-24 11:41:01 +01:00
280e88a526 Check if user already exists and bug fix 2024-02-24 10:56:12 +01:00
8ef6b6472d Added ability to add user 2024-02-24 10:28:12 +01:00
2e08600814 Added ability to login 2024-02-24 09:54:25 +01:00
068bf045a7 Check user credentials before adding user 2024-02-22 20:12:09 +01:00
96fe38726c Added ability to update Passwords 2024-02-22 19:27:41 +01:00
75a21eeb9f Added ability to add user 2024-02-22 18:49:51 +01:00
6020b24e44 Changed error messages 2024-02-22 15:23:29 +01:00
ebfe01069c Added HTML sanitizer 2024-02-22 15:22:45 +01:00
5d41543543 Added initial support for MySQL databases 2024-02-18 16:37:13 +01:00
2ccc9c7397 Handle misssed errors for encoding and decoding feeds 2024-02-18 14:31:28 +01:00
c5623fe4fd Added description and a way to save and restore the RSS feed. 2024-02-18 14:01:06 +01:00
ee04a2a351 Create RSS from HTML 2024-02-18 12:41:49 +01:00
aa034701df Show HTML on website 2024-02-18 10:48:37 +01:00
ad9bfb2439 First implementation of web based editor to HTML pipeline 2024-02-18 10:07:49 +01:00
35 changed files with 829 additions and 1526 deletions

View File

@ -1,50 +0,0 @@
package control
import (
"fmt"
"time"
"git.streifling.com/jason/rss"
"streifling.com/jason/cpolis/cmd/model"
)
func GetChannel(db *model.DB, title, link, description string) (*rss.Channel, error) {
channel := &rss.Channel{
Title: title,
Link: link,
Description: description,
Items: make([]*rss.Item, 0),
}
articles, err := db.GetCertainArticles(true, false)
if err != nil {
return nil, fmt.Errorf("error fetching published articles: %v", err)
}
for _, article := range articles {
tags, err := db.GetArticleTags(article.ID)
if err != nil {
return nil, fmt.Errorf("error fetching tags for article %v: %v", article.Title, err)
}
tagNames := make([]string, 0)
for _, tag := range tags {
tagNames = append(tagNames, tag.Name)
}
user, err := db.GetUser(article.AuthorID)
if err != nil {
return nil, fmt.Errorf("error finding user %v: %v", article.AuthorID, err)
}
channel.Items = append(channel.Items, &rss.Item{
Title: article.Title,
Author: user.FirstName + user.LastName,
PubDate: article.Created.Format(time.RFC1123Z),
Description: article.Description,
Content: &rss.Content{Value: article.Content},
Categories: tagNames,
})
}
return channel, nil
}

89
cmd/data/articles.go Normal file
View File

@ -0,0 +1,89 @@
package data
import (
"sync"
"time"
"github.com/google/uuid"
)
type Article struct {
Title string
Author string
Created time.Time
Desc string
Content string
Tags []string
UUID uuid.UUID
AuthorID int64
}
type ArticleList struct {
addCh chan *Article
delCh chan uuid.UUID
retCh chan *Article
getCh chan []Article
list []*Article
wg sync.WaitGroup
}
func minArticleList() *ArticleList {
return &ArticleList{
addCh: make(chan *Article),
delCh: make(chan uuid.UUID),
retCh: make(chan *Article),
getCh: make(chan []Article),
}
}
func (l *ArticleList) start() {
l.wg.Done()
for {
select {
case article := <-l.addCh:
l.list = append(l.list, article)
case uuid := <-l.delCh:
for i, article := range l.list {
if article.UUID == uuid {
l.list = append(l.list[:i], l.list[i+1:]...)
l.retCh <- article
}
}
case l.getCh <- func() []Article {
var list []Article
for _, article := range l.list {
list = append(list, *article)
}
return list
}():
}
}
}
func NewArticleList() *ArticleList {
list := minArticleList()
list.list = []*Article{}
list.wg.Add(1)
go list.start()
list.wg.Wait()
return list
}
func (l *ArticleList) Add(a *Article) {
l.addCh <- a
}
func (l *ArticleList) Release(uuid uuid.UUID) (*Article, bool) {
l.delCh <- uuid
article := <-l.retCh
if article == nil {
return nil, false
}
return article, true
}
func (l *ArticleList) Get() []Article {
return <-l.getCh
}

143
cmd/data/db.go Normal file
View File

@ -0,0 +1,143 @@
package data
import (
"database/sql"
"fmt"
"github.com/go-sql-driver/mysql"
"golang.org/x/crypto/bcrypt"
)
type DB struct {
*sql.DB
}
func OpenDB(dbName string) (*DB, error) {
var err error
db := DB{DB: &sql.DB{}}
cfg := mysql.NewConfig()
cfg.DBName = dbName
cfg.User, cfg.Passwd, err = getCredentials()
if err != nil {
return nil, fmt.Errorf("error reading user credentials for DB: %v", err)
}
db.DB, err = sql.Open("mysql", cfg.FormatDSN())
if err != nil {
return nil, fmt.Errorf("error opening DB: %v", err)
}
if err = db.Ping(); err != nil {
return nil, fmt.Errorf("error pinging DB: %v", err)
}
return &db, nil
}
func (db *DB) AddUser(user User, pass string) error {
hashedPass, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("error creating password hash: %v", err)
}
query := `
INSERT INTO users
(username, password, first_name, last_name, role)
VALUES (?, ?, ?, ?, ?)
`
_, err = db.Exec(query, user.UserName, string(hashedPass), user.FirstName, user.LastName, user.Role)
if err != nil {
return fmt.Errorf("error inserting user into DB: %v", err)
}
return nil
}
func (db *DB) GetID(userName string) (int64, error) {
var id int64
query := `
SELECT id
FROM users
WHERE username = ?
`
row := db.QueryRow(query, userName)
if err := row.Scan(&id); err != nil {
return 0, fmt.Errorf("user not in DB: %v", err)
}
return id, nil
}
func (db *DB) CheckPassword(id int64, pass string) error {
var queriedPass string
query := `
SELECT password
FROM users
WHERE id = ?
`
row := db.QueryRow(query, id)
if err := row.Scan(&queriedPass); err != nil {
return fmt.Errorf("error reading password from DB: %v", err)
}
if err := bcrypt.CompareHashAndPassword([]byte(queriedPass), []byte(pass)); err != nil {
return fmt.Errorf("incorrect password: %v", err)
}
return nil
}
func (db *DB) ChangePassword(id int64, oldPass, newPass string) error {
if err := db.CheckPassword(id, oldPass); err != nil {
return fmt.Errorf("error checking password: %v", err)
}
newHashedPass, err := bcrypt.GenerateFromPassword([]byte(newPass), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("error creating password hash: %v", err)
}
query := `
UPDATE users
SET password = ?
WHERE id = ?
`
_, err = db.Exec(query, string(newHashedPass), id)
if err != nil {
return fmt.Errorf("error updating password in DB: %v", err)
}
return nil
}
func (db *DB) CountEntries() (int64, error) {
var count int64
query := `SELECT COUNT(*) FROM users`
row := db.QueryRow(query)
if err := row.Scan(&count); err != nil {
return 0, fmt.Errorf("error counting rows in user DB: %v", err)
}
return count, nil
}
// TODO: No need for ID field in general
func (db *DB) GetUser(id int64) (*User, error) {
user := new(User)
query := `
SELECT id, username, first_name, last_name, role
FROM users
WHERE id = ?
`
row := db.QueryRow(query, id)
if err := row.Scan(&user.ID, &user.UserName, &user.FirstName,
&user.LastName, &user.Role); err != nil {
return nil, fmt.Errorf("error reading user information: %v", err)
}
return user, nil
}

52
cmd/data/helpers.go Normal file
View File

@ -0,0 +1,52 @@
package data
import (
"bufio"
"fmt"
"os"
"strings"
"syscall"
"golang.org/x/term"
)
func getUsername() (string, error) {
user := os.Getenv("DB_USER")
if user == "" {
var err error
fmt.Printf("DB Benutzer: ")
user, err = bufio.NewReader(os.Stdin).ReadString('\n')
if err != nil {
return "", fmt.Errorf("error reading username: %v", err)
}
}
return strings.TrimSpace(user), nil
}
func getPassword() (string, error) {
pass := os.Getenv("DB_PASS")
if pass == "" {
fmt.Printf("DB Passwort: ")
bytePass, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
return "", fmt.Errorf("error reading password: %v", err)
}
fmt.Println()
pass = strings.TrimSpace(string(bytePass))
}
return pass, nil
}
func getCredentials() (string, string, error) {
user, err := getUsername()
if err != nil {
return "", "", fmt.Errorf("error getting username: %v", err)
}
pass, err := getPassword()
if err != nil {
return "", "", fmt.Errorf("error getting password: %v", err)
}
return user, pass, nil
}

View File

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

106
cmd/data/rss.go Normal file
View File

@ -0,0 +1,106 @@
package data
import (
"encoding/gob"
"fmt"
"os"
"sync"
"github.com/gorilla/feeds"
)
type Feed struct {
addCh chan *feeds.Item
setCh chan feeds.Feed
getCh chan feeds.Feed
feed feeds.Feed
wg sync.WaitGroup
}
func minFeed() *Feed {
return &Feed{
addCh: make(chan *feeds.Item),
setCh: make(chan feeds.Feed),
getCh: make(chan feeds.Feed),
}
}
func (f *Feed) start() {
f.wg.Done()
for {
select {
case item := <-f.addCh:
f.feed.Items = append(f.feed.Items, item)
case f.getCh <- f.feed:
case f.feed = <-f.setCh:
}
}
}
func NewFeed(title, link, desc string) *Feed {
feed := minFeed()
feed.feed = feeds.Feed{
Title: title,
Link: &feeds.Link{Href: link},
Description: desc,
}
feed.wg.Add(1)
go feed.start()
feed.wg.Wait()
return feed
}
func (f *Feed) Get() feeds.Feed {
return <-f.getCh
}
func (f *Feed) Set(feed feeds.Feed) {
f.setCh <- feed
}
func OpenFeed(filename string) (*Feed, error) {
file, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("error opening file %v: %v", filename, err)
}
defer file.Close()
feed := minFeed()
feed.wg.Add(1)
go feed.start()
feed.wg.Wait()
decoder := gob.NewDecoder(file)
tmpFeed := new(feeds.Feed)
err = decoder.Decode(tmpFeed)
if err != nil {
return nil, fmt.Errorf("error decoding file %v: %v", filename, err)
}
feed.Set(*tmpFeed)
return feed, nil
}
func (f *Feed) Save(filename string) error {
file, err := os.Create(filename)
if err != nil {
return fmt.Errorf("error creating file %v: %v", filename, err)
}
defer file.Close()
encoder := gob.NewEncoder(file)
feed := f.Get()
err = encoder.Encode(feed)
if err != nil {
return fmt.Errorf("error encoding file %v: %v", filename, err)
}
return nil
}
func (f *Feed) Add(i *feeds.Item) {
f.addCh <- i
}

View File

@ -1,4 +1,4 @@
package control
package data
import (
"crypto/rand"
@ -17,7 +17,8 @@ type CookieStore struct {
func NewKey() ([]byte, error) {
key := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, key); err != nil {
_, err := io.ReadFull(rand.Reader, key)
if err != nil {
return nil, fmt.Errorf("error generating key: %v", err)
}
@ -32,7 +33,9 @@ func SaveKey(key []byte, filename string) error {
defer file.Close()
file.Chmod(0600)
if err = gob.NewEncoder(file).Encode(key); err != nil {
encoder := gob.NewEncoder(file)
err = encoder.Encode(key)
if err != nil {
return fmt.Errorf("error ecoding key: %v", err)
}
@ -46,7 +49,9 @@ func LoadKey(filename string) ([]byte, error) {
}
key := make([]byte, 32)
if err = gob.NewDecoder(file).Decode(&key); err != nil {
decoder := gob.NewDecoder(file)
err = decoder.Decode(&key)
if err != nil {
return nil, fmt.Errorf("error decoding key: %v", err)
}

16
cmd/data/user.go Normal file
View File

@ -0,0 +1,16 @@
package data
const (
Admin = iota
Editor
Writer
)
type User struct {
UserName string
FirstName string
LastName string
RejectedArticles []*Article
ID int64
Role int
}

View File

@ -1,75 +0,0 @@
package main
import (
"encoding/gob"
"log"
"net/http"
"os"
"streifling.com/jason/cpolis/cmd/control"
"streifling.com/jason/cpolis/cmd/model"
"streifling.com/jason/cpolis/cmd/view"
)
func init() {
gob.Register(model.User{})
}
func main() {
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)
db, err := model.OpenDB("cpolis")
if err != nil {
log.Fatalln(err)
}
defer db.Close()
key, err := control.LoadKey("tmp/key.gob")
if err != nil {
key, err = control.NewKey()
if err != nil {
log.Fatalln(err)
}
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("web/static/"))))
mux.HandleFunc("/", view.HomePage(db, store))
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 /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 /unpublished-articles/", view.ShowUnpublishedArticles(db))
mux.HandleFunc("GET /write-article/", view.WriteArticle(db))
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(":8080", mux))
}

View File

@ -1,97 +0,0 @@
package model
import (
"fmt"
"time"
)
type Article struct {
Title string
Created time.Time
Description string
Content string
Published bool
Rejected bool
ID int64
AuthorID int64
}
func (db *DB) AddArticle(a *Article) (int64, error) {
query := `
INSERT INTO articles
(title, description, content, published, rejected, author_id)
VALUES (?, ?, ?, ?, ?, ?)
`
result, err := db.Exec(query, a.Title, a.Description, a.Content,
a.Published, a.Rejected, a.AuthorID)
if err != nil {
return 0, fmt.Errorf("error inserting article into DB: %v", err)
}
id, err := result.LastInsertId()
if err != nil {
return 0, fmt.Errorf("error retrieving last ID: %v", err)
}
return id, nil
}
func (db *DB) GetArticle(id int64) (*Article, error) {
query := `
SELECT title, created, description, content, published, author_id
FROM articles
WHERE id = ?
`
row := db.QueryRow(query, id)
article := new(Article)
var created []byte
var err error
if err := row.Scan(&article.Title, &created, &article.Description,
&article.Content, &article.Published, &article.AuthorID); err != nil {
return nil, fmt.Errorf("error scanning article row: %v", err)
}
article.ID = id
article.Created, err = time.Parse("2006-01-02 15:04:05", string(created))
if err != nil {
return nil, fmt.Errorf("error parsing created: %v", err)
}
return article, nil
}
func (db *DB) GetCertainArticles(published, rejected bool) ([]*Article, error) {
query := `
SELECT id, title, created, description, content, author_id
FROM articles
WHERE published = ?
AND rejected = ?
`
rows, err := db.Query(query, published, rejected)
if err != nil {
return nil, fmt.Errorf("error querying articles: %v", err)
}
articleList := make([]*Article, 0)
for rows.Next() {
article := new(Article)
var created []byte
if err = rows.Scan(&article.ID, &article.Title, &created, &article.Description,
&article.Content, &article.AuthorID); err != nil {
return nil, fmt.Errorf("error scanning article row: %v", err)
}
article.Published = false
article.Created, err = time.Parse("2006-01-02 15:04:05", string(created))
if err != nil {
return nil, fmt.Errorf("error parsing created: %v", err)
}
articleList = append(articleList, article)
}
return articleList, nil
}

View File

@ -1,72 +0,0 @@
package model
import (
"fmt"
"log"
"math"
"math/rand/v2"
"time"
)
func (db *DB) WriteArticleTags(articleID int64, tagIDs []int64) error {
for i := 0; i < TxMaxRetries; i++ {
err := func() error {
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("error starting transaction: %v", err)
}
for _, tagID := range tagIDs {
query := `
INSERT INTO articles_tags (article_id, tag_id)
VALUES (?, ?)
`
if _, err := tx.Exec(query, articleID, tagID); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("error: transaction error: %v, rollback error: %v", err, rollbackErr)
}
return fmt.Errorf("error inserting into articles_tags: %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)
waitTime := time.Duration(math.Pow(2, float64(i))) * time.Second
jitter := time.Duration(rand.IntN(1000)) * time.Millisecond
time.Sleep(waitTime + jitter)
}
return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries)
}
func (db *DB) GetArticleTags(articleID int64) ([]*Tag, error) {
query := `
SELECT t.id, t.name
FROM articles a
INNER JOIN articles_tags at ON a.id = at.article_id
INNER JOIN tags t ON at.tag_id = t.id
WHERE a.id = ?
`
rows, err := db.Query(query, articleID)
if err != nil {
return nil, fmt.Errorf("error querying articles_tags: %v", err)
}
tags := make([]*Tag, 0)
for rows.Next() {
tag := new(Tag)
if err = rows.Scan(&tag.ID, &tag.Name); err != nil {
return nil, fmt.Errorf("error scanning rows: %v", err)
}
tags = append(tags, tag)
}
return tags, nil
}

View File

@ -1,187 +0,0 @@
package model
import (
"bufio"
"database/sql"
"fmt"
"log"
"math"
"math/rand/v2"
"os"
"strings"
"syscall"
"time"
"github.com/go-sql-driver/mysql"
"golang.org/x/term"
)
var TxMaxRetries = 3
type (
DB struct{ *sql.DB }
Tx struct{ *sql.Tx }
Attribute struct {
Value interface{}
Table string
AttName string
ID int64
}
)
func getUsername() (string, error) {
user := os.Getenv("DB_USER")
if user == "" {
var err error
fmt.Printf("DB Benutzer: ")
user, err = bufio.NewReader(os.Stdin).ReadString('\n')
if err != nil {
return "", fmt.Errorf("error reading username: %v", err)
}
}
return strings.TrimSpace(user), nil
}
func getPassword() (string, error) {
pass := os.Getenv("DB_PASS")
if pass == "" {
fmt.Printf("DB Passwort: ")
bytePass, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
return "", fmt.Errorf("error reading password: %v", err)
}
fmt.Println()
pass = strings.TrimSpace(string(bytePass))
}
return pass, nil
}
func getCredentials() (string, string, error) {
user, err := getUsername()
if err != nil {
return "", "", fmt.Errorf("error getting username: %v", err)
}
pass, err := getPassword()
if err != nil {
return "", "", fmt.Errorf("error getting password: %v", err)
}
return user, pass, nil
}
func OpenDB(dbName string) (*DB, error) {
var err error
db := DB{DB: &sql.DB{}}
cfg := mysql.NewConfig()
cfg.DBName = dbName
cfg.User, cfg.Passwd, err = getCredentials()
if err != nil {
return nil, fmt.Errorf("error reading user credentials for DB: %v", err)
}
db.DB, err = sql.Open("mysql", cfg.FormatDSN())
if err != nil {
return nil, fmt.Errorf("error opening DB: %v", err)
}
if err = db.Ping(); err != nil {
return nil, fmt.Errorf("error pinging DB: %v", err)
}
return &db, nil
}
func (db *DB) UpdateAttributes(a ...*Attribute) error {
for i := 0; i < TxMaxRetries; i++ {
err := func() error {
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("error starting transaction: %v", err)
}
for _, attribute := range a {
query := fmt.Sprintf(`
UPDATE %s
SET %s = ?
WHERE id = ?
`, attribute.Table, attribute.AttName)
if _, err := tx.Exec(query, attribute.Value, attribute.ID); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("error: transaction error: %v, rollback error: %v", err, rollbackErr)
}
return fmt.Errorf("error updating %v in DB: %v", attribute.AttName, 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)
waitTime := time.Duration(math.Pow(2, float64(i))) * time.Second
jitter := time.Duration(rand.IntN(1000)) * time.Millisecond
time.Sleep(waitTime + jitter)
}
return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries)
}
func (db *DB) CountEntries(table string) (int64, error) {
var count int64
query := fmt.Sprintf("SELECT COUNT(*) FROM %s", table)
row := db.QueryRow(query)
if err := row.Scan(&count); err != nil {
return 0, fmt.Errorf("error counting rows in user DB: %v", err)
}
return count, nil
}
func (db *DB) StartTransaction() (*Tx, error) {
tx := &Tx{Tx: new(sql.Tx)}
var err error
tx.Tx, err = db.Begin()
if err != nil {
return nil, fmt.Errorf("error starting transaction: %v", err)
}
return tx, nil
}
func (tx *Tx) CommitTransaction() error {
if err := tx.Commit(); err != nil {
return fmt.Errorf("error committing transaction: %v", err)
}
return nil
}
func (tx *Tx) RollbackTransaction() {
if err := tx.Rollback(); err != nil {
log.Fatalf("error rolling back transaction: %v", err)
}
}
func (tx *Tx) UpdateAttributes(a ...*Attribute) error {
for _, attribute := range a {
query := fmt.Sprintf(`
UPDATE %s
SET %s = ?
WHERE id = ?
`, attribute.Table, attribute.AttName)
if _, err := tx.Exec(query, attribute.Value, attribute.ID); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("error: transaction error: %v, rollback error: %v", err, rollbackErr)
}
return fmt.Errorf("error updating %v in DB: %v", attribute.AttName, err)
}
}
return nil
}

View File

@ -1,35 +0,0 @@
package model
import "fmt"
type Tag struct {
Name string
ID int64
}
func (db *DB) AddTag(tagName string) error {
query := "INSERT INTO tags (name) VALUES (?)"
if _, err := db.Exec(query, tagName); err != nil {
return fmt.Errorf("error inserting tag into DB: %v", err)
}
return nil
}
func (db *DB) GetTagList() ([]*Tag, error) {
query := "SELECT id, name FROM tags"
rows, err := db.Query(query)
if err != nil {
return nil, fmt.Errorf("error querying tags: %v", err)
}
tagList := make([]*Tag, 0)
for rows.Next() {
tag := new(Tag)
if err = rows.Scan(&tag.ID, &tag.Name); err != nil {
return nil, fmt.Errorf("error scanning tag row: %v", err)
}
tagList = append(tagList, tag)
}
return tagList, nil
}

View File

@ -1,198 +0,0 @@
package model
import (
"fmt"
"log"
"math"
"math/rand/v2"
"time"
"golang.org/x/crypto/bcrypt"
)
const (
Admin = iota
Editor
Writer
)
type User struct {
UserName string
FirstName string
LastName string
ID int64
Role int
}
func (db *DB) AddUser(user *User, pass string) error {
hashedPass, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("error creating password hash: %v", err)
}
query := `
INSERT INTO users (username, password, first_name, last_name, role)
VALUES (?, ?, ?, ?, ?)
`
if _, err = db.Exec(query, user.UserName, string(hashedPass), user.FirstName, user.LastName, user.Role); err != nil {
return fmt.Errorf("error inserting user into DB: %v", err)
}
return nil
}
func (db *DB) GetID(userName string) (int64, bool) {
var id int64
query := `
SELECT id
FROM users
WHERE username = ?
`
row := db.QueryRow(query, userName)
if err := row.Scan(&id); err != nil {
return 0, false
}
return id, true
}
func (db *DB) CheckPassword(id int64, pass string) error {
var queriedPass string
query := `
SELECT password
FROM users
WHERE id = ?
`
row := db.QueryRow(query, id)
if err := row.Scan(&queriedPass); err != nil {
return fmt.Errorf("error reading password from DB: %v", err)
}
if err := bcrypt.CompareHashAndPassword([]byte(queriedPass), []byte(pass)); err != nil {
return fmt.Errorf("incorrect password: %v", err)
}
return nil
}
func (tx *Tx) ChangePassword(id int64, oldPass, newPass string) error {
var queriedPass string
getQuery := `
SELECT password
FROM users
WHERE id = ?
`
row := tx.QueryRow(getQuery, id)
if err := row.Scan(&queriedPass); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("error: transaction error: %v, rollback error: %v", err, rollbackErr)
}
return fmt.Errorf("error reading password from DB: %v", err)
}
if err := bcrypt.CompareHashAndPassword([]byte(queriedPass), []byte(oldPass)); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("error: transaction error: %v, rollback error: %v", err, rollbackErr)
}
return fmt.Errorf("incorrect password: %v", err)
}
newHashedPass, err := bcrypt.GenerateFromPassword([]byte(newPass), bcrypt.DefaultCost)
if err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("error: 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(newHashedPass), id); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("error: transaction error: %v, rollback error: %v", err, rollbackErr)
}
return fmt.Errorf("error updating password in DB: %v", err)
}
return nil
}
// TODO: No need for ID field in general
func (db *DB) GetUser(id int64) (*User, error) {
user := new(User)
query := `
SELECT id, username, first_name, last_name, role
FROM users
WHERE id = ?
`
row := db.QueryRow(query, id)
if err := row.Scan(&user.ID, &user.UserName, &user.FirstName, &user.LastName, &user.Role); err != nil {
return nil, fmt.Errorf("error reading user information: %v", err)
}
return user, nil
}
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 {
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.ChangePassword(id, oldPass, newPass); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("error: 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},
); err != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("error: 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)
waitTime := time.Duration(math.Pow(2, float64(i))) * time.Second
jitter := time.Duration(rand.IntN(1000)) * time.Millisecond
time.Sleep(waitTime + jitter)
}
return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries)
}

105
cmd/ui/admin.go Normal file
View File

@ -0,0 +1,105 @@
package ui
import (
"fmt"
"html/template"
"log"
"net/http"
"strconv"
"streifling.com/jason/cpolis/cmd/data"
)
type AddUserData struct {
Msg string
data.User
}
func inputsEmpty(user data.User, pass, pass2 string) bool {
return len(user.UserName) == 0 ||
len(user.FirstName) == 0 ||
len(user.LastName) == 0 ||
len(pass) == 0 ||
len(pass2) == 0
}
func checkUserStrings(user data.User) (string, int, bool) {
userLen := 15
nameLen := 50
if len(user.UserName) > userLen {
return "Benutzername", userLen, false
} else if len(user.FirstName) > nameLen {
return "Vorname", nameLen, false
} else if len(user.LastName) > nameLen {
return "Nachname", nameLen, false
} else {
return "", 0, true
}
}
func CreateUser() http.HandlerFunc {
return func(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(db *data.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
role, err := strconv.Atoi(r.PostFormValue("role"))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
htmlData := AddUserData{
User: data.User{
UserName: r.PostFormValue("username"),
FirstName: r.PostFormValue("first-name"),
LastName: r.PostFormValue("last-name"),
Role: role,
},
}
pass := r.PostFormValue("password")
pass2 := r.PostFormValue("password2")
if inputsEmpty(htmlData.User, pass, pass2) {
htmlData.Msg = "Alle Felder müssen ausgefüllt werden."
tmpl, err := template.ParseFiles("web/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("web/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("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("web/templates/add-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData)
return
}
if err := db.AddUser(htmlData.User, pass); 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", nil)
}
}

153
cmd/ui/articles.go Normal file
View File

@ -0,0 +1,153 @@
package ui
import (
"html/template"
"log"
"net/http"
"time"
"github.com/google/uuid"
"github.com/gorilla/feeds"
"streifling.com/jason/cpolis/cmd/data"
)
func ShowHub(s *data.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := s.Get(r, "cookie")
if err != nil {
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("web/templates/hub.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", session.Values["role"])
}
}
func WriteArticle(w http.ResponseWriter, r *http.Request) {
tmpl, err := template.ParseFiles("web/templates/editor.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil)
}
func FinishArticle(l *data.ArticleList, s *data.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
article := new(data.Article)
var err error
article.Title, err = data.ConvertToPlain(r.PostFormValue("editor-title"))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
article.Desc, err = data.ConvertToPlain(r.PostFormValue("editor-desc"))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
article.Content, err = data.ConvertToHTML(r.PostFormValue("editor-text"))
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("web/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
article.UUID = uuid.New()
article.Author = session.Values["name"].(string)
article.Created = time.Now()
article.AuthorID = session.Values["id"].(int64)
l.Add(article)
tmpl, err := template.ParseFiles("web/templates/hub.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
}
}
func ShowUnpublishedArticles(l *data.ArticleList) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
tmpl, err := template.ParseFiles("web/templates/unpublished-articles.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", l.Get())
}
}
func ReviewArticle(l *data.ArticleList, s *data.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
uuid, err := uuid.Parse(r.PostFormValue("uuid"))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
for _, article := range l.Get() {
if article.UUID == uuid {
tmpl, err := template.ParseFiles("web/templates/to-be-published.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", article)
return
}
}
session, err := s.Get(r, "cookie")
if err != nil {
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("web/templates/hub.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
}
}
func PublishArticle(f *data.Feed, l *data.ArticleList, s *data.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
uuid, err := uuid.Parse(r.PostFormValue("uuid"))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
article, ok := l.Release(uuid)
if !ok {
// TODO: Warnung anzeigen
// msg = "Alle Felder müssen ausgefüllt werden."
// tmpl, err := template.ParseFiles("web/templates/add-user.html")
// template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
return
}
f.Add(&feeds.Item{
Title: article.Title,
Created: article.Created,
Description: article.Desc,
Content: article.Content,
})
f.Save("tmp/rss.gob")
session, err := s.Get(r, "cookie")
if err != nil {
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("web/templates/hub.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
}
}

25
cmd/ui/rss.go Normal file
View File

@ -0,0 +1,25 @@
package ui
import (
"html/template"
"log"
"net/http"
"streifling.com/jason/cpolis/cmd/data"
)
func ShowRSS(f *data.Feed) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
feed := f.Get()
rss, err := feed.ToRss()
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

@ -1,35 +1,16 @@
package view
package ui
import (
"fmt"
"html/template"
"log"
"net/http"
"streifling.com/jason/cpolis/cmd/control"
"streifling.com/jason/cpolis/cmd/model"
"streifling.com/jason/cpolis/cmd/data"
)
func saveSession(w http.ResponseWriter, r *http.Request, s *control.CookieStore, u *model.User) error {
session, err := s.Get(r, "cookie")
if err != nil {
return fmt.Errorf("error getting session: %v", err)
}
session.Values["authenticated"] = true
session.Values["id"] = u.ID
session.Values["name"] = u.FirstName + u.LastName
session.Values["role"] = u.Role
if err := session.Save(r, w); err != nil {
return fmt.Errorf("error saving session: %v", err)
}
return nil
}
func HomePage(db *model.DB, s *control.CookieStore) http.HandlerFunc {
func HomePage(db *data.DB, s *data.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
numRows, err := db.CountEntries("users")
numRows, err := db.CountEntries()
if err != nil {
log.Fatalln(err)
}
@ -54,14 +35,15 @@ func HomePage(db *model.DB, s *control.CookieStore) http.HandlerFunc {
}
}
func Login(db *model.DB, s *control.CookieStore) http.HandlerFunc {
func Login(db *data.DB, s *data.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userName := r.PostFormValue("username")
password := r.PostFormValue("password")
id, ok := db.GetID(userName)
if !ok {
http.Error(w, fmt.Sprintf("no such user: %v", userName), http.StatusInternalServerError)
id, err := db.GetID(userName)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@ -78,7 +60,18 @@ func Login(db *model.DB, s *control.CookieStore) http.HandlerFunc {
return
}
if err := saveSession(w, r, s, user); err != nil {
session, err := s.Get(r, "cookie")
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
session.Values["authenticated"] = true
session.Values["id"] = user.ID
session.Values["name"] = user.FirstName + user.LastName
session.Values["role"] = user.Role
if err := session.Save(r, w); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -88,25 +81,3 @@ func Login(db *model.DB, s *control.CookieStore) http.HandlerFunc {
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", user.Role)
}
}
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("web/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
session.Options.MaxAge = -1
if err = session.Save(r, w); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles("web/templates/login.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", nil)
}
}

View File

@ -1,319 +0,0 @@
package view
import (
"html/template"
"log"
"net/http"
"strconv"
"time"
"streifling.com/jason/cpolis/cmd/control"
"streifling.com/jason/cpolis/cmd/model"
)
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("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("web/templates/hub.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", session.Values["role"].(int))
}
}
func WriteArticle(db *model.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
tags, err := db.GetTagList()
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles("web/templates/editor.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", tags)
}
}
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("web/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
article := &model.Article{
Title: r.PostFormValue("article-title"),
Description: r.PostFormValue("article-description"),
Content: r.PostFormValue("article-content"),
Published: false,
Rejected: false,
AuthorID: session.Values["id"].(int64),
}
article.ID, err = db.AddArticle(article)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
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.WriteArticleTags(article.ID, tags); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles("web/templates/hub.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
}
}
func ResubmitArticle(db *model.DB, s *control.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PostFormValue("article-id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
title := r.PostFormValue("article-title")
description := r.PostFormValue("article-description")
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},
); 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("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("web/templates/hub.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
}
}
func ShowUnpublishedArticles(db *model.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
unpublishedArticles, err := db.GetCertainArticles(false, false)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles("web/templates/unpublished-articles.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", unpublishedArticles)
}
}
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
RejectedArticles []*model.Article
}
data := new(htmlData)
session, err := s.Get(r, "cookie")
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.RejectedArticles, err = db.GetCertainArticles(false, true)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.MyIDs = make(map[int64]bool)
for _, article := range data.RejectedArticles {
if article.AuthorID == session.Values["id"].(int64) {
data.MyIDs[article.ID] = true
}
}
tmpl, err := template.ParseFiles("web/templates/rejected-articles.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", data)
}
}
func ReviewUnpublishedArticle(db *model.DB, s *control.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
type htmlData struct {
Article *model.Article
Tags []*model.Tag
}
data := new(htmlData)
id, err := strconv.ParseInt(r.PostFormValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Article, err = db.GetArticle(id)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Tags, err = db.GetArticleTags(id)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles("web/templates/to-be-published.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", data)
}
}
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
Article *model.Article
Tags []*model.Tag
}
data := new(htmlData)
id, err := strconv.ParseInt(r.PostFormValue("id"), 10, 64)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Article, err = db.GetArticle(id)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Tags, err = db.GetTagList()
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
selectedTags, err := db.GetArticleTags(id)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data.Selected = make(map[int64]bool)
for _, tag := range selectedTags {
data.Selected[tag.ID] = true
}
tmpl, err := template.ParseFiles("web/templates/rework-article.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", data)
}
}
func PublishArticle(db *model.DB, s *control.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PostFormValue("id"), 10, 64)
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("web/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
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")},
); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles("web/templates/hub.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
}
}
func RejectArticle(db *model.DB, s *control.CookieStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PostFormValue("id"), 10, 64)
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("web/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
if err = db.UpdateAttributes(
&model.Attribute{Table: "articles", ID: id, AttName: "rejected", Value: true},
); err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles("web/templates/hub.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
}
}

View File

@ -1,31 +0,0 @@
package view
import (
"html/template"
"net/http"
"streifling.com/jason/cpolis/cmd/control"
"streifling.com/jason/cpolis/cmd/model"
)
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(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("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("web/templates/hub.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"])
}
}

View File

@ -1,93 +0,0 @@
package view
import (
"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)
}
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

@ -1,212 +0,0 @@
package view
import (
"fmt"
"html/template"
"log"
"net/http"
"strconv"
"streifling.com/jason/cpolis/cmd/control"
"streifling.com/jason/cpolis/cmd/model"
)
type UserData struct {
*model.User
Msg string
}
func checkUserStrings(user *model.User) (string, int, bool) {
userLen := 15
nameLen := 50
if len(user.UserName) > userLen {
return "Benutzername", userLen, false
} else if len(user.FirstName) > nameLen {
return "Vorname", nameLen, false
} else if len(user.LastName) > nameLen {
return "Nachname", nameLen, false
} else {
return "", 0, true
}
}
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(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 {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
htmlData := UserData{
User: &model.User{
UserName: r.PostFormValue("username"),
FirstName: r.PostFormValue("first-name"),
LastName: r.PostFormValue("last-name"),
Role: role,
},
}
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("web/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("web/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("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("web/templates/add-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", htmlData)
return
}
num, err := db.CountEntries("users")
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if num == 0 {
if htmlData.Role != model.Admin {
htmlData.Msg = "Der erste Benutzer muss ein Administrator sein."
htmlData.Role = model.Admin
tmpl, err := template.ParseFiles("web/templates/add-user.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", htmlData)
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.AddUser(htmlData.User, pass); 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 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("web/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
user, err := db.GetUser(session.Values["id"].(int64))
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl, err := template.ParseFiles("web/templates/edit-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", user)
}
}
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("web/templates/login.html")
msg := "Session nicht mehr gültig. Bitte erneut anmelden."
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", msg)
}
userData := UserData{
User: &model.User{
ID: session.Values["id"].(int64),
UserName: r.PostFormValue("username"),
FirstName: r.PostFormValue("first-name"),
LastName: r.PostFormValue("last-name"),
},
}
oldPass := r.PostFormValue("old-password")
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("web/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("web/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("web/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,
oldPass,
newPass,
newPass2); err != nil {
userData.Msg = "Aktualisierung der Benutzerdaten fehlgeschlagen."
tmpl, err := template.ParseFiles("web/templates/edit-user.html")
template.Must(tmpl, err).ExecuteTemplate(w, "page-content", userData)
}
tmpl, err := template.ParseFiles("web/templates/hub.html")
tmpl = template.Must(tmpl, err)
tmpl.ExecuteTemplate(w, "page-content", session.Values["role"].(int))
}
}

3
go.mod
View File

@ -3,8 +3,9 @@ module streifling.com/jason/cpolis
go 1.22.0
require (
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/feeds v1.1.2
github.com/gorilla/sessions v1.2.2
github.com/microcosm-cc/bluemonday v1.0.26
github.com/yuin/goldmark v1.7.0

12
go.sum
View File

@ -1,19 +1,27 @@
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/feeds v1.1.2 h1:pxzZ5PD3RJdhFH2FsJJ4x6PqMqbgFk1+Vez4XWBW8Iw=
github.com/gorilla/feeds v1.1.2/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=

68
main.go Normal file
View File

@ -0,0 +1,68 @@
package main
import (
"encoding/gob"
"log"
"net/http"
"os"
"streifling.com/jason/cpolis/cmd/data"
"streifling.com/jason/cpolis/cmd/ui"
)
func init() {
gob.Register(data.User{})
}
func main() {
logFile, err := os.Create("tmp/cpolis.log")
if err != nil {
log.Fatalln(err)
}
defer logFile.Close()
log.SetOutput(logFile)
db, err := data.OpenDB("cpolis")
if err != nil {
log.Fatalln(err)
}
defer db.Close()
feed, err := data.OpenFeed("tmp/rss.gob")
if err != nil {
log.Println(err)
feed = data.NewFeed("Freimaurer Distrikt Niedersachsen und Sachsen-Anhalt",
"https://distrikt-ni-st.de",
"Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität")
}
key, err := data.LoadKey("tmp/key.gob")
if err != nil {
key, err = data.NewKey()
if err != nil {
log.Fatalln(err)
}
data.SaveKey(key, "tmp/key.gob")
}
store := data.NewCookieStore(key)
articleList := data.NewArticleList()
mux := http.NewServeMux()
mux.Handle("/web/static/", http.StripPrefix("/web/static/", http.FileServer(http.Dir("web/static/"))))
mux.HandleFunc("/", ui.HomePage(db, store))
mux.HandleFunc("GET /hub/", ui.ShowHub(store))
mux.HandleFunc("GET /rss/", ui.ShowRSS(feed))
mux.HandleFunc("POST /add-user/", ui.AddUser(db))
mux.HandleFunc("POST /create-user/", ui.CreateUser())
mux.HandleFunc("POST /finish-article/", ui.FinishArticle(articleList, store))
mux.HandleFunc("POST /login/", ui.Login(db, store))
mux.HandleFunc("POST /review-article/", ui.ReviewArticle(articleList, store))
mux.HandleFunc("POST /publish-article/", ui.PublishArticle(feed, articleList, store))
mux.HandleFunc("POST /unpublished-articles/", ui.ShowUnpublishedArticles(articleList))
mux.HandleFunc("POST /write-article/", ui.WriteArticle)
log.Fatalln(http.ListenAndServe(":8080", mux))
}

View File

@ -1,8 +0,0 @@
{{define "page-content"}}
<h2>Neuer Benutzer</h2>
<form>
<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

@ -8,16 +8,15 @@
<input required name="first-name" placeholder="Vorname" type="text" value="{{.FirstName}}" />
<input required name="last-name" placeholder="Nachname" type="text" value="{{.LastName}}" />
<input required id="writer" name="role" type="radio" value="2" {{if eq .Role 2 }}checked{{end}} />
<label for="writer">Schreiber</label>
<input required id="editor" name="role" type="radio" value="1" {{if eq .Role 1 }}checked{{end}} />
<input required id="writer" name="role" type="radio" value="2" {{if eq .Role "2" }}checked{{end}} />
<label for="editor">Redakteur</label>
<input required id="admin" name="role" type="radio" value="0" {{if eq .Role 0 }}checked{{end}} />
<input required id="editor" name="role" type="radio" value="1" {{if eq .Role "1" }}checked{{end}} />
<label for="admin">Admin</label>
<input required id="admin" name="role" type="radio" value="0" {{if eq .Role "0" }}checked{{end}} />
<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,13 +0,0 @@
{{define "page-content"}}
<form>
<input name="username" type="text" value="{{.UserName}}" />
<input name="first-name" type="text" value="{{.FirstName}}" />
<input name="last-name" type="text" value="{{.LastName}}" />
<input name="old-password" placeholder="Altes Passwort" type="password" />
<input name="password" placeholder="Neues Passwort" type="password" />
<input name="password2" placeholder="Wiederholen" type="password" />
<input type="submit" value="Aktualisieren" hx-post="/update-user/" hx-target="#page-content" />
</form>
{{end}}

View File

@ -1,13 +1,13 @@
{{define "page-content"}}
<h2>Editor</h2>
<form>
<input name="article-title" placeholder="Titel" type="text" />
<textarea name="article-description" placeholder="Beschreibung"></textarea>
<textarea name="article-content" placeholder="Artikel"></textarea>
{{range .}}
<input id="{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" />
<label for="{{.Name}}">{{.Name}}</label>
{{end}}
<input type="submit" value="Senden" hx-post="/submit-article/" hx-target="#page-content" />
<input name="editor-title" placeholder="Titel" type="text" />
<textarea name="editor-desc" placeholder="Beschreibung"></textarea>
<textarea name="editor-text" placeholder="Artikel"></textarea>
<input type="submit" value="Senden" hx-post="/finish-article/" hx-target="#page-content" />
</form>
{{end}}
{{define "html-result"}}
{{.}}
{{end}}

View File

@ -1,14 +1,11 @@
{{define "page-content"}}
<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 . 2}}
<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}}
<button hx-post="/write-article/" hx-target="#page-content">Artikel schreiben</button>
<button hx-post="/rss/" hx-target="#page-content">RSS Feed</button>
{{if eq . 0}}
<button hx-get="/create-user/" hx-target="#page-content">Benutzer hinzufügen</button>
<button hx-post="/create-user/" hx-target="#page-content">Benutzer hinzufügen</button>
{{end}}
{{if lt . 2}}
<button hx-post="/unpublished-articles/" hx-target="#page-content">Unveröffentlichte Artikel</button>
{{end}}
{{end}}

View File

@ -9,22 +9,13 @@
</head>
<body>
<header>
<h1>Orient Editor</h1>
<button hx-get="logout" hx-target="#page-content">Abmelden</button>
</header>
<h1>Orient Editor</h1>
<main>
<div id="page-content">
{{template "page-content" .}}
</div>
<div id="page-content">
{{template "page-content" .}}
</div>
<script src="web/static/js/htmx.min.js"></script>
</main>
<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 +0,0 @@
{{define "page-content"}}
<form>
{{range .RejectedArticles}}
{{if index $.MyIDs .ID}}
<input required id="{{.ID}}" name="id" type="radio" value="{{.ID}}" />
<label for="{{.ID}}">{{.Title}}</label>
{{end}}
{{end}}
<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,16 +0,0 @@
{{define "page-content"}}
<h2>Editor</h2>
<form>
<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>
<input name="article-id" type="hidden" value="{{.Article.ID}}" />
{{range .Tags}}
<input id="tag-{{.Name}}" name="tags" type="checkbox" value="{{.ID}}" {{if index $.Selected .ID}}checked{{end}} />
<label for="tag-{{.Name}}">{{.Name}}</label>
{{end}}
<input type="submit" value="Senden" hx-post="/resubmit-article/" hx-target="#page-content" />
</form>
{{end}}

View File

@ -1,19 +1,11 @@
{{define "page-content"}}
<form>
<h2>{{.Article.Title}}</h2>
<p>{{.Article.Description}}</p>
{{.Article.Content}}
<p>
{{range .Tags}}
{{.Name}}
{{end}}
</p>
<input name="id" type="hidden" value="{{.Article.ID}}" />
<input name="editor-title" type="text" value="{{.Title}}" />
<textarea name="editor-desc">{{.Desc}}</textarea>
<textarea name="editor-text">{{.Content}}</textarea>
<input name="uuid" type="hidden" value="{{.UUID}}" />
<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,10 +1,10 @@
{{define "page-content"}}
<form>
{{range .}}
<input required id="{{.ID}}" name="id" type="radio" value="{{.ID}}" />
<label for="{{.ID}}">{{.Title}}</label>
<input required id="{{.UUID}}" name="uuid" type="radio" value="{{.UUID}}" />
<label for="{{.UUID}}">{{.Title}}</label>
{{end}}
<input type="submit" value="Auswählen" hx-post="/review-unpublished-article/" hx-target="#page-content" />
<input type="submit" value="Auswählen" hx-post="/review-article/" hx-target="#page-content" />
</form>
<button hx-get="/hub/" hx-target="#page-content">Zurück</button>
{{end}}