diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5fa2459 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.idea +keys +nkey.sh +users +bin +db.sql +*crt +*key +Makefile2 +*~ diff --git a/config/cfg.go b/config/cfg.go index 78a57c1..0ad327e 100644 --- a/config/cfg.go +++ b/config/cfg.go @@ -3,33 +3,21 @@ package config import "os" type ( - DBCfg struct { - DatabaseURL string - } - - ServerCfg struct { - SessionKey string - Env string - CertFile string - CertKeyFile string - } - Config struct { - DB DBCfg - Server ServerCfg + DatabaseURL string + SessionKey string + Env string + CertFile string + KeyFile string } ) func New() *Config { return &Config{ - DB: DBCfg{ - DatabaseURL: os.Getenv("DATABASE_URL"), - }, - Server: ServerCfg{ - SessionKey: os.Getenv("SESSION_KEY"), - Env: os.Getenv("ENV"), - CertFile: os.Getenv("CERT_FILE"), - CertKeyFile: os.Getenv("CERT_KEY_FILE"), - }, + DatabaseURL: os.Getenv("DATABASE_URL"), + SessionKey: os.Getenv("SESSION_KEY"), + Env: os.Getenv("ENV"), + CertFile: os.Getenv("CERT_FILE"), + KeyFile: os.Getenv("CERT_KEY_FILE"), } } diff --git a/generate.go b/generate.go index 5185c35..2149b0d 100644 --- a/generate.go +++ b/generate.go @@ -81,5 +81,6 @@ func generateMap(target string, pkg string, mapName string, srcFiles []string) { func main() { 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")) } diff --git a/go.mod b/go.mod index 76f93a0..dd86997 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module status go 1.16 require ( + github.com/gorilla/mux v1.8.0 + github.com/gorilla/sessions v1.2.1 github.com/lib/pq v1.10.4 golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 ) diff --git a/go.sum b/go.sum index 233fb43..709d15c 100644 --- a/go.sum +++ b/go.sum @@ -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/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI= diff --git a/main.go b/main.go index 8b60401..200c4ba 100644 --- a/main.go +++ b/main.go @@ -5,17 +5,17 @@ import ( "log" "status/config" "status/storage" + "status/web" ) func main() { cfg := config.New() - db, err := storage.InitDB(cfg.DB) + db, err := storage.InitDB(cfg.DatabaseURL) if err != nil { log.Fatal(err) } - storage.New(db) - //data := storage.New(db) - //log.Fatal( - // server.Serve(data, cfg), - //) + data := storage.New(db) + log.Fatal( + web.Serve(data, cfg), + ) } diff --git a/storage/db.go b/storage/db.go index a3a11a1..fd877fb 100644 --- a/storage/db.go +++ b/storage/db.go @@ -3,11 +3,10 @@ package storage import ( "database/sql" _ "github.com/lib/pq" - "status/config" ) -func InitDB(cfg config.DBCfg) (*sql.DB, error) { - db, err := sql.Open("postgres", cfg.DatabaseURL) +func InitDB(databaseURL string) (*sql.DB, error) { + db, err := sql.Open("postgres", databaseURL) if err != nil { return db, err } diff --git a/storage/status.go b/storage/status.go index 7e2d21a..28b0f4e 100644 --- a/storage/status.go +++ b/storage/status.go @@ -55,27 +55,27 @@ func (s *Storage) StatusById(id int64) (model.Status, error) { 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{ where: `author = $1`, limit: strconv.Itoa(perPage + 1), offset: `$2`, }.build(), user, page*int64(perPage)) if err != nil { - return nil, false, err + return nil, err } var statuses []model.Status for rows.Next() { post, err := s.populateStatus(rows) if err != nil { - return statuses, false, err + return statuses, err } statuses = append(statuses, post) } 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) { diff --git a/web/handler/common.go b/web/handler/common.go new file mode 100644 index 0000000..7771ded --- /dev/null +++ b/web/handler/common.go @@ -0,0 +1,22 @@ +// Code generated by go generate; DO NOT EDIT. + +package handler + +var TplCommonMap = map[string]string{ + "layout": `{{ define "layout" }} + + + + + + + Status + {{ template "head" . }} + + + {{ template "content" . }} + + +{{ end }} +{{ define "head" }}{{ end }}`, +} diff --git a/web/handler/form/editor.go b/web/handler/form/editor.go new file mode 100644 index 0000000..eda9462 --- /dev/null +++ b/web/handler/form/editor.go @@ -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"), + } +} diff --git a/web/handler/form/folder.go b/web/handler/form/folder.go new file mode 100644 index 0000000..274d015 --- /dev/null +++ b/web/handler/form/folder.go @@ -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"), + } +} diff --git a/web/handler/form/homepage.go b/web/handler/form/homepage.go new file mode 100644 index 0000000..5d79ae3 --- /dev/null +++ b/web/handler/form/homepage.go @@ -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"), + } +} diff --git a/web/handler/form/login.go b/web/handler/form/login.go new file mode 100644 index 0000000..2d49373 --- /dev/null +++ b/web/handler/form/login.go @@ -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"), + } +} diff --git a/web/handler/form/upload.go b/web/handler/form/upload.go new file mode 100644 index 0000000..401df4d --- /dev/null +++ b/web/handler/form/upload.go @@ -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 +} diff --git a/web/handler/handler.go b/web/handler/handler.go new file mode 100644 index 0000000..eadaa39 --- /dev/null +++ b/web/handler/handler.go @@ -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 +} diff --git a/web/handler/html.go b/web/handler/html.go new file mode 100644 index 0000000..1c50033 --- /dev/null +++ b/web/handler/html.go @@ -0,0 +1,9 @@ +// Code generated by go generate; DO NOT EDIT. + +package handler + +var TplMap = map[string]string{ + "index": `{{ define "content" }} +

This is the index

+{{ end }}`, +} diff --git a/web/handler/html/common/layout.html b/web/handler/html/common/layout.html new file mode 100644 index 0000000..887e31b --- /dev/null +++ b/web/handler/html/common/layout.html @@ -0,0 +1,16 @@ +{{ define "layout" }} + + + + + + + Status + {{ template "head" . }} + + + {{ template "content" . }} + + +{{ end }} +{{ define "head" }}{{ end }} \ No newline at end of file diff --git a/web/handler/html/index.html b/web/handler/html/index.html new file mode 100644 index 0000000..1f75611 --- /dev/null +++ b/web/handler/html/index.html @@ -0,0 +1,3 @@ +{{ define "content" }} +

This is the index

+{{ end }} \ No newline at end of file diff --git a/web/handler/index_show.go b/web/handler/index_show.go new file mode 100644 index 0000000..317ff4c --- /dev/null +++ b/web/handler/index_show.go @@ -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{}{}, "") +} diff --git a/web/handler/tpl.go b/web/handler/tpl.go new file mode 100644 index 0000000..9463b30 --- /dev/null +++ b/web/handler/tpl.go @@ -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] +} diff --git a/web/session/session.go b/web/session/session.go new file mode 100644 index 0000000..a78a624 --- /dev/null +++ b/web/session/session.go @@ -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 +} diff --git a/web/web.go b/web/web.go new file mode 100644 index 0000000..34473e6 --- /dev/null +++ b/web/web.go @@ -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 +}