2021-03-14 20:49:58 +00:00
|
|
|
package website
|
|
|
|
|
|
|
|
import (
|
2021-04-06 03:30:11 +00:00
|
|
|
"bytes"
|
2021-03-14 20:49:58 +00:00
|
|
|
"context"
|
2021-03-21 20:38:37 +00:00
|
|
|
"errors"
|
2021-03-14 20:49:58 +00:00
|
|
|
"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"
|
|
|
|
)
|
|
|
|
|
|
|
|
func NewWebsiteRoutes(conn *pgxpool.Pool) http.Handler {
|
2021-04-06 05:06:19 +00:00
|
|
|
router := httprouter.New()
|
|
|
|
routes := RouteBuilder{
|
|
|
|
Router: router,
|
|
|
|
BeforeHandlers: []HMNBeforeHandler{
|
|
|
|
func(c *RequestContext) (bool, ResponseData) {
|
|
|
|
c.Conn = conn
|
|
|
|
return true, ResponseData{}
|
|
|
|
},
|
2021-04-11 21:46:06 +00:00
|
|
|
// TODO: Add a timeout? We don't want routes hanging forever
|
2021-03-22 03:07:18 +00:00
|
|
|
},
|
2021-04-06 05:06:19 +00:00
|
|
|
AfterHandlers: []HMNAfterHandler{ErrorLoggingHandler},
|
2021-03-14 20:49:58 +00:00
|
|
|
}
|
|
|
|
|
2021-04-06 05:06:19 +00:00
|
|
|
routes.POST("/login", Login)
|
|
|
|
routes.GET("/logout", Logout)
|
|
|
|
routes.ServeFiles("/public/*filepath", http.Dir("public"))
|
|
|
|
|
|
|
|
mainRoutes := routes
|
|
|
|
mainRoutes.BeforeHandlers = append(mainRoutes.BeforeHandlers,
|
|
|
|
CommonWebsiteDataWrapper,
|
|
|
|
)
|
|
|
|
|
2021-04-06 03:30:11 +00:00
|
|
|
mainRoutes.GET("/", func(c *RequestContext) ResponseData {
|
2021-04-22 23:02:50 +00:00
|
|
|
if c.CurrentProject.IsHMN() {
|
2021-04-06 05:06:19 +00:00
|
|
|
return Index(c)
|
2021-03-31 03:55:19 +00:00
|
|
|
} else {
|
|
|
|
// TODO: Return the project landing page
|
2021-04-06 03:30:11 +00:00
|
|
|
panic("route not implemented")
|
2021-03-31 03:55:19 +00:00
|
|
|
}
|
|
|
|
})
|
2021-04-25 19:33:22 +00:00
|
|
|
mainRoutes.GET("/feed", Feed)
|
|
|
|
mainRoutes.GET("/feed/:page", Feed)
|
|
|
|
|
2021-04-06 05:06:19 +00:00
|
|
|
mainRoutes.GET("/assets/project.css", ProjectCSS)
|
2021-03-21 20:38:37 +00:00
|
|
|
|
2021-04-06 05:06:19 +00:00
|
|
|
router.NotFound = mainRoutes.ChainHandlers(FourOhFour)
|
2021-03-22 03:07:18 +00:00
|
|
|
|
2021-04-06 05:06:19 +00:00
|
|
|
adminRoutes := routes
|
|
|
|
adminRoutes.BeforeHandlers = append(adminRoutes.BeforeHandlers,
|
|
|
|
func(c *RequestContext) (ok bool, res ResponseData) {
|
|
|
|
return false, ResponseData{
|
|
|
|
StatusCode: http.StatusUnauthorized,
|
|
|
|
Body: bytes.NewBufferString("No one is allowed!\n"),
|
|
|
|
}
|
|
|
|
},
|
|
|
|
)
|
|
|
|
adminRoutes.AfterHandlers = append(adminRoutes.AfterHandlers,
|
|
|
|
func(c *RequestContext, res ResponseData) ResponseData {
|
|
|
|
res.Body.WriteString("Now go away. Sincerely, the after handler.\n")
|
|
|
|
return res
|
|
|
|
},
|
|
|
|
)
|
2021-03-14 20:49:58 +00:00
|
|
|
|
2021-04-06 05:06:19 +00:00
|
|
|
adminRoutes.GET("/admin", func(c *RequestContext) ResponseData {
|
|
|
|
return ResponseData{
|
|
|
|
Body: bytes.NewBufferString("Here are all the secrets.\n"),
|
|
|
|
}
|
|
|
|
})
|
2021-03-31 04:20:50 +00:00
|
|
|
|
2021-04-06 05:06:19 +00:00
|
|
|
return router
|
2021-03-14 20:49:58 +00:00
|
|
|
}
|
|
|
|
|
2021-04-06 05:06:19 +00:00
|
|
|
func getBaseData(c *RequestContext) templates.BaseData {
|
2021-03-27 21:10:11 +00:00
|
|
|
var templateUser *templates.User
|
2021-04-06 03:30:11 +00:00
|
|
|
if c.CurrentUser != nil {
|
2021-03-27 21:10:11 +00:00
|
|
|
templateUser = &templates.User{
|
2021-04-06 03:30:11 +00:00
|
|
|
Username: c.CurrentUser.Username,
|
|
|
|
Email: c.CurrentUser.Email,
|
|
|
|
IsSuperuser: c.CurrentUser.IsSuperuser,
|
|
|
|
IsStaff: c.CurrentUser.IsStaff,
|
2021-03-27 21:10:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-21 20:38:37 +00:00
|
|
|
return templates.BaseData{
|
2021-04-11 21:46:06 +00:00
|
|
|
Project: templates.ProjectToTemplate(c.CurrentProject),
|
|
|
|
User: templateUser,
|
|
|
|
Theme: "dark",
|
2021-03-21 20:38:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func FetchProjectBySlug(ctx context.Context, conn *pgxpool.Pool, slug string) (*models.Project, error) {
|
2021-03-31 03:55:19 +00:00
|
|
|
subdomainProjectRow, err := db.QueryOne(ctx, conn, models.Project{}, "SELECT $columns FROM handmade_project WHERE slug = $1", slug)
|
2021-03-21 20:38:37 +00:00
|
|
|
if err == nil {
|
2021-03-31 03:55:19 +00:00
|
|
|
subdomainProject := subdomainProjectRow.(models.Project)
|
2021-03-21 20:38:37 +00:00
|
|
|
return &subdomainProject, nil
|
|
|
|
} else if !errors.Is(err, db.ErrNoMatchingRows) {
|
|
|
|
return nil, oops.New(err, "failed to get projects by slug")
|
|
|
|
}
|
|
|
|
|
2021-03-31 03:55:19 +00:00
|
|
|
defaultProjectRow, err := db.QueryOne(ctx, conn, models.Project{}, "SELECT $columns FROM handmade_project WHERE id = $1", models.HMNProjectID)
|
2021-03-21 20:38:37 +00:00
|
|
|
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")
|
|
|
|
}
|
|
|
|
}
|
2021-03-31 03:55:19 +00:00
|
|
|
defaultProject := defaultProjectRow.(*models.Project)
|
2021-03-21 20:38:37 +00:00
|
|
|
|
2021-03-31 03:55:19 +00:00
|
|
|
return defaultProject, nil
|
2021-03-14 20:49:58 +00:00
|
|
|
}
|
|
|
|
|
2021-04-06 05:06:19 +00:00
|
|
|
func ProjectCSS(c *RequestContext) ResponseData {
|
2021-03-18 02:14:06 +00:00
|
|
|
color := c.URL().Query().Get("color")
|
2021-03-14 20:49:58 +00:00
|
|
|
if color == "" {
|
2021-04-06 03:30:11 +00:00
|
|
|
return ErrorResponse(http.StatusBadRequest, NewSafeError(nil, "You must provide a 'color' parameter.\n"))
|
2021-03-14 20:49:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
templateData := struct {
|
|
|
|
Color string
|
|
|
|
Theme string
|
|
|
|
}{
|
|
|
|
Color: color,
|
|
|
|
Theme: "dark",
|
|
|
|
}
|
|
|
|
|
2021-04-06 03:30:11 +00:00
|
|
|
var res ResponseData
|
|
|
|
res.Headers().Add("Content-Type", "text/css")
|
|
|
|
err := res.WriteTemplate("project.css", templateData)
|
2021-03-14 20:49:58 +00:00
|
|
|
if err != nil {
|
2021-04-06 03:30:11 +00:00
|
|
|
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to generate project CSS"))
|
2021-03-14 20:49:58 +00:00
|
|
|
}
|
2021-04-06 03:30:11 +00:00
|
|
|
|
|
|
|
return res
|
2021-03-14 20:49:58 +00:00
|
|
|
}
|
2021-03-21 20:38:37 +00:00
|
|
|
|
2021-04-06 05:06:19 +00:00
|
|
|
func FourOhFour(c *RequestContext) ResponseData {
|
2021-04-06 03:30:11 +00:00
|
|
|
return ResponseData{
|
|
|
|
StatusCode: http.StatusNotFound,
|
2021-04-06 05:06:19 +00:00
|
|
|
Body: bytes.NewBufferString("go away\n"),
|
2021-04-06 03:30:11 +00:00
|
|
|
}
|
2021-03-31 04:20:50 +00:00
|
|
|
}
|
|
|
|
|
2021-04-06 05:06:19 +00:00
|
|
|
func ErrorLoggingHandler(c *RequestContext, res ResponseData) ResponseData {
|
|
|
|
for _, err := range res.Errors {
|
|
|
|
c.Logger.Error().Err(err).Msg("error occurred during request")
|
2021-03-22 03:07:18 +00:00
|
|
|
}
|
|
|
|
|
2021-04-06 05:06:19 +00:00
|
|
|
return res
|
|
|
|
}
|
2021-03-27 21:10:11 +00:00
|
|
|
|
2021-04-06 05:06:19 +00:00
|
|
|
func CommonWebsiteDataWrapper(c *RequestContext) (bool, ResponseData) {
|
|
|
|
// get project
|
|
|
|
{
|
|
|
|
slug := ""
|
|
|
|
hostParts := strings.SplitN(c.Req.Host, ".", 3)
|
|
|
|
if len(hostParts) >= 3 {
|
|
|
|
slug = hostParts[0]
|
|
|
|
}
|
2021-03-27 21:10:11 +00:00
|
|
|
|
2021-04-06 05:06:19 +00:00
|
|
|
dbProject, err := FetchProjectBySlug(c.Context(), c.Conn, slug)
|
|
|
|
if err != nil {
|
|
|
|
return false, ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch current project"))
|
2021-03-21 20:38:37 +00:00
|
|
|
}
|
|
|
|
|
2021-04-06 05:06:19 +00:00
|
|
|
c.CurrentProject = dbProject
|
|
|
|
}
|
2021-03-21 20:38:37 +00:00
|
|
|
|
2021-04-06 05:06:19 +00:00
|
|
|
sessionCookie, err := c.Req.Cookie(auth.SessionCookieName)
|
|
|
|
if err == nil {
|
2021-04-17 00:01:13 +00:00
|
|
|
user, err := getCurrentUser(c, sessionCookie.Value)
|
2021-04-06 05:06:19 +00:00
|
|
|
if err != nil {
|
2021-04-17 00:01:13 +00:00
|
|
|
return false, ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get current user"))
|
2021-03-27 21:10:11 +00:00
|
|
|
}
|
2021-03-21 20:38:37 +00:00
|
|
|
|
2021-04-06 05:06:19 +00:00
|
|
|
c.CurrentUser = user
|
2021-03-21 20:38:37 +00:00
|
|
|
}
|
2021-04-06 05:06:19 +00:00
|
|
|
// http.ErrNoCookie is the only error Cookie ever returns, so no further handling to do here.
|
|
|
|
|
|
|
|
return true, ResponseData{}
|
2021-03-21 20:38:37 +00:00
|
|
|
}
|
2021-03-27 21:10:11 +00:00
|
|
|
|
2021-04-17 00:01:13 +00:00
|
|
|
// Given a session id, fetches user data from the database. Will return nil if
|
|
|
|
// the user cannot be found, and will only return an error if it's serious.
|
|
|
|
func getCurrentUser(c *RequestContext, sessionId string) (*models.User, error) {
|
2021-04-06 05:06:19 +00:00
|
|
|
session, err := auth.GetSession(c.Context(), c.Conn, sessionId)
|
2021-03-27 21:10:11 +00:00
|
|
|
if err != nil {
|
|
|
|
if errors.Is(err, auth.ErrNoSession) {
|
|
|
|
return nil, nil
|
|
|
|
} else {
|
|
|
|
return nil, oops.New(err, "failed to get current session")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-06 05:06:19 +00:00
|
|
|
userRow, err := db.QueryOne(c.Context(), c.Conn, models.User{}, "SELECT $columns FROM auth_user WHERE username = $1", session.Username)
|
2021-03-27 21:10:11 +00:00
|
|
|
if err != nil {
|
|
|
|
if errors.Is(err, db.ErrNoMatchingRows) {
|
|
|
|
logging.Debug().Str("username", session.Username).Msg("returning no current user for this request because the user for the session couldn't be found")
|
|
|
|
return nil, nil // user was deleted or something
|
|
|
|
} else {
|
|
|
|
return nil, oops.New(err, "failed to get user for session")
|
|
|
|
}
|
|
|
|
}
|
2021-03-31 03:55:19 +00:00
|
|
|
user := userRow.(*models.User)
|
2021-03-27 21:10:11 +00:00
|
|
|
|
2021-03-31 03:55:19 +00:00
|
|
|
return user, nil
|
2021-03-27 21:10:11 +00:00
|
|
|
}
|