2021-03-14 20:49:58 +00:00
|
|
|
package website
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2021-03-21 20:38:37 +00:00
|
|
|
"errors"
|
2021-03-14 20:49:58 +00:00
|
|
|
"fmt"
|
|
|
|
"net/http"
|
2021-03-21 20:38:37 +00:00
|
|
|
"strings"
|
2021-03-14 20:49:58 +00:00
|
|
|
|
2021-03-22 03:07:18 +00:00
|
|
|
"git.handmade.network/hmn/hmn/src/auth"
|
2021-03-21 20:38:37 +00:00
|
|
|
"git.handmade.network/hmn/hmn/src/db"
|
2021-03-22 03:07:18 +00:00
|
|
|
"git.handmade.network/hmn/hmn/src/logging"
|
2021-03-21 20:38:37 +00:00
|
|
|
"git.handmade.network/hmn/hmn/src/models"
|
|
|
|
"git.handmade.network/hmn/hmn/src/oops"
|
2021-03-14 20:49:58 +00:00
|
|
|
"git.handmade.network/hmn/hmn/src/templates"
|
|
|
|
"github.com/jackc/pgx/v4/pgxpool"
|
|
|
|
"github.com/julienschmidt/httprouter"
|
|
|
|
)
|
|
|
|
|
|
|
|
type websiteRoutes struct {
|
2021-03-18 02:14:06 +00:00
|
|
|
*HMNRouter
|
2021-03-14 20:49:58 +00:00
|
|
|
|
|
|
|
conn *pgxpool.Pool
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewWebsiteRoutes(conn *pgxpool.Pool) http.Handler {
|
|
|
|
routes := &websiteRoutes{
|
2021-03-22 03:07:18 +00:00
|
|
|
HMNRouter: &HMNRouter{
|
|
|
|
HttpRouter: httprouter.New(),
|
|
|
|
Wrappers: []HMNHandlerWrapper{ErrorLoggingWrapper},
|
|
|
|
},
|
|
|
|
conn: conn,
|
2021-03-14 20:49:58 +00:00
|
|
|
}
|
|
|
|
|
2021-03-21 20:38:37 +00:00
|
|
|
mainRoutes := routes.WithWrappers(routes.CommonWebsiteDataWrapper)
|
|
|
|
mainRoutes.GET("/", routes.Index)
|
|
|
|
mainRoutes.GET("/project/:id", routes.Project)
|
|
|
|
mainRoutes.GET("/assets/project.css", routes.ProjectCSS)
|
|
|
|
|
2021-03-22 03:07:18 +00:00
|
|
|
routes.POST("/login", routes.Login)
|
|
|
|
|
2021-03-18 01:25:06 +00:00
|
|
|
routes.ServeFiles("/public/*filepath", http.Dir("public"))
|
2021-03-14 20:49:58 +00:00
|
|
|
|
|
|
|
return routes
|
|
|
|
}
|
|
|
|
|
2021-03-21 20:38:37 +00:00
|
|
|
func (s *websiteRoutes) getBaseData(c *RequestContext) templates.BaseData {
|
|
|
|
return templates.BaseData{
|
2021-03-18 01:25:06 +00:00
|
|
|
Project: templates.Project{
|
2021-03-21 20:38:37 +00:00
|
|
|
Name: c.currentProject.Name,
|
|
|
|
Subdomain: c.currentProject.Slug,
|
|
|
|
Color: c.currentProject.Color1,
|
2021-03-18 01:25:06 +00:00
|
|
|
|
2021-03-21 20:38:37 +00:00
|
|
|
IsHMN: c.currentProject.IsHMN(),
|
2021-03-18 01:25:06 +00:00
|
|
|
|
|
|
|
HasBlog: true,
|
|
|
|
HasForum: true,
|
|
|
|
HasWiki: true,
|
|
|
|
HasLibrary: true,
|
|
|
|
},
|
|
|
|
Theme: "dark",
|
2021-03-21 20:38:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func FetchProjectBySlug(ctx context.Context, conn *pgxpool.Pool, slug string) (*models.Project, error) {
|
|
|
|
var subdomainProject models.Project
|
|
|
|
err := db.QueryOneToStruct(ctx, conn, &subdomainProject, "SELECT $columns FROM handmade_project WHERE slug = $1", slug)
|
|
|
|
if err == nil {
|
|
|
|
return &subdomainProject, nil
|
|
|
|
} else if !errors.Is(err, db.ErrNoMatchingRows) {
|
|
|
|
return nil, oops.New(err, "failed to get projects by slug")
|
|
|
|
}
|
|
|
|
|
|
|
|
var defaultProject models.Project
|
|
|
|
err = db.QueryOneToStruct(ctx, conn, &defaultProject, "SELECT $columns FROM handmade_project WHERE id = $1", models.HMNProjectID)
|
|
|
|
if err != nil {
|
|
|
|
if errors.Is(err, db.ErrNoMatchingRows) {
|
|
|
|
return nil, oops.New(nil, "default project didn't exist in the database")
|
|
|
|
} else {
|
|
|
|
return nil, oops.New(err, "failed to get default project")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return &defaultProject, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *websiteRoutes) Index(c *RequestContext, p httprouter.Params) {
|
|
|
|
err := c.WriteTemplate("index.html", s.getBaseData(c))
|
2021-03-14 20:49:58 +00:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-18 02:14:06 +00:00
|
|
|
func (s *websiteRoutes) Project(c *RequestContext, p httprouter.Params) {
|
2021-03-14 20:49:58 +00:00
|
|
|
id := p.ByName("id")
|
|
|
|
row := s.conn.QueryRow(context.Background(), "SELECT name FROM handmade_project WHERE id = $1", p.ByName("id"))
|
|
|
|
var name string
|
|
|
|
err := row.Scan(&name)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
2021-03-18 02:14:06 +00:00
|
|
|
c.Body.Write([]byte(fmt.Sprintf("(%s) %s\n", id, name)))
|
2021-03-14 20:49:58 +00:00
|
|
|
}
|
|
|
|
|
2021-03-18 02:14:06 +00:00
|
|
|
func (s *websiteRoutes) ProjectCSS(c *RequestContext, p httprouter.Params) {
|
|
|
|
color := c.URL().Query().Get("color")
|
2021-03-14 20:49:58 +00:00
|
|
|
if color == "" {
|
2021-03-18 02:14:06 +00:00
|
|
|
c.StatusCode = http.StatusBadRequest
|
|
|
|
c.Body.Write([]byte("You must provide a 'color' parameter.\n"))
|
2021-03-14 20:49:58 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
templateData := struct {
|
|
|
|
Color string
|
|
|
|
Theme string
|
|
|
|
}{
|
|
|
|
Color: color,
|
|
|
|
Theme: "dark",
|
|
|
|
}
|
|
|
|
|
2021-03-18 02:14:06 +00:00
|
|
|
c.Headers().Add("Content-Type", "text/css")
|
|
|
|
err := c.WriteTemplate("project.css", templateData)
|
2021-03-14 20:49:58 +00:00
|
|
|
if err != nil {
|
2021-03-21 20:38:37 +00:00
|
|
|
c.Logger.Error().Err(err).Msg("failed to generate project CSS")
|
2021-03-14 20:49:58 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2021-03-21 20:38:37 +00:00
|
|
|
|
2021-03-22 03:07:18 +00:00
|
|
|
func (s *websiteRoutes) Login(c *RequestContext, p httprouter.Params) {
|
|
|
|
// TODO: Update this endpoint to give uniform responses on errors and to be resilient to timing attacks.
|
|
|
|
|
2021-03-26 03:33:00 +00:00
|
|
|
form, err := c.GetFormValues()
|
2021-03-22 03:07:18 +00:00
|
|
|
if err != nil {
|
2021-03-26 03:33:00 +00:00
|
|
|
c.Errored(http.StatusBadRequest, NewSafeError(err, "request must contain form data"))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
username := form.Get("username")
|
|
|
|
password := form.Get("password")
|
|
|
|
if username == "" || password == "" {
|
|
|
|
c.Errored(http.StatusBadRequest, NewSafeError(err, "you must provide both a username and password"))
|
|
|
|
}
|
|
|
|
|
|
|
|
redirect := form.Get("redirect")
|
|
|
|
if redirect == "" {
|
|
|
|
redirect = "/"
|
2021-03-22 03:07:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var user models.User
|
2021-03-26 03:33:00 +00:00
|
|
|
err = db.QueryOneToStruct(c.Context(), s.conn, &user, "SELECT $columns FROM auth_user WHERE username = $1", username)
|
2021-03-22 03:07:18 +00:00
|
|
|
if err != nil {
|
|
|
|
if errors.Is(err, db.ErrNoMatchingRows) {
|
|
|
|
c.StatusCode = http.StatusUnauthorized
|
|
|
|
} else {
|
|
|
|
c.Errored(http.StatusInternalServerError, oops.New(err, "failed to look up user by username"))
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-03-26 03:33:00 +00:00
|
|
|
hashed, err := auth.ParsePasswordString(user.Password)
|
2021-03-22 03:07:18 +00:00
|
|
|
if err != nil {
|
|
|
|
c.Errored(http.StatusInternalServerError, oops.New(err, "failed to parse password string"))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-03-26 03:33:00 +00:00
|
|
|
passwordsMatch, err := auth.CheckPassword(password, hashed)
|
2021-03-22 03:07:18 +00:00
|
|
|
if err != nil {
|
|
|
|
c.Errored(http.StatusInternalServerError, oops.New(err, "failed to check password against hash"))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if passwordsMatch {
|
2021-03-26 03:33:00 +00:00
|
|
|
logging.Debug().Str("cookie", auth.NewAuthCookie(username).String()).Msg("logged in")
|
|
|
|
c.SetCookie(auth.NewAuthCookie(username))
|
|
|
|
c.Redirect(redirect, http.StatusSeeOther)
|
|
|
|
return
|
2021-03-22 03:07:18 +00:00
|
|
|
} else {
|
2021-03-26 03:33:00 +00:00
|
|
|
c.Redirect("/", http.StatusSeeOther) // TODO: Redirect to standalone login page with error
|
|
|
|
return
|
2021-03-22 03:07:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func ErrorLoggingWrapper(h HMNHandler) HMNHandler {
|
|
|
|
return func(c *RequestContext, p httprouter.Params) {
|
|
|
|
h(c, p)
|
|
|
|
|
|
|
|
for _, err := range c.Errors {
|
|
|
|
c.Logger.Error().Err(err).Msg("error occurred during request")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-21 20:38:37 +00:00
|
|
|
func (s *websiteRoutes) CommonWebsiteDataWrapper(h HMNHandler) HMNHandler {
|
|
|
|
return func(c *RequestContext, p httprouter.Params) {
|
|
|
|
slug := ""
|
|
|
|
hostParts := strings.SplitN(c.Req.Host, ".", 3)
|
|
|
|
if len(hostParts) >= 3 {
|
|
|
|
slug = hostParts[0]
|
|
|
|
}
|
|
|
|
|
|
|
|
dbProject, err := FetchProjectBySlug(c.Context(), s.conn, slug)
|
|
|
|
if err != nil {
|
2021-03-22 03:07:18 +00:00
|
|
|
c.Errored(http.StatusInternalServerError, oops.New(err, "failed to fetch current project"))
|
2021-03-21 20:38:37 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
c.currentProject = dbProject
|
|
|
|
|
|
|
|
h(c, p)
|
|
|
|
}
|
|
|
|
}
|