Add web package

This commit is contained in:
m15o 2021-11-22 10:06:23 +01:00
parent 7cd973f0a6
commit 8b054e454a
22 changed files with 466 additions and 37 deletions

10
.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
.idea
keys
nkey.sh
users
bin
db.sql
*crt
*key
Makefile2
*~

View file

@ -3,33 +3,21 @@ package config
import "os" import "os"
type ( type (
DBCfg struct {
DatabaseURL string
}
ServerCfg struct {
SessionKey string
Env string
CertFile string
CertKeyFile string
}
Config struct { Config struct {
DB DBCfg DatabaseURL string
Server ServerCfg SessionKey string
Env string
CertFile string
KeyFile string
} }
) )
func New() *Config { func New() *Config {
return &Config{ return &Config{
DB: DBCfg{ DatabaseURL: os.Getenv("DATABASE_URL"),
DatabaseURL: os.Getenv("DATABASE_URL"), SessionKey: os.Getenv("SESSION_KEY"),
}, Env: os.Getenv("ENV"),
Server: ServerCfg{ CertFile: os.Getenv("CERT_FILE"),
SessionKey: os.Getenv("SESSION_KEY"), KeyFile: os.Getenv("CERT_KEY_FILE"),
Env: os.Getenv("ENV"),
CertFile: os.Getenv("CERT_FILE"),
CertKeyFile: os.Getenv("CERT_KEY_FILE"),
},
} }
} }

View file

@ -81,5 +81,6 @@ func generateMap(target string, pkg string, mapName string, srcFiles []string) {
func main() { func main() {
generateMap(path.Join("storage", "sql.go"), "storage", "SqlMap", glob("storage/sql/*.sql")) generateMap(path.Join("storage", "sql.go"), "storage", "SqlMap", glob("storage/sql/*.sql"))
//generateMap(path.Join("template", "html.go"), "template", "TplMap", glob("template/html/*.html")) generateMap(path.Join("web", "handler", "html.go"), "handler", "TplMap", glob("web/handler/html/*.html"))
generateMap(path.Join("web", "handler", "common.go"), "handler", "TplCommonMap", glob("web/handler/html/common/*.html"))
} }

2
go.mod
View file

@ -3,6 +3,8 @@ module status
go 1.16 go 1.16
require ( require (
github.com/gorilla/mux v1.8.0
github.com/gorilla/sessions v1.2.1
github.com/lib/pq v1.10.4 github.com/lib/pq v1.10.4
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871
) )

6
go.sum
View file

@ -1,3 +1,9 @@
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
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/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk=
github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI= golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI=

12
main.go
View file

@ -5,17 +5,17 @@ import (
"log" "log"
"status/config" "status/config"
"status/storage" "status/storage"
"status/web"
) )
func main() { func main() {
cfg := config.New() cfg := config.New()
db, err := storage.InitDB(cfg.DB) db, err := storage.InitDB(cfg.DatabaseURL)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
storage.New(db) data := storage.New(db)
//data := storage.New(db) log.Fatal(
//log.Fatal( web.Serve(data, cfg),
// server.Serve(data, cfg), )
//)
} }

View file

@ -3,11 +3,10 @@ package storage
import ( import (
"database/sql" "database/sql"
_ "github.com/lib/pq" _ "github.com/lib/pq"
"status/config"
) )
func InitDB(cfg config.DBCfg) (*sql.DB, error) { func InitDB(databaseURL string) (*sql.DB, error) {
db, err := sql.Open("postgres", cfg.DatabaseURL) db, err := sql.Open("postgres", databaseURL)
if err != nil { if err != nil {
return db, err return db, err
} }

View file

@ -55,27 +55,27 @@ func (s *Storage) StatusById(id int64) (model.Status, error) {
return status, err return status, err
} }
func (s *Storage) StatusByUsername(user string, perPage int, page int64) ([]model.Status, bool, error) { func (s *Storage) StatusByUsername(user string, perPage int, page int64) ([]model.Status, error) {
rows, err := s.db.Query(statusQueryBuilder{ rows, err := s.db.Query(statusQueryBuilder{
where: `author = $1`, where: `author = $1`,
limit: strconv.Itoa(perPage + 1), limit: strconv.Itoa(perPage + 1),
offset: `$2`, offset: `$2`,
}.build(), user, page*int64(perPage)) }.build(), user, page*int64(perPage))
if err != nil { if err != nil {
return nil, false, err return nil, err
} }
var statuses []model.Status var statuses []model.Status
for rows.Next() { for rows.Next() {
post, err := s.populateStatus(rows) post, err := s.populateStatus(rows)
if err != nil { if err != nil {
return statuses, false, err return statuses, err
} }
statuses = append(statuses, post) statuses = append(statuses, post)
} }
if len(statuses) > perPage { if len(statuses) > perPage {
return statuses[0:perPage], true, err return statuses[0:perPage], err
} }
return statuses, false, err return statuses, err
} }
func (s *Storage) Statuses(page int64, perPage int) ([]model.Status, bool, error) { func (s *Storage) Statuses(page int64, perPage int) ([]model.Status, bool, error) {

22
web/handler/common.go Normal file
View file

@ -0,0 +1,22 @@
// Code generated by go generate; DO NOT EDIT.
package handler
var TplCommonMap = map[string]string{
"layout": `{{ define "layout" }}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/style.css"/>
<title>Status</title>
{{ template "head" . }}
</head>
<body>
{{ template "content" . }}
</body>
</html>
{{ end }}
{{ define "head" }}{{ end }}`,
}

View file

@ -0,0 +1,34 @@
package form
import (
"net/http"
)
type EditorForm struct {
Name string
Content string
}
//func (f *HomepageForm) Validate() error {
// if f.Password != f.Confirm {
// return errors.New("password doesn't match confirmation")
// }
// if len(f.Username) < 3 {
// return errors.New("username needs to be at least 3 characters")
// }
// match, _ := regexp.MatchString("^[a-z0-9-_]+$", f.Username)
// if !match {
// return errors.New("only lowercase letters and digits are accepted for username")
// }
// if len(f.Password) < 6 {
// return errors.New("password needs to be at least 6 characters")
// }
// return nil
//}
func NewEditorForm(r *http.Request) *EditorForm {
return &EditorForm{
Name: r.FormValue("name"),
Content: r.FormValue("content"),
}
}

View file

@ -0,0 +1,15 @@
package form
import (
"net/http"
)
type FolderForm struct {
Name string
}
func NewFolderForm(r *http.Request) *FolderForm {
return &FolderForm{
Name: r.FormValue("name"),
}
}

View file

@ -0,0 +1,41 @@
package form
import (
"errors"
"net/http"
"regexp"
)
type HomepageForm struct {
Username string
Password string
Confirm string
Key string
Error string
}
func (f *HomepageForm) Validate() error {
if f.Password != f.Confirm {
return errors.New("password doesn't match confirmation")
}
if len(f.Username) < 3 {
return errors.New("username needs to be at least 3 characters")
}
match, _ := regexp.MatchString("^[a-z0-9-_]+$", f.Username)
if !match {
return errors.New("only lowercase letters and digits are accepted for username")
}
if len(f.Password) < 6 {
return errors.New("password needs to be at least 6 characters")
}
return nil
}
func NewHomepageForm(r *http.Request) *HomepageForm {
return &HomepageForm{
Username: r.FormValue("name"),
Password: r.FormValue("password"),
Confirm: r.FormValue("password-confirm"),
Key: r.FormValue("key"),
}
}

18
web/handler/form/login.go Normal file
View file

@ -0,0 +1,18 @@
package form
import (
"net/http"
)
type LoginForm struct {
Username string
Password string
Error string
}
func NewLoginForm(r *http.Request) *LoginForm {
return &LoginForm{
Username: r.FormValue("name"),
Password: r.FormValue("password"),
}
}

View file

@ -0,0 +1,26 @@
package form
import (
"io"
"net/http"
)
type UploadForm struct {
Filename string
File io.Reader
}
func NewUploadForm(r *http.Request) (*UploadForm, error) {
if err := r.ParseMultipartForm(2 << 20); err != nil {
return nil, err
}
file, handler, err := r.FormFile("file")
if err != nil {
return nil, err
}
defer file.Close()
return &UploadForm{
Filename: handler.Filename,
File: file,
}, err
}

78
web/handler/handler.go Normal file
View file

@ -0,0 +1,78 @@
package handler
import (
"fmt"
"github.com/gorilla/mux"
"log"
"net/http"
"status/config"
"status/storage"
"status/web/session"
)
func serverError(w http.ResponseWriter, err error) {
log.Println("[server error]", err)
http.Error(w, fmt.Sprintf("server error: %s", err), http.StatusInternalServerError)
}
func notFound(w http.ResponseWriter) {
http.Error(w, "Page Not Found", http.StatusNotFound)
}
func unauthorized(w http.ResponseWriter) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
}
type Handler struct {
cfg *config.Config
mux *mux.Router
storage *storage.Storage
sess *session.Session
}
//func (h *Handler) getUser(r *http.Request) (string, error) {
// user, err := h.sess.Get(r)
// if err != nil {
// return "", err
// }
// if h.cfg.Env != "PROD" {
// return user, err
// }
// // Removes "https://" from referer before checking.
// // Thank you crussel for this fix!
// if !strings.HasPrefix(r.Referer()[8:], h.cfg.Host) && !strings.HasPrefix(r.Referer()[8:], user+h.cfg.Host) {
// err = errors.New("wrong referer")
// }
// return user, err
//}
func New(cfg *config.Config, sess *session.Session, data *storage.Storage) (http.Handler, error) {
router := mux.NewRouter()
h := &Handler{
cfg: cfg,
mux: router,
storage: data,
sess: sess,
}
h.initTpl()
// Index
router.HandleFunc("/", h.showIndexView).Methods(http.MethodGet)
//router.HandleFunc("/login", h.showLoginView).Methods(http.MethodGet)
//router.HandleFunc("/check-login", h.checkLogin).Methods(http.MethodPost)
//router.HandleFunc("/help", h.showHelpView).Methods(http.MethodGet)
//router.HandleFunc("/profile/{username}", h.showProfileView).Methods(http.MethodGet)
//router.HandleFunc("/register", h.handleRegister)
//router.HandleFunc("/editor", h.handleEditor)
//router.HandleFunc("/upload", h.upload)
//router.HandleFunc("/files", h.handleFiles)
//router.HandleFunc("/new-folder", h.handleNewFolder)
//router.HandleFunc("/new-file", h.handleNewFile)
//router.HandleFunc("/homepages", h.showHomepagesView).Methods(http.MethodGet)
//router.HandleFunc("/rename", h.handleRename)
//router.HandleFunc("/delete", h.handleDelete).Methods(http.MethodPost)
//router.HandleFunc("/logout", h.logout).Methods(http.MethodGet)
//router.PathPrefix("/").Handler(http.FileServer(http.Dir(cfg.AssetsDir)))
return router, nil
}

9
web/handler/html.go Normal file
View file

@ -0,0 +1,9 @@
// Code generated by go generate; DO NOT EDIT.
package handler
var TplMap = map[string]string{
"index": `{{ define "content" }}
<p>This is the index</p>
{{ end }}`,
}

View file

@ -0,0 +1,16 @@
{{ define "layout" }}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/style.css"/>
<title>Status</title>
{{ template "head" . }}
</head>
<body>
{{ template "content" . }}
</body>
</html>
{{ end }}
{{ define "head" }}{{ end }}

View file

@ -0,0 +1,3 @@
{{ define "content" }}
<p>This is the index</p>
{{ end }}

22
web/handler/index_show.go Normal file
View file

@ -0,0 +1,22 @@
package handler
import (
"net/http"
)
type Update struct {
UpdatedAgo string
Author string
}
func (h *Handler) showIndexView(w http.ResponseWriter, r *http.Request) {
// user, _ := h.sess.Get(r)
//h.renderLayout(w, "index", map[string]interface{}{
// "Pages": pages,
// "Files": files,
// "News": news,
//}, user)
h.renderLayout(w, "index", map[string]interface{}{}, "")
}

34
web/handler/tpl.go Normal file
View file

@ -0,0 +1,34 @@
package handler
import (
"html/template"
"io"
)
var views = make(map[string]*template.Template)
func (h *Handler) initTpl() {
commonTemplates := ""
for _, content := range TplCommonMap {
commonTemplates += content
}
for name, content := range TplMap {
views[name] = template.Must(template.New("main").Parse(commonTemplates + content))
}
}
func (h *Handler) renderLayout(w io.Writer, view string, params map[string]interface{}, user string) {
data := make(map[string]interface{})
if params != nil {
for k, v := range params {
data[k] = v
}
}
data["logged"] = user
views[view].ExecuteTemplate(w, "layout", data)
}
func (h *Handler) view(view string) *template.Template {
return views[view]
}

58
web/session/session.go Normal file
View file

@ -0,0 +1,58 @@
package session
import (
"errors"
"github.com/gorilla/sessions"
"net/http"
"status/storage"
)
const cookieName = "status"
type Session struct {
Store *sessions.CookieStore
Storage *storage.Storage
}
func New(key string, storage *storage.Storage) *Session {
store := sessions.NewCookieStore([]byte(key))
store.Options = &sessions.Options{
HttpOnly: true,
MaxAge: 86400 * 30,
}
return &Session{
Store: store,
Storage: storage,
}
}
func (s *Session) Delete(w http.ResponseWriter, r *http.Request) error {
session, err := s.Store.Get(r, cookieName)
if err != nil {
return err
}
session.Options.MaxAge = -1
err = session.Save(r, w)
return err
}
func (s *Session) Save(r *http.Request, w http.ResponseWriter, name string) error {
session, _ := s.Store.Get(r, cookieName)
session.Values["name"] = name
return session.Save(r, w)
}
func (s *Session) Get(r *http.Request) (string, error) {
session, err := s.Store.Get(r, cookieName)
if err != nil {
return "", err
}
name, ok := session.Values["name"].(string)
if name == "" || !ok {
return "", errors.New("error extracting session")
}
if ok := s.Storage.UserExists(name); !ok {
return "", errors.New("user doesn't exit")
}
return name, nil
}

47
web/web.go Normal file
View file

@ -0,0 +1,47 @@
package web
import (
"fmt"
"log"
"net/http"
"status/config"
"status/storage"
"status/web/handler"
"status/web/session"
)
func httpToHTTPSHandler() *http.ServeMux {
handleRedirect := func(w http.ResponseWriter, r *http.Request) {
newURI := "https://" + r.Host + r.URL.String()
http.Redirect(w, r, newURI, http.StatusFound)
}
mux := &http.ServeMux{}
mux.HandleFunc("/", handleRedirect)
return mux
}
func Serve(data *storage.Storage, cfg *config.Config) error {
var err error
sess := session.New(cfg.SessionKey, data)
s, err := handler.New(cfg, sess, data)
if err != nil {
log.Fatal(err)
}
switch cfg.Env {
case "PROD":
go func() {
fmt.Printf("Starting HTTP server on :443\n")
err := http.ListenAndServeTLS(":443", cfg.CertFile, cfg.KeyFile, s)
if err != nil {
log.Fatalf("httpsSrv.ListendAndServeTLS() failed with %s", err)
}
}()
fmt.Printf("Starting HTTP to HTTPS server on :80\n")
err = http.ListenAndServe(":80", httpToHTTPSHandler())
break
default:
fmt.Printf("Starting HTTP server on port 8000\n")
err = http.ListenAndServe(":8000", s)
}
return err
}