mysql db for session storage

This commit is contained in:
Timon Ringwald 2022-09-06 11:22:28 +02:00
parent d9301099c6
commit a6d99c0ccc
8 changed files with 242 additions and 104 deletions

View File

@ -78,7 +78,9 @@ func DefaultBookmarks() []Bookmark {
} }
type Bookmark struct { type Bookmark struct {
Title string `json:"title"` SessionID string `json:"-"`
Title string `json:"title"`
Image string `json:"image"` Image string `json:"image"`
ImageSize string `json:"image_size"` ImageSize string `json:"image_size"`
IconPadding string `json:"icon_padding"` IconPadding string `json:"icon_padding"`

90
db.go Normal file
View File

@ -0,0 +1,90 @@
package main
import (
"time"
"git.milar.in/milarin/advsql"
)
var Database = &advsql.Database{}
var (
InsertSession = advsql.Insert(Database, "INSERT INTO sessions VALUES (?, ?, ?)", ScanSessionPkFirst)
UpdateSession = advsql.Update(Database, "UPDATE sessions SET expiration_date = ?, creation_date = ? WHERE id = ?", ScanSessionPkLast)
GetSessionByID = advsql.QueryOne(Database, "SELECT * FROM sessions WHERE id = ?", ScanSessionPkFirst)
InsertBookmark = advsql.Insert(Database, "INSERT INTO bookmarks VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", ScanBookmarkPkFirst)
UpdateBookmark = advsql.Update(Database, "UPDATE bookmarks SET link = ?, image = ?, color = ?, image_size = ?, icon_padding = ?, hide_border = ?, order_priority = ? WHERE session_id = ? AND title = ?", ScanBookmarkPkLast)
GetBookmarkBySessionIDAndTitle = advsql.QueryOne(Database, "SELECT * FROM bookmarks WHERE session_id = ? AND title = ?", ScanBookmarkPkFirst)
GetBookmarksBySessionIdOrdered = advsql.QueryMany(Database, "SELECT * FROM bookmarks WHERE session_id = ? ORDER BY order_priority", ScanBookmarkPkFirst)
DeleteBookmarksBySessionID = advsql.Delete(Database, "DELETE FROM bookmarks WHERE session_id = ?")
InsertSettings = advsql.Insert(Database, "INSERT INTO settings VALUES (?, ?, ?, ?, ?)", ScanSettingsPkFirst)
UpdateSettings = advsql.Update(Database, "UPDATE settings SET background_color = ?, foreground_color = ?, search_query = ?, border_radius = ? WHERE session_id = ?", ScanSettingsPkLast)
GetSettingsBySessionID = advsql.QueryOne(Database, "SELECT * FROM settings WHERE session_id = ?", ScanSettingsPkFirst)
)
type Session struct {
ID string
ExpirationDate time.Time
CreationDate time.Time
}
func ScanSessionPkFirst(u *Session, encode advsql.ScanFunc) error {
return encode(&u.ID, &u.ExpirationDate, &u.CreationDate)
}
func ScanSessionPkLast(u *Session, encode advsql.ScanFunc) error {
return encode(&u.ExpirationDate, &u.CreationDate, &u.ID)
}
func NewSession() (*Session, error) {
id, err := generateRandomID(32)
if err != nil {
return nil, err
}
return &Session{
ID: id,
CreationDate: time.Now(),
ExpirationDate: time.Now().AddDate(1, 0, 0),
}, nil
}
func ScanBookmarkPkFirst(u *Bookmark, encode advsql.ScanFunc) error {
return encode(
&u.SessionID,
&u.Title,
&u.Link,
&u.Image,
&u.Color,
&u.ImageSize,
&u.IconPadding,
&u.HideBorder,
&u.Order,
)
}
func ScanBookmarkPkLast(u *Bookmark, encode advsql.ScanFunc) error {
return encode(
&u.Link,
&u.Image,
&u.Color,
&u.ImageSize,
&u.IconPadding,
&u.HideBorder,
&u.Order,
&u.SessionID,
&u.Title,
)
}
func ScanSettingsPkFirst(u *Settings, encode advsql.ScanFunc) error {
return encode(&u.SessionID, &u.Background, &u.Foreground, &u.Search, &u.BorderRadius)
}
func ScanSettingsPkLast(u *Settings, encode advsql.ScanFunc) error {
return encode(&u.Background, &u.Foreground, &u.Search, &u.BorderRadius, &u.SessionID)
}

10
go.mod
View File

@ -3,15 +3,11 @@ module git.milar.in/milarin/startpage
go 1.18 go 1.18
require ( require (
git.milar.in/milarin/advsql v0.0.16
git.milar.in/milarin/channel v0.0.10
git.milar.in/milarin/envvars/v2 v2.0.0 git.milar.in/milarin/envvars/v2 v2.0.0
git.milar.in/milarin/slices v0.0.4
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0
github.com/gorilla/sessions v1.2.1
github.com/srinathgs/mysqlstore v0.0.0-20200417050510-9cbb9420fc4c
golang.org/x/text v0.3.7 golang.org/x/text v0.3.7
) )
require ( require github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
)

12
go.sum
View File

@ -1,16 +1,12 @@
git.milar.in/milarin/advsql v0.0.16 h1:vPGPM4Vyb8aR8qcj479JXpC+MlUjeFsPZ6CUIoKkC+I=
git.milar.in/milarin/advsql v0.0.16/go.mod h1:bdQrtqZaGkxmbYRi2OhF5T8DDDztnvbE41faIVSZPLQ=
git.milar.in/milarin/channel v0.0.10 h1:oHCx69lMF/KIot38OgfVtZ9oBmv5FadZp2MMDPPnC3o=
git.milar.in/milarin/channel v0.0.10/go.mod h1:We83LTI8S7u7II3pD+A2ChCDWJfCkcBUCUqii9HjTtM=
git.milar.in/milarin/envvars/v2 v2.0.0 h1:DWRQCWaHqzDD8NGpSgv5tYLuF9A/dVFPAtTvz3oiIqE= git.milar.in/milarin/envvars/v2 v2.0.0 h1:DWRQCWaHqzDD8NGpSgv5tYLuF9A/dVFPAtTvz3oiIqE=
git.milar.in/milarin/envvars/v2 v2.0.0/go.mod h1:HkdEi+gG2lJSmVq547bTlQV4qQ0hO333bE8IrE0B9yY= git.milar.in/milarin/envvars/v2 v2.0.0/go.mod h1:HkdEi+gG2lJSmVq547bTlQV4qQ0hO333bE8IrE0B9yY=
git.milar.in/milarin/slices v0.0.4 h1:z92jgsKcnLPLfgXkTVCzH2XXesfXzhe0Osx+PkfCHVI=
git.milar.in/milarin/slices v0.0.4/go.mod h1:NOr53AOeur/qscu/FBj3lsFR262PNYBccLYSTCAXRk4=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/srinathgs/mysqlstore v0.0.0-20200417050510-9cbb9420fc4c h1:HT6QRF79dL2Ed6HCrX9RufkxFGo7+NPkgYF1Uzvv/js=
github.com/srinathgs/mysqlstore v0.0.0-20200417050510-9cbb9420fc4c/go.mod h1:kt46Hd+lF0rtpeRgOvYSWYJItOAd73EKkIBZFbX7TXs=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=

137
main.go
View File

@ -2,31 +2,20 @@ package main
import ( import (
"embed" "embed"
"encoding/gob"
"encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"flag"
"fmt" "fmt"
"html/template" "html/template"
"io"
"net/http" "net/http"
"net/url" "net/url"
"os"
"strings" "strings"
"time"
_ "embed" _ "embed"
"git.milar.in/milarin/advsql"
"git.milar.in/milarin/channel"
"git.milar.in/milarin/envvars/v2" "git.milar.in/milarin/envvars/v2"
"git.milar.in/milarin/slices"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/srinathgs/mysqlstore"
)
var (
intf = flag.String("i", "", "interface")
port = flag.Uint("p", 80, "port")
) )
var ( var (
@ -37,28 +26,18 @@ var (
StaticFS embed.FS StaticFS embed.FS
Templates *template.Template Templates *template.Template
SessionStore = Must(mysqlstore.NewMySQLStore(
fmt.Sprintf(
"%s:%s@tcp(%s:%d)/%s?parseTime=true&loc=Local",
envvars.String("DB_USER", ""),
envvars.String("DB_PASS", ""),
envvars.String("DB_HOST", ""),
envvars.Uint16("DB_PORT", 3306),
envvars.String("DB_BASE", "startpage"),
),
"sessions",
"/",
int((time.Hour * 24 * 365).Seconds()),
Must(hex.DecodeString(os.Getenv("SESSION_KEY"))),
))
) )
func main() { func main() {
gob.Register([]Bookmark{}) advsql.InitMysqlDatabase(
gob.Register(&Settings{}) Database,
envvars.String("DB_HOST", ""),
flag.Parse() envvars.Uint16("DB_PORT", 3306),
envvars.String("DB_USER", ""),
envvars.String("DB_PASS", ""),
envvars.String("DB_BASE", "startpage"),
)
defer Database.Close()
if tmpl, err := template.New("homepage").ParseFS(TemplateFS, "templates/*"); err == nil { if tmpl, err := template.New("homepage").ParseFS(TemplateFS, "templates/*"); err == nil {
Templates = tmpl Templates = tmpl
@ -73,18 +52,23 @@ func main() {
r.HandleFunc("/search", search) r.HandleFunc("/search", search)
r.PathPrefix("/static/").Handler(http.FileServer(http.FS(StaticFS))) r.PathPrefix("/static/").Handler(http.FileServer(http.FS(StaticFS)))
if err := http.ListenAndServe(fmt.Sprintf("%s:%d", *intf, *port), r); err != nil { if err := http.ListenAndServe(fmt.Sprintf("%s:%d", envvars.String("HTTP_INTF", ""), envvars.Uint16("HTTP_PORT", 80)), r); err != nil {
panic(err) panic(err)
} }
} }
func handler(w http.ResponseWriter, r *http.Request) { func handler(w http.ResponseWriter, r *http.Request) {
session, _ := SessionStore.Get(r, "settings") session, err := GetSession(w, r)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Println(err)
return
}
data := &TmplData{ data := &TmplData{
text: GetText(r), text: GetText(r),
Bookmarks: GetValueDefault(session, "bookmarks", DefaultBookmarks()), Bookmarks: channel.ToSliceDeref(GetBookmarksBySessionIdOrdered(session.ID)),
Settings: GetValueDefault(session, "settings", DefaultSettings()), Settings: GetSettingsBySessionID(session.ID),
} }
if err := Templates.ExecuteTemplate(w, "index.html", data); err != nil { if err := Templates.ExecuteTemplate(w, "index.html", data); err != nil {
@ -92,23 +76,14 @@ func handler(w http.ResponseWriter, r *http.Request) {
} }
} }
func ProvideFile(path, contentType string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
file, err := StaticFS.Open(path)
if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
defer file.Close()
w.Header().Add("Content-Type", contentType)
io.Copy(w, file)
}
}
func search(w http.ResponseWriter, r *http.Request) { func search(w http.ResponseWriter, r *http.Request) {
session, _ := SessionStore.Get(r, "settings") session, err := GetSession(w, r)
settings := GetValueDefault(session, "settings", DefaultSettings()) if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Println(err)
return
}
settings := GetSettingsBySessionID(session.ID)
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
fmt.Println(err) fmt.Println(err)
@ -131,21 +106,24 @@ func search(w http.ResponseWriter, r *http.Request) {
} }
func customize(w http.ResponseWriter, r *http.Request) { func customize(w http.ResponseWriter, r *http.Request) {
session, _ := SessionStore.Get(r, "settings") session, err := GetSession(w, r)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Println(err)
return
}
text := GetText(r) text := GetText(r)
bookmarks := slices.Map(GetValueDefault(session, "bookmarks", DefaultBookmarks()), func(b Bookmark) BookmarkData {
return BookmarkData{
Bookmark: &b,
text: text,
}
})
data := &CustomizeData{ data := &CustomizeData{
text: text, text: text,
Bookmarks: bookmarks, Settings: GetSettingsBySessionID(session.ID),
Settings: GetValueDefault(session, "settings", DefaultSettings()), Bookmarks: channel.ToSliceDeref(channel.MapSuccessive(GetBookmarksBySessionIdOrdered(session.ID), func(b *Bookmark) *BookmarkData {
return &BookmarkData{
Bookmark: b,
text: text,
}
})),
} }
if err := Templates.ExecuteTemplate(w, "customize.html", data); err != nil { if err := Templates.ExecuteTemplate(w, "customize.html", data); err != nil {
@ -159,21 +137,38 @@ func saveChanges(w http.ResponseWriter, r *http.Request) {
return return
} }
session, _ := SessionStore.Get(r, "settings") session, err := GetSession(w, r)
sessionData := &SessionData{}
err := json.NewDecoder(r.Body).Decode(&sessionData)
if err != nil { if err != nil {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
fmt.Println(err) fmt.Println(err)
return return
} }
Reorder(sessionData.Bookmarks) sessionData := &SessionData{}
session.Values["bookmarks"] = sessionData.Bookmarks err = json.NewDecoder(r.Body).Decode(&sessionData)
session.Values["settings"] = sessionData.Settings if err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
if err := session.Save(r, w); err != nil { Reorder(sessionData.Bookmarks)
if err := DeleteBookmarksBySessionID(session.ID); err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
for _, bookmark := range sessionData.Bookmarks {
bookmark.SessionID = session.ID
if err := InsertBookmark(&bookmark); err != nil {
fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
sessionData.Settings.SessionID = session.ID
if err := UpdateSettings(sessionData.Settings); err != nil {
fmt.Println(err) fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
return return

View File

@ -1,6 +1,8 @@
package main package main
type Settings struct { type Settings struct {
SessionID string `json:"-"`
Background string `json:"background_color"` Background string `json:"background_color"`
Foreground string `json:"foreground_color"` Foreground string `json:"foreground_color"`
Search string `json:"search_query"` Search string `json:"search_query"`

View File

@ -16,6 +16,12 @@
background-color: {{ .Settings.Background }}; background-color: {{ .Settings.Background }};
color: {{ .Settings.Foreground }}; color: {{ .Settings.Foreground }};
} }
{{ if not .Settings.Search }}
#search {
display: none;
}
{{ end }}
</style> </style>
</head> </head>

View File

@ -1,7 +1,10 @@
package main package main
import ( import (
"github.com/gorilla/sessions" crand "crypto/rand"
"encoding/hex"
"net/http"
"time"
) )
func Must[T any](value T, err error) T { func Must[T any](value T, err error) T {
@ -11,25 +14,73 @@ func Must[T any](value T, err error) T {
return value return value
} }
func GetValue[K any, T any](session *sessions.Session, key K) (T, bool) { func generateRandomID(length int) (string, error) {
value, ok := session.Values[key] data := make([]byte, 32)
if !ok && value == nil { if _, err := crand.Read(data); err != nil {
return *new(T), false return "", err
} }
return hex.EncodeToString(data), nil
castedValue, ok := value.(T)
if !ok {
return *new(T), false
}
return castedValue, true
} }
func GetValueDefault[K any, T any](session *sessions.Session, key K, defaultValue T) T { func GetSession(w http.ResponseWriter, r *http.Request) (*Session, error) {
v, ok := GetValue[K, T](session, key) cookie, err := r.Cookie("session")
if !ok { if err != nil {
return defaultValue return makeNewSession(w)
} }
return v session := GetSessionByID(cookie.Value)
if session == nil {
return makeNewSession(w)
}
// update cookie expiration date
session.ExpirationDate = time.Now().AddDate(1, 0, 0)
if err := UpdateSession(session); err != nil {
return nil, err
}
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: session.ID,
Expires: session.ExpirationDate,
Secure: true,
HttpOnly: true,
})
return session, nil
}
func makeNewSession(w http.ResponseWriter) (*Session, error) {
session, err := NewSession()
if err != nil {
return nil, err
}
if err := InsertSession(session); err != nil {
return nil, err
}
// insert default bookmarks
for _, bookmark := range DefaultBookmarks() {
bookmark.SessionID = session.ID
if err := InsertBookmark(&bookmark); err != nil {
return nil, err
}
}
// insert default settings
settings := DefaultSettings()
settings.SessionID = session.ID
if err := InsertSettings(settings); err != nil {
return nil, err
}
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: session.ID,
Expires: session.ExpirationDate,
Secure: true,
HttpOnly: true,
})
return session, nil
} }