Add some permissions and scaffolding for education CRUD
This commit is contained in:
parent
97d7fa96dc
commit
08d96a1040
|
@ -0,0 +1,57 @@
|
||||||
|
package hmndata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"git.handmade.network/hmn/hmn/src/db"
|
||||||
|
"git.handmade.network/hmn/hmn/src/models"
|
||||||
|
"git.handmade.network/hmn/hmn/src/oops"
|
||||||
|
"git.handmade.network/hmn/hmn/src/perf"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EduArticleQuery struct {
|
||||||
|
Slugs []string
|
||||||
|
Types []models.EduArticleType
|
||||||
|
}
|
||||||
|
|
||||||
|
type EduArticleAndStuff struct {
|
||||||
|
Article models.EduArticle `db:"a"`
|
||||||
|
CurrentVersion models.EduArticleVersion `db:"v"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchEduArticles(
|
||||||
|
ctx context.Context,
|
||||||
|
dbConn db.ConnOrTx,
|
||||||
|
currentUser *models.User,
|
||||||
|
q EduArticleQuery,
|
||||||
|
) ([]*EduArticleAndStuff, error) {
|
||||||
|
perf := perf.ExtractPerf(ctx)
|
||||||
|
perf.StartBlock("SQL", "Fetch education articles")
|
||||||
|
defer perf.EndBlock()
|
||||||
|
|
||||||
|
var qb db.QueryBuilder
|
||||||
|
qb.Add(`
|
||||||
|
SELECT $columns
|
||||||
|
FROM
|
||||||
|
education_article AS a
|
||||||
|
JOIN education_article_version AS v ON a.current_version = v.id
|
||||||
|
WHERE
|
||||||
|
TRUE
|
||||||
|
`)
|
||||||
|
if len(q.Slugs) > 0 {
|
||||||
|
qb.Add(`AND a.slug = ANY ($?)`, q.Slugs)
|
||||||
|
}
|
||||||
|
if len(q.Types) > 0 {
|
||||||
|
qb.Add(`AND a.type = ANY ($?)`, q.Types)
|
||||||
|
}
|
||||||
|
if currentUser == nil || !currentUser.CanSeeUnpublishedEducationContent() {
|
||||||
|
qb.Add(`AND a.published`)
|
||||||
|
}
|
||||||
|
|
||||||
|
articles, err := db.Query[EduArticleAndStuff](ctx, dbConn, qb.String(), qb.Args()...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, oops.New(err, "failed to fetch education articles")
|
||||||
|
}
|
||||||
|
|
||||||
|
return articles, nil
|
||||||
|
}
|
|
@ -463,6 +463,25 @@ func BuildEducationArticle(slug string) string {
|
||||||
return Url(fmt.Sprintf("/education/%s", slug), nil)
|
return Url(fmt.Sprintf("/education/%s", slug), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var RegexEducationArticleEdit = regexp.MustCompile(`^/education/(?P<slug>[^/]+)/edit$`)
|
||||||
|
|
||||||
|
func BuildEducationArticleEdit(slug string) string {
|
||||||
|
return Url(fmt.Sprintf("/education/%s/edit", slug), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexEducationArticleDelete = regexp.MustCompile(`^/education/(?P<slug>[^/]+)/delete$`)
|
||||||
|
|
||||||
|
func BuildEducationArticleDelete(slug string) string {
|
||||||
|
return Url(fmt.Sprintf("/education/%s/delete", slug), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexEducationAdmin = regexp.MustCompile(`^/education/admin$`)
|
||||||
|
|
||||||
|
func BuildEducationAdmin() string {
|
||||||
|
defer CatchPanic()
|
||||||
|
return Url("/education/admin", nil)
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Forums
|
* Forums
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -52,6 +52,14 @@ func init() {
|
||||||
}
|
}
|
||||||
migrateCommand.Flags().BoolVar(&listMigrations, "list", false, "List available migrations")
|
migrateCommand.Flags().BoolVar(&listMigrations, "list", false, "List available migrations")
|
||||||
|
|
||||||
|
rollbackCommand := &cobra.Command{
|
||||||
|
Use: "rollback",
|
||||||
|
Short: "Roll back the most recent completed migration",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
Rollback()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
makeMigrationCommand := &cobra.Command{
|
makeMigrationCommand := &cobra.Command{
|
||||||
Use: "makemigration <name> <description>...",
|
Use: "makemigration <name> <description>...",
|
||||||
Short: "Create a new database migration file",
|
Short: "Create a new database migration file",
|
||||||
|
@ -95,6 +103,7 @@ func init() {
|
||||||
|
|
||||||
website.WebsiteCommand.AddCommand(dbCommand)
|
website.WebsiteCommand.AddCommand(dbCommand)
|
||||||
dbCommand.AddCommand(migrateCommand)
|
dbCommand.AddCommand(migrateCommand)
|
||||||
|
dbCommand.AddCommand(rollbackCommand)
|
||||||
dbCommand.AddCommand(makeMigrationCommand)
|
dbCommand.AddCommand(makeMigrationCommand)
|
||||||
dbCommand.AddCommand(seedCommand)
|
dbCommand.AddCommand(seedCommand)
|
||||||
dbCommand.AddCommand(seedFromFileCommand)
|
dbCommand.AddCommand(seedFromFileCommand)
|
||||||
|
@ -126,7 +135,7 @@ func getCurrentVersion(ctx context.Context, conn *pgx.Conn) (types.MigrationVers
|
||||||
|
|
||||||
func tryGetCurrentVersion(ctx context.Context) types.MigrationVersion {
|
func tryGetCurrentVersion(ctx context.Context) types.MigrationVersion {
|
||||||
defer func() {
|
defer func() {
|
||||||
recover()
|
recover() // NOTE(ben): wat
|
||||||
}()
|
}()
|
||||||
|
|
||||||
conn := db.NewConn()
|
conn := db.NewConn()
|
||||||
|
@ -267,8 +276,8 @@ func Migrate(targetVersion types.MigrationVersion) {
|
||||||
}
|
}
|
||||||
defer tx.Rollback(ctx)
|
defer tx.Rollback(ctx)
|
||||||
|
|
||||||
fmt.Printf("Rolling back migration %v\n", version)
|
|
||||||
migration := migrations.All[version]
|
migration := migrations.All[version]
|
||||||
|
fmt.Printf("Rolling back migration %v (%s)\n", migration.Version(), migration.Name())
|
||||||
err = migration.Down(ctx, tx)
|
err = migration.Down(ctx, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("MIGRATION FAILED for migration %v.\n", version)
|
fmt.Printf("MIGRATION FAILED for migration %v.\n", version)
|
||||||
|
@ -291,6 +300,39 @@ func Migrate(targetVersion types.MigrationVersion) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Rollback() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
conn := db.NewConnWithConfig(config.PostgresConfig{
|
||||||
|
LogLevel: pgx.LogLevelWarn,
|
||||||
|
})
|
||||||
|
defer conn.Close(ctx)
|
||||||
|
|
||||||
|
currentVersion := tryGetCurrentVersion(ctx)
|
||||||
|
if currentVersion.IsZero() {
|
||||||
|
fmt.Println("You have never run migrations; nothing to do.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var target types.MigrationVersion
|
||||||
|
versions := getSortedMigrationVersions()
|
||||||
|
for i := 1; i < len(versions); i++ {
|
||||||
|
if versions[i].Equal(currentVersion) {
|
||||||
|
target = versions[i-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE(ben): It occurs to me that we don't have a way to roll back the initial migration, ever.
|
||||||
|
// Not that we would ever want to....?
|
||||||
|
|
||||||
|
if target.IsZero() {
|
||||||
|
fmt.Println("You are already at the earliest migration; nothing to do.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Migrate(target)
|
||||||
|
}
|
||||||
|
|
||||||
//go:embed migrationTemplate.txt
|
//go:embed migrationTemplate.txt
|
||||||
var migrationTemplate string
|
var migrationTemplate string
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,7 @@ func (m AddEducationResources) Up(ctx context.Context, tx pgx.Tx) error {
|
||||||
slug VARCHAR(255) NOT NULL,
|
slug VARCHAR(255) NOT NULL,
|
||||||
description TEXT NOT NULL,
|
description TEXT NOT NULL,
|
||||||
type INT NOT NULL,
|
type INT NOT NULL,
|
||||||
|
published BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
current_version INT NOT NULL REFERENCES education_article_version (id)
|
current_version INT NOT NULL REFERENCES education_article_version (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -55,6 +56,17 @@ func (m AddEducationResources) Up(ctx context.Context, tx pgx.Tx) error {
|
||||||
return oops.New(err, "failed to create education tables")
|
return oops.New(err, "failed to create education tables")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err = tx.Exec(ctx,
|
||||||
|
`
|
||||||
|
ALTER TABLE hmn_user
|
||||||
|
DROP edit_library,
|
||||||
|
ADD education_role INT NOT NULL DEFAULT 0;
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return oops.New(err, "failed to update user stuff")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,5 +79,14 @@ func (m AddEducationResources) Down(ctx context.Context, tx pgx.Tx) error {
|
||||||
return oops.New(err, "failed to delete education tables")
|
return oops.New(err, "failed to delete education tables")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err = tx.Exec(ctx, `
|
||||||
|
ALTER TABLE hmn_user
|
||||||
|
DROP education_role,
|
||||||
|
ADD edit_library BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return oops.New(err, "failed to delete education tables")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,26 +2,28 @@ package models
|
||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type EducationArticle struct {
|
type EduArticle struct {
|
||||||
ID int `db:"id"`
|
ID int `db:"id"`
|
||||||
|
|
||||||
Title string `db:"title"`
|
Title string `db:"title"`
|
||||||
Slug string `db:"slug"`
|
Slug string `db:"slug"`
|
||||||
Description string `db:"description"`
|
Description string `db:"description"`
|
||||||
|
Published bool `db:"published"` // Unpublished articles are visible to authors and beta testers.
|
||||||
|
|
||||||
Type EducationArticleType `db:"type"`
|
Type EduArticleType `db:"type"`
|
||||||
|
|
||||||
CurrentVersion int `db:"current_version"`
|
CurrentVersionID int `db:"current_version"`
|
||||||
|
CurrentVersion *EduArticleVersion // not in DB, set by helpers
|
||||||
}
|
}
|
||||||
|
|
||||||
type EducationArticleType int
|
type EduArticleType int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
EducationArticleTypeArticle EducationArticleType = iota + 1
|
EduArticleTypeArticle EduArticleType = iota + 1
|
||||||
EducationArticleTypeGlossary
|
EduArticleTypeGlossary
|
||||||
)
|
)
|
||||||
|
|
||||||
type EducationArticleVersion struct {
|
type EduArticleVersion struct {
|
||||||
ID int `db:"id"`
|
ID int `db:"id"`
|
||||||
ArticleID int `db:"article_id"`
|
ArticleID int `db:"article_id"`
|
||||||
Date time.Time `db:"date"`
|
Date time.Time `db:"date"`
|
||||||
|
@ -30,3 +32,11 @@ type EducationArticleVersion struct {
|
||||||
ContentRaw string `db:"content_raw"`
|
ContentRaw string `db:"content_raw"`
|
||||||
ContentHTML string `db:"content_html"`
|
ContentHTML string `db:"content_html"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EduRole int
|
||||||
|
|
||||||
|
const (
|
||||||
|
EduRoleNone EduRole = iota
|
||||||
|
EduRoleBeta
|
||||||
|
EduRoleAuthor
|
||||||
|
)
|
||||||
|
|
|
@ -13,9 +13,9 @@ type UserStatus int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
UserStatusInactive UserStatus = 1 // Default for new users
|
UserStatusInactive UserStatus = 1 // Default for new users
|
||||||
UserStatusConfirmed = 2 // Confirmed email address
|
UserStatusConfirmed UserStatus = 2 // Confirmed email address
|
||||||
UserStatusApproved = 3 // Approved by an admin and allowed to publicly post
|
UserStatusApproved UserStatus = 3 // Approved by an admin and allowed to publicly post
|
||||||
UserStatusBanned = 4 // BALEETED
|
UserStatusBanned UserStatus = 4 // BALEETED
|
||||||
)
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
|
@ -28,8 +28,9 @@ type User struct {
|
||||||
DateJoined time.Time `db:"date_joined"`
|
DateJoined time.Time `db:"date_joined"`
|
||||||
LastLogin *time.Time `db:"last_login"`
|
LastLogin *time.Time `db:"last_login"`
|
||||||
|
|
||||||
IsStaff bool `db:"is_staff"`
|
IsStaff bool `db:"is_staff"`
|
||||||
Status UserStatus `db:"status"`
|
Status UserStatus `db:"status"`
|
||||||
|
EducationRole EduRole `db:"education_role"`
|
||||||
|
|
||||||
Name string `db:"name"`
|
Name string `db:"name"`
|
||||||
Bio string `db:"bio"`
|
Bio string `db:"bio"`
|
||||||
|
@ -40,8 +41,7 @@ type User struct {
|
||||||
DarkTheme bool `db:"darktheme"`
|
DarkTheme bool `db:"darktheme"`
|
||||||
Timezone string `db:"timezone"`
|
Timezone string `db:"timezone"`
|
||||||
|
|
||||||
ShowEmail bool `db:"showemail"`
|
ShowEmail bool `db:"showemail"`
|
||||||
CanEditLibrary bool `db:"edit_library"`
|
|
||||||
|
|
||||||
DiscordSaveShowcase bool `db:"discord_save_showcase"`
|
DiscordSaveShowcase bool `db:"discord_save_showcase"`
|
||||||
DiscordDeleteSnippetOnMessageDelete bool `db:"discord_delete_snippet_on_message_delete"`
|
DiscordDeleteSnippetOnMessageDelete bool `db:"discord_delete_snippet_on_message_delete"`
|
||||||
|
@ -63,3 +63,11 @@ func (u *User) BestName() string {
|
||||||
func (u *User) IsActive() bool {
|
func (u *User) IsActive() bool {
|
||||||
return u.Status == UserStatusConfirmed
|
return u.Status == UserStatusConfirmed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *User) CanSeeUnpublishedEducationContent() bool {
|
||||||
|
return u.IsStaff || u.EducationRole == EduRoleBeta || u.EducationRole == EduRoleAuthor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) CanAuthorEducation() bool {
|
||||||
|
return u.IsStaff || u.EducationRole == EduRoleAuthor
|
||||||
|
}
|
||||||
|
|
|
@ -211,7 +211,6 @@ func UserToTemplate(u *models.User, currentTheme string) User {
|
||||||
DarkTheme: u.DarkTheme,
|
DarkTheme: u.DarkTheme,
|
||||||
Timezone: u.Timezone,
|
Timezone: u.Timezone,
|
||||||
|
|
||||||
CanEditLibrary: u.CanEditLibrary,
|
|
||||||
DiscordSaveShowcase: u.DiscordSaveShowcase,
|
DiscordSaveShowcase: u.DiscordSaveShowcase,
|
||||||
DiscordDeleteSnippetOnMessageDelete: u.DiscordDeleteSnippetOnMessageDelete,
|
DiscordDeleteSnippetOnMessageDelete: u.DiscordDeleteSnippetOnMessageDelete,
|
||||||
}
|
}
|
||||||
|
@ -493,6 +492,27 @@ func TagToTemplate(t *models.Tag) Tag {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func EducationArticleToTemplate(a *models.EduArticle, currentVersion *models.EduArticleVersion) EduArticle {
|
||||||
|
res := EduArticle{
|
||||||
|
Title: a.Title,
|
||||||
|
Slug: a.Slug,
|
||||||
|
Description: a.Description,
|
||||||
|
Published: a.Published,
|
||||||
|
|
||||||
|
Url: hmnurl.BuildEducationArticle(a.Slug),
|
||||||
|
EditUrl: hmnurl.BuildEducationArticleEdit(a.Slug),
|
||||||
|
DeleteUrl: hmnurl.BuildEducationArticleDelete(a.Slug),
|
||||||
|
|
||||||
|
Content: "NO CONTENT HERE FOLKS YOU DID A BUG",
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentVersion != nil {
|
||||||
|
res.Content = template.HTML(currentVersion.ContentHTML)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
func maybeString(s *string) string {
|
func maybeString(s *string) string {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return ""
|
return ""
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
{{ template "base.html" . }}
|
||||||
|
|
||||||
|
{{ define "content" }}
|
||||||
|
<h2>Articles</h2>
|
||||||
|
<ul>
|
||||||
|
{{ range .Articles }}
|
||||||
|
<li>
|
||||||
|
{{ .Title }}
|
||||||
|
</li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Glossary Terms</h2>
|
||||||
|
<ul>
|
||||||
|
{{ range .GlossaryTerms }}
|
||||||
|
<li>
|
||||||
|
{{ .Title }}
|
||||||
|
</li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
{{ end }}
|
|
@ -189,7 +189,6 @@ type User struct {
|
||||||
ShowEmail bool
|
ShowEmail bool
|
||||||
Timezone string
|
Timezone string
|
||||||
|
|
||||||
CanEditLibrary bool
|
|
||||||
DiscordSaveShowcase bool
|
DiscordSaveShowcase bool
|
||||||
DiscordDeleteSnippetOnMessageDelete bool
|
DiscordDeleteSnippetOnMessageDelete bool
|
||||||
}
|
}
|
||||||
|
@ -385,3 +384,16 @@ type Tag struct {
|
||||||
Text string
|
Text string
|
||||||
Url string
|
Url string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EduArticle struct {
|
||||||
|
Title string
|
||||||
|
Slug string
|
||||||
|
Description string
|
||||||
|
Published bool
|
||||||
|
|
||||||
|
Url string
|
||||||
|
EditUrl string
|
||||||
|
DeleteUrl string
|
||||||
|
|
||||||
|
Content template.HTML
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.handmade.network/hmn/hmn/src/db"
|
"git.handmade.network/hmn/hmn/src/db"
|
||||||
|
"git.handmade.network/hmn/hmn/src/hmndata"
|
||||||
"git.handmade.network/hmn/hmn/src/models"
|
"git.handmade.network/hmn/hmn/src/models"
|
||||||
"git.handmade.network/hmn/hmn/src/templates"
|
"git.handmade.network/hmn/hmn/src/templates"
|
||||||
)
|
)
|
||||||
|
@ -46,11 +47,11 @@ func EducationArticle(c *RequestContext) ResponseData {
|
||||||
}
|
}
|
||||||
|
|
||||||
type articleResult struct {
|
type articleResult struct {
|
||||||
Article models.EducationArticle `db:"a"`
|
Article models.EduArticle `db:"a"`
|
||||||
CurrentVersion models.EducationArticleVersion `db:"v"`
|
CurrentVersion models.EduArticleVersion `db:"v"`
|
||||||
}
|
}
|
||||||
|
|
||||||
article, err := db.QueryOne[articleResult](c.Context(), c.Conn,
|
article, err := db.QueryOne[articleResult](c, c.Conn,
|
||||||
`
|
`
|
||||||
SELECT $columns
|
SELECT $columns
|
||||||
FROM
|
FROM
|
||||||
|
@ -61,7 +62,7 @@ func EducationArticle(c *RequestContext) ResponseData {
|
||||||
AND type = $2
|
AND type = $2
|
||||||
`,
|
`,
|
||||||
c.PathParams["slug"],
|
c.PathParams["slug"],
|
||||||
models.EducationArticleTypeArticle,
|
models.EduArticleTypeArticle,
|
||||||
)
|
)
|
||||||
if errors.Is(err, db.NotFound) {
|
if errors.Is(err, db.NotFound) {
|
||||||
return FourOhFour(c)
|
return FourOhFour(c)
|
||||||
|
@ -78,3 +79,48 @@ func EducationArticle(c *RequestContext) ResponseData {
|
||||||
res.MustWriteTemplate("education_article.html", tmpl, c.Perf)
|
res.MustWriteTemplate("education_article.html", tmpl, c.Perf)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func EducationAdmin(c *RequestContext) ResponseData {
|
||||||
|
articles, err := hmndata.FetchEduArticles(c, c.Conn, c.CurrentUser, hmndata.EduArticleQuery{})
|
||||||
|
if err != nil {
|
||||||
|
return c.ErrorResponse(http.StatusInternalServerError, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tmplArticles []templates.EduArticle
|
||||||
|
var tmplGlossaryTerms []templates.EduArticle
|
||||||
|
for _, a := range articles {
|
||||||
|
tmpl := templates.EducationArticleToTemplate(&a.Article, &a.CurrentVersion)
|
||||||
|
switch a.Article.Type {
|
||||||
|
case models.EduArticleTypeArticle:
|
||||||
|
tmplArticles = append(tmplArticles, tmpl)
|
||||||
|
case models.EduArticleTypeGlossary:
|
||||||
|
tmplGlossaryTerms = append(tmplGlossaryTerms, tmpl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type adminData struct {
|
||||||
|
templates.BaseData
|
||||||
|
Articles []templates.EduArticle
|
||||||
|
GlossaryTerms []templates.EduArticle
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl := adminData{
|
||||||
|
BaseData: getBaseData(c, "Education Admin", nil),
|
||||||
|
Articles: tmplArticles,
|
||||||
|
GlossaryTerms: tmplGlossaryTerms,
|
||||||
|
}
|
||||||
|
|
||||||
|
var res ResponseData
|
||||||
|
res.MustWriteTemplate("education_admin.html", tmpl, c.Perf)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func EducationArticleEdit(c *RequestContext) ResponseData {
|
||||||
|
// TODO
|
||||||
|
panic("not implemented yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
func EducationArticleDelete(c *RequestContext) ResponseData {
|
||||||
|
// TODO
|
||||||
|
panic("not implemented yet")
|
||||||
|
}
|
||||||
|
|
|
@ -56,7 +56,7 @@ func trackRequestPerf(h Handler) Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
func needsAuth(h Handler) Handler {
|
func needsAuth(h Handler) Handler {
|
||||||
return func(c *RequestContext) (res ResponseData) {
|
return func(c *RequestContext) ResponseData {
|
||||||
if c.CurrentUser == nil {
|
if c.CurrentUser == nil {
|
||||||
return c.Redirect(hmnurl.BuildLoginPage(c.FullUrl()), http.StatusSeeOther)
|
return c.Redirect(hmnurl.BuildLoginPage(c.FullUrl()), http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,7 @@ func needsAuth(h Handler) Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
func adminsOnly(h Handler) Handler {
|
func adminsOnly(h Handler) Handler {
|
||||||
return func(c *RequestContext) (res ResponseData) {
|
return func(c *RequestContext) ResponseData {
|
||||||
if c.CurrentUser == nil || !c.CurrentUser.IsStaff {
|
if c.CurrentUser == nil || !c.CurrentUser.IsStaff {
|
||||||
return FourOhFour(c)
|
return FourOhFour(c)
|
||||||
}
|
}
|
||||||
|
@ -75,6 +75,16 @@ func adminsOnly(h Handler) Handler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func educationAuthorsOnly(h Handler) Handler {
|
||||||
|
return func(c *RequestContext) ResponseData {
|
||||||
|
if c.CurrentUser == nil || !c.CurrentUser.CanAuthorEducation() {
|
||||||
|
return FourOhFour(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func csrfMiddleware(h Handler) Handler {
|
func csrfMiddleware(h Handler) Handler {
|
||||||
// CSRF mitigation actions per the OWASP cheat sheet:
|
// CSRF mitigation actions per the OWASP cheat sheet:
|
||||||
// https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
|
// https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
|
||||||
|
|
|
@ -118,12 +118,17 @@ func NewWebsiteRoutes(conn *pgxpool.Pool) http.Handler {
|
||||||
hmnOnly.GET(hmnurl.RegexFishbowl, Fishbowl)
|
hmnOnly.GET(hmnurl.RegexFishbowl, Fishbowl)
|
||||||
|
|
||||||
hmnOnly.GET(hmnurl.RegexEducationIndex, EducationIndex)
|
hmnOnly.GET(hmnurl.RegexEducationIndex, EducationIndex)
|
||||||
hmnOnly.GET(hmnurl.RegexEducationGlossary, EducationGlossary) // Must be above article so `/glossary` does not match as an article slug
|
hmnOnly.GET(hmnurl.RegexEducationGlossary, EducationGlossary)
|
||||||
hmnOnly.GET(hmnurl.RegexEducationArticle, EducationArticle)
|
hmnOnly.GET(hmnurl.RegexEducationAdmin, educationAuthorsOnly(EducationAdmin))
|
||||||
|
hmnOnly.GET(hmnurl.RegexEducationArticle, EducationArticle) // Article stuff must be last so `/glossary` and others do not match as an article slug
|
||||||
|
hmnOnly.GET(hmnurl.RegexEducationArticleEdit, educationAuthorsOnly(EducationArticleEdit))
|
||||||
|
hmnOnly.GET(hmnurl.RegexEducationArticleDelete, educationAuthorsOnly(EducationArticleDelete))
|
||||||
|
|
||||||
hmnOnly.POST(hmnurl.RegexAPICheckUsername, csrfMiddleware(APICheckUsername))
|
hmnOnly.POST(hmnurl.RegexAPICheckUsername, csrfMiddleware(APICheckUsername))
|
||||||
|
|
||||||
hmnOnly.GET(hmnurl.RegexLibraryAny, LibraryNotPortedYet)
|
hmnOnly.GET(hmnurl.RegexLibraryAny, func(c *RequestContext) ResponseData {
|
||||||
|
return c.Redirect(hmnurl.BuildEducationIndex(), http.StatusFound)
|
||||||
|
})
|
||||||
|
|
||||||
// Project routes can appear either at the root (e.g. hero.handmade.network/edit)
|
// Project routes can appear either at the root (e.g. hero.handmade.network/edit)
|
||||||
// or on a personal project path (e.g. handmade.network/p/123/hero/edit). So, we
|
// or on a personal project path (e.g. handmade.network/p/123/hero/edit). So, we
|
||||||
|
|
Loading…
Reference in New Issue