From 5ab8633cf5b5167d3c354d925169b5518a6bc7eb Mon Sep 17 00:00:00 2001
From: Kelly Wong <kelly@Kellys-MacBook-Air-2.local>
Date: Wed, 20 Mar 2024 21:50:40 +0800
Subject: [PATCH 01/17] inital stories moderation

---
 .env.example                                 | 18 ----
 controller/stories/stories.go                | 66 +++++++++++++++
 controller/stories/update.go                 | 62 ++++++++++++++
 go.mod                                       | 16 +++-
 go.sum                                       | 48 +++++++++++
 internal/auth/middleware.go                  |  7 +-
 internal/permissiongroups/stories/stories.go |  5 ++
 internal/permissions/users/permissions.go    |  1 +
 internal/permissions/users/role.go           |  2 +
 internal/permissions/users/users.go          |  1 +
 internal/router/router.go                    |  7 +-
 migrations/20230807190030-add_status.sql     |  9 ++
 model/stories.go                             | 86 ++++++++++++++++----
 model/usergroups.go                          | 15 ++++
 params/stories/moderate.go                   | 28 +++++++
 15 files changed, 332 insertions(+), 39 deletions(-)
 delete mode 100644 .env.example
 create mode 100644 migrations/20230807190030-add_status.sql
 create mode 100644 params/stories/moderate.go

diff --git a/.env.example b/.env.example
deleted file mode 100644
index 042807b..0000000
--- a/.env.example
+++ /dev/null
@@ -1,18 +0,0 @@
-HOST=localhost
-PORT=8080
-
-DB_TIMEZONE=Asia/Singapore
-DB_HOSTNAME=localhost
-DB_PORT=5432
-DB_USERNAME=postgres
-DB_PASSWORD=
-DB_NAME=
-
-SENTRY_DSN=
-
-JWKS_ENDPOINT=
-
-# only for non-development mode
-ALLOWED_ORIGIN_1=
-ALLOWED_ORIGIN_2=
-# ... and so on
diff --git a/controller/stories/stories.go b/controller/stories/stories.go
index aed024e..4453614 100644
--- a/controller/stories/stories.go
+++ b/controller/stories/stories.go
@@ -54,6 +54,72 @@ func HandleList(w http.ResponseWriter, r *http.Request) error {
 	return nil
 }
 
+func HandleListPublished(w http.ResponseWriter, r *http.Request) error {
+	err := auth.CheckPermissions(r, storypermissiongroups.List())
+	if err != nil {
+		logrus.Error(err)
+		return apierrors.ClientForbiddenError{
+			Message: fmt.Sprintf("Error listing stories: %v", err),
+		}
+	}
+
+	// Get DB instance
+	db, err := database.GetDBFrom(r)
+	if err != nil {
+		logrus.Error(err)
+		return err
+	}
+
+	// Get group id from context
+	groupID, err := usergroups.GetGroupIDFrom(r)
+	if err != nil {
+		logrus.Error(err)
+		return err
+	}
+
+	stories, err := model.GetAllPublishedStories(db, groupID)
+	if err != nil {
+		logrus.Error(err)
+		return err
+	}
+
+	controller.EncodeJSONResponse(w, storyviews.ListFrom(stories))
+	return nil
+}
+
+func HandleListDraft(w http.ResponseWriter, r *http.Request) error {
+	err := auth.CheckPermissions(r, storypermissiongroups.List())
+	if err != nil {
+		logrus.Error(err)
+		return apierrors.ClientForbiddenError{
+			Message: fmt.Sprintf("Error listing stories: %v", err),
+		}
+	}
+
+	// Get DB instance
+	db, err := database.GetDBFrom(r)
+	if err != nil {
+		logrus.Error(err)
+		return err
+	}
+
+	// Get group id from context
+	groupID, err := usergroups.GetGroupIDFrom(r)
+	if err != nil {
+		logrus.Error(err)
+		return err
+	}
+
+	stories, err := model.GetAllDraftStories(db, groupID)
+	if err != nil {
+		logrus.Error(err)
+		return err
+	}
+
+	controller.EncodeJSONResponse(w, storyviews.ListFrom(stories))
+	return nil
+}
+
 func HandleRead(w http.ResponseWriter, r *http.Request) error {
 	storyIDStr := chi.URLParam(r, "storyID")
 	storyID, err := strconv.Atoi(storyIDStr)
diff --git a/controller/stories/update.go b/controller/stories/update.go
index 3079e67..61dda32 100644
--- a/controller/stories/update.go
+++ b/controller/stories/update.go
@@ -85,3 +85,65 @@ func HandleUpdate(w http.ResponseWriter, r *http.Request) error {
 	controller.EncodeJSONResponse(w, storyviews.SingleFrom(storyModel))
 	return nil
 }
+
+func HandlePublish(w http.ResponseWriter, r *http.Request) error {
+	storyIDStr := chi.URLParam(r, "storyID")
+	storyID, err := strconv.Atoi(storyIDStr)
+	if err != nil {
+		return apierrors.ClientBadRequestError{
+			Message: fmt.Sprintf("Invalid storyID: %v", err),
+		}
+	}
+
+	err = auth.CheckPermissions(r, storypermissiongroups.Publish())
+	if err != nil {
+		logrus.Error(err)
+		return apierrors.ClientForbiddenError{
+			Message: fmt.Sprintf("Error moderating story: %v", err),
+		}
+	}
+
+	// Extra params won't do anything, e.g. authorID can't be changed.
+	// TODO: Error on extra params?
+	var params storyparams.Update
+	if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
+		e, ok := err.(*json.UnmarshalTypeError)
+		if !ok {
+			logrus.Error(err)
+			return apierrors.ClientBadRequestError{
+				Message: fmt.Sprintf("Bad JSON parsing: %v", err),
+			}
+		}
+
+		// TODO: Investigate if we should use errors.Wrap instead
+		return apierrors.ClientUnprocessableEntityError{
+			Message: fmt.Sprintf("Invalid JSON format: %s should be a %s.", e.Field, e.Type),
+		}
+	}
+
+	err = params.Validate()
+	if err != nil {
+		logrus.Error(err)
+		return apierrors.ClientUnprocessableEntityError{
+			Message: fmt.Sprintf("JSON validation failed: %v", err),
+		}
+	}
+
+	storyModel := *params.ToModel()
+
+	// Get DB instance
+	db, err := database.GetDBFrom(r)
+	if err != nil {
+		logrus.Error(err)
+		return err
+	}
+
+	err = model.UpdateStory(db, storyID, &storyModel)
+	if err != nil {
+		logrus.Error(err)
+		return err
+	}
+
+	controller.EncodeJSONResponse(w, storyviews.SingleFrom(storyModel))
+	return nil
+}
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 9a7d32c..d39159e 100644
--- a/go.mod
+++ b/go.mod
@@ -19,11 +19,13 @@ require (
 )
 
 require (
+	github.com/BurntSushi/toml v1.2.1 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
 	github.com/go-gorp/gorp/v3 v3.1.0 // indirect
 	github.com/go-sql-driver/mysql v1.7.1 // indirect
 	github.com/goccy/go-json v0.10.2 // indirect
+	github.com/google/go-cmp v0.5.9 // indirect
 	github.com/jackc/pgpassfile v1.0.0 // indirect
 	github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
 	github.com/jinzhu/inflection v1.0.0 // indirect
@@ -38,8 +40,20 @@ require (
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/rogpeppe/go-internal v1.10.0 // indirect
 	github.com/segmentio/asm v1.2.0 // indirect
+	github.com/sergi/go-diff v1.1.0 // indirect
+	github.com/zmb3/gogetdoc v0.0.0-20190228002656-b37376c5da6a // indirect
 	golang.org/x/crypto v0.17.0 // indirect
-	golang.org/x/sys v0.15.0 // indirect
+	golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 // indirect
+	golang.org/x/mod v0.15.0 // indirect
+	golang.org/x/sync v0.6.0 // indirect
+	golang.org/x/sys v0.16.0 // indirect
+	golang.org/x/telemetry v0.0.0-20231114163143-69313e640400 // indirect
 	golang.org/x/text v0.14.0 // indirect
+	golang.org/x/tools v0.17.0 // indirect
+	golang.org/x/tools/gopls v0.14.2 // indirect
+	golang.org/x/vuln v1.0.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
+	honnef.co/go/tools v0.4.5 // indirect
+	mvdan.cc/gofumpt v0.4.0 // indirect
+	mvdan.cc/xurls/v2 v2.4.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 00f7ef2..3f033f1 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,5 @@
+github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
+github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
 github.com/TwiN/go-color v1.4.0 h1:fNbOwOrvup5oj934UragnW0B1WKaAkkB85q19Y7h4ng=
 github.com/TwiN/go-color v1.4.0/go.mod h1:0QTVEPlu+AoCyTrho7bXbVkrCkVpdQr7YF7PYWEtSxM=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -25,6 +27,7 @@ github.com/gobuffalo/packr/v2 v2.8.3 h1:xE1yzvnO56cUC0sTpKR3DIbxZgB54AftTFMhB2XE
 github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
 github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
 github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
 github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
@@ -38,7 +41,10 @@ github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/
 github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
 github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
 github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
 github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
@@ -61,22 +67,27 @@ github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI=
 github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
 github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
 github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY=
+github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
 github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
 github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
 github.com/rubenv/sql-migrate v1.5.1 h1:WsZo4jPQfjmddDTh/suANP2aKPA7/ekN0LzuuajgQEo=
 github.com/rubenv/sql-migrate v1.5.1/go.mod h1:H38GW8Vqf8F0Su5XignRyaRcbXbJunSWxs+kmzlg0Is=
 github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
 github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
+github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
+github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
 github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
 github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -84,21 +95,34 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/zmb3/gogetdoc v0.0.0-20190228002656-b37376c5da6a h1:00UFliGZl2UciXe8o/2iuEsRQ9u7z0rzDTVzuj6EYY0=
+github.com/zmb3/gogetdoc v0.0.0-20190228002656-b37376c5da6a/go.mod h1:ofmGw6LrMypycsiWcyug6516EXpIxSbZ+uI9ppGypfY=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
 golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
 golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 h1:2O2DON6y3XMJiQRAS1UWU+54aec2uopH3x7MAiqGW6Y=
+golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
+golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
+golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
+golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -110,6 +134,10 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
 golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
+golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/telemetry v0.0.0-20231114163143-69313e640400 h1:brbkEFfGwNGAEkykUOcryE/JiHUMMJouzE0fWWmz/QU=
+golang.org/x/telemetry v0.0.0-20231114163143-69313e640400/go.mod h1:P6hMdmAcoG7FyATwqSr6R/U0n7yeXNP/QXeRlxb1szE=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -123,12 +151,26 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181207195948-8634b1ecd393/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.14.1-0.20231114185516-c9d3e7de13fd h1:Oku7E+OCrXHyst1dG1z10etCTxewCHXNFLRlyMPbh3w=
+golang.org/x/tools v0.14.1-0.20231114185516-c9d3e7de13fd/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
+golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
+golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
+golang.org/x/tools/gopls v0.14.2 h1:sIw6vjZiuQ9S7s0auUUkHlWgsCkKZFWDHmrge8LYsnc=
+golang.org/x/tools/gopls v0.14.2/go.mod h1:o2s+suLlFye+MHxEofsxJPkBQ/kT/ucP4iJV3ohjGEQ=
+golang.org/x/vuln v1.0.1 h1:KUas02EjQK5LTuIx1OylBQdKKZ9jeugs+HiqO5HormU=
+golang.org/x/vuln v1.0.1/go.mod h1:bb2hMwln/tqxg32BNY4CcxHWtHXuYa3SbIBmtsyjxtM=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -136,3 +178,9 @@ gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0=
 gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8=
 gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64=
 gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
+honnef.co/go/tools v0.4.5 h1:YGD4H+SuIOOqsyoLOpZDWcieM28W47/zRO7f+9V3nvo=
+honnef.co/go/tools v0.4.5/go.mod h1:GUV+uIBCLpdf0/v6UhHHG/yzI/z6qPskBeQCjcNB96k=
+mvdan.cc/gofumpt v0.4.0 h1:JVf4NN1mIpHogBj7ABpgOyZc65/UUOkKQFkoURsz4MM=
+mvdan.cc/gofumpt v0.4.0/go.mod h1:PljLOHDeZqgS8opHRKLzp2It2VBuSdteAgqUfzMTxlQ=
+mvdan.cc/xurls/v2 v2.4.0 h1:tzxjVAj+wSBmDcF6zBB7/myTy3gX9xvi8Tyr28AuQgc=
+mvdan.cc/xurls/v2 v2.4.0/go.mod h1:+GEjq9uNjqs8LQfM9nVnM8rff0OQ5Iash5rzX+N1CSg=
diff --git a/internal/auth/middleware.go b/internal/auth/middleware.go
index 88987e3..85e01db 100644
--- a/internal/auth/middleware.go
+++ b/internal/auth/middleware.go
@@ -28,8 +28,11 @@ func MakeMiddlewareFrom(conf *config.Config) func(http.Handler) http.Handler {
 	// Skip auth in development mode
 	if conf.Environment == envutils.ENV_DEVELOPMENT {
 		return func(next http.Handler) http.Handler {
-			return next
-		}
+			return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			 r = injectUserIDToContext(r, 1)
+			 next.ServeHTTP(w, r)
+			})
+		}		 
 	}
 
 	keySet := getJWKS(conf.JWKSEndpoint)
diff --git a/internal/permissiongroups/stories/stories.go b/internal/permissiongroups/stories/stories.go
index 06ddcbd..5d38811 100644
--- a/internal/permissiongroups/stories/stories.go
+++ b/internal/permissiongroups/stories/stories.go
@@ -20,6 +20,11 @@ func Read() permissions.PermissionGroup {
 		GetRolePermission(userpermissions.CanReadStories)
 }
 
+func Publish() permissions.PermissionGroup {
+	return userpermissions.
+		GetRolePermission(userpermissions.CanPublishStories)
+}
+
 func Update(storyID uint) permissions.PermissionGroup {
 	return permissions.AnyOf{
 		Groups: []permissions.PermissionGroup{
diff --git a/internal/permissions/users/permissions.go b/internal/permissions/users/permissions.go
index bc0a2f7..a5673bb 100644
--- a/internal/permissions/users/permissions.go
+++ b/internal/permissions/users/permissions.go
@@ -17,4 +17,5 @@ const (
 	CanReadStories   Permission = "can_read_stories"
 	CanUpdateStories Permission = "can_update_stories"
 	CanDeleteStories Permission = "can_delete_stories"
+	CanPublishStories Permission = "can_publish_stories"
 )
diff --git a/internal/permissions/users/role.go b/internal/permissions/users/role.go
index c0dccc7..0e0c7c4 100644
--- a/internal/permissions/users/role.go
+++ b/internal/permissions/users/role.go
@@ -19,3 +19,5 @@ func (p RolePermission) IsAuthorized(r *http.Request) bool {
 	}
 	return groupenums.IsRoleGreaterThan(*role, p.Role)
 }
+
+
diff --git a/internal/permissions/users/users.go b/internal/permissions/users/users.go
index a6e8b8b..d4c4992 100644
--- a/internal/permissions/users/users.go
+++ b/internal/permissions/users/users.go
@@ -23,6 +23,7 @@ func GetRolePermission(p Permission) *RolePermission {
 	case
 		// Additional permissions for moderators and administrators
 		CanUpdateStories,
+		CanPublishStories,
 		CanDeleteStories:
 		return &RolePermission{
 			Permission: p,
diff --git a/internal/router/router.go b/internal/router/router.go
index 891e9e8..78f4ff8 100644
--- a/internal/router/router.go
+++ b/internal/router/router.go
@@ -55,18 +55,21 @@ func Setup(config *config.Config, injectMiddleWares []func(http.Handler) http.Ha
 			r.Use(usergroups.InjectUserGroupIntoContext)
 			r.Route("/stories", func(r chi.Router) {
 				r.Get("/", handleAPIError(stories.HandleList))
+				r.Get("/draft", handleAPIError(stories.HandleListDraft))
+				r.Get("/published", handleAPIError(stories.HandleListPublished))
 				r.Get("/{storyID}", handleAPIError(stories.HandleRead))
 				r.Put("/{storyID}", handleAPIError(stories.HandleUpdate))
+				r.Put("/{storyID}/publish", handleAPIError(stories.HandlePublish))
 				r.Delete("/{storyID}", handleAPIError(stories.HandleDelete))
 				r.Post("/", handleAPIError(stories.HandleCreate))
-			})
+			})			
 
 			r.Route("/users", func(r chi.Router) {
 				r.Get("/", handleAPIError(users.HandleList))
 				r.Get("/{userID}", handleAPIError(users.HandleRead))
 				r.Delete("/{userID}", handleAPIError(users.HandleDelete))
 				r.Post("/", handleAPIError(users.HandleCreate))
-				r.Post("/batch", handleAPIError(usergroupscontroller.HandleBatchCreate))
+				r.Put("/batch", handleAPIError(usergroupscontroller.HandleBatchCreate))
 			})
 		})
 
diff --git a/migrations/20230807190030-add_status.sql b/migrations/20230807190030-add_status.sql
new file mode 100644
index 0000000..8cf8060
--- /dev/null
+++ b/migrations/20230807190030-add_status.sql
@@ -0,0 +1,9 @@
+-- +migrate Up
+
+ALTER TABLE stories
+    ADD COLUMN status INT;
+
+-- +migrate Down
+
+ALTER TABLE stories
+    DROP COLUMN status;
\ No newline at end of file
diff --git a/model/stories.go b/model/stories.go
index a3ccc07..bcade9e 100644
--- a/model/stories.go
+++ b/model/stories.go
@@ -1,20 +1,31 @@
 package model
 
 import (
+	"fmt"
+
 	"github.com/source-academy/stories-backend/internal/database"
+	groupenums "github.com/source-academy/stories-backend/internal/enums/groups"
 	"gorm.io/gorm"
 	"gorm.io/gorm/clause"
 )
 
+type StoryStatus int
+
+const (
+    Draft StoryStatus = iota
+    Published
+)
+
 type Story struct {
-	gorm.Model
-	AuthorID uint
-	Author   User
-	GroupID  *uint // null means this is a public story
-	Group    Group
-	Title    string
-	Content  string
-	PinOrder *int // nil if not pinned
+    gorm.Model
+    AuthorID   uint
+    Author     User
+    GroupID    *uint
+    Group      Group
+    Title      string
+    Content    string
+    PinOrder   *int
+    Status     StoryStatus
 }
 
 // Passing nil to omit the filtering and get all stories
@@ -35,6 +46,38 @@ func GetAllStoriesInGroup(db *gorm.DB, groupID *uint) ([]Story, error) {
 	return stories, nil
 }
 
+func GetAllPublishedStories(db *gorm.DB, groupID *uint) ([]Story, error) {
+	var stories []Story
+	err := db.
+		// FIXME: Handle nil case properly
+		Where(Story{GroupID: groupID, Status: Published}).
+		Preload(clause.Associations).
+		// TODO: Abstract out the sorting logic
+		Order("pin_order ASC NULLS LAST, title ASC, content ASC").
+		Find(&stories).
+		Error
+	if err != nil {
+		return stories, database.HandleDBError(err, "story")
+	}
+	return stories, nil
+}
+
+func GetAllDraftStories(db *gorm.DB, groupID *uint) ([]Story, error) {
+	var stories []Story
+	err := db.
+		// FIXME: Handle nil case properly
+		Where(Story{GroupID: groupID, Status: Draft}).
+		Preload(clause.Associations).
+		// TODO: Abstract out the sorting logic
+		Order("pin_order ASC NULLS LAST, title ASC, content ASC").
+		Find(&stories).
+		Error
+	if err != nil {
+		return stories, database.HandleDBError(err, "story")
+	}
+	return stories, nil
+}
+
 func GetStoryByID(db *gorm.DB, id int) (Story, error) {
 	var story Story
 	err := db.
@@ -48,17 +91,29 @@ func GetStoryByID(db *gorm.DB, id int) (Story, error) {
 }
 
 func CreateStory(db *gorm.DB, story *Story) error {
-	err := db.
+	// Check author's role
+	role, err := GetUserRoleByID(db, story.AuthorID)
+	if err != nil {
+		return fmt.Errorf("failed to get user role: %w", err)
+	}
+
+	// Set story status based on author's role
+	if role == groupenums.RoleStandard {
+		story.Status = Draft
+	} else {
+		story.Status = Published
+	}
+
+	// Create the story in the database
+	if err := db.
 		Preload(clause.Associations).
 		Create(story).
-		// Get associated Author. See
-		// https://github.com/go-gorm/gen/issues/618 on why
-		// a separate .First() is needed.
+		// Get associated Author.
 		First(story).
-		Error
-	if err != nil {
-		return database.HandleDBError(err, "story")
+		Error; err != nil {
+		return fmt.Errorf("failed to create story: %w", err)
 	}
+
 	return nil
 }
 
@@ -88,7 +143,6 @@ func UpdateStory(db *gorm.DB, storyID int, newStory *Story) error {
 					return database.HandleDBError(err, "story")
 				}
 			}
-
 			// Update remaining fields
 			err = tx.
 				Preload(clause.Associations).
diff --git a/model/usergroups.go b/model/usergroups.go
index 3bab68e..66cdb83 100644
--- a/model/usergroups.go
+++ b/model/usergroups.go
@@ -31,6 +31,21 @@ func GetUserGroupByID(db *gorm.DB, userID uint, groupID uint) (UserGroup, error)
 	return userGroup, nil
 }
 
+func GetUserRoleByID(db *gorm.DB, userID uint) (groupenums.Role, error) {
+    var userGroup UserGroup
+
+    err := db.Model(&userGroup).
+        Where(UserGroup{UserID: userID}).
+        First(&userGroup).Error
+
+    if err != nil {
+        return userGroup.Role, database.HandleDBError(err, "userRole")
+    }
+
+    return userGroup.Role, nil
+}
+
+
 func CreateUserGroup(db *gorm.DB, userGroup *UserGroup) error {
 	err := db.Create(userGroup).Error
 	if err != nil {
diff --git a/params/stories/moderate.go b/params/stories/moderate.go
new file mode 100644
index 0000000..e667c8e
--- /dev/null
+++ b/params/stories/moderate.go
@@ -0,0 +1,28 @@
+package storyparams
+
+import (
+    "github.com/source-academy/stories-backend/model"
+)
+
+type Publish struct {
+    IsPublished bool `json:"boolean"`
+}
+
+// Validate validates the Publish params.
+func (params *Publish) Validate() error {
+    // Validation logic can be added here if needed.
+    return nil
+}
+
+// ToModel converts Publish params to a Story model.
+func (params *Publish) ToModel() *model.Story {
+	if (params.IsPublished) {
+		return &model.Story{
+			Status: model.Published,
+		}
+	} else {
+		return &model.Story{
+			Status: model.Draft,
+		}
+	}
+}
\ No newline at end of file

From eeb21063146344ae0261a70d6205220e61da8f24 Mon Sep 17 00:00:00 2001
From: Kelly Wong <kelly@Kellys-MacBook-Air-2.local>
Date: Wed, 20 Mar 2024 22:33:21 +0800
Subject: [PATCH 02/17] fixed GET published and draft stories

---
 model/stories.go | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/model/stories.go b/model/stories.go
index bcade9e..aedb316 100644
--- a/model/stories.go
+++ b/model/stories.go
@@ -49,8 +49,8 @@ func GetAllStoriesInGroup(db *gorm.DB, groupID *uint) ([]Story, error) {
 func GetAllPublishedStories(db *gorm.DB, groupID *uint) ([]Story, error) {
 	var stories []Story
 	err := db.
-		// FIXME: Handle nil case properly
-		Where(Story{GroupID: groupID, Status: Published}).
+		Where("status = ?", int(Published)).
+		Where("group_id = ?", groupID).
 		Preload(clause.Associations).
 		// TODO: Abstract out the sorting logic
 		Order("pin_order ASC NULLS LAST, title ASC, content ASC").
@@ -65,8 +65,8 @@ func GetAllPublishedStories(db *gorm.DB, groupID *uint) ([]Story, error) {
 func GetAllDraftStories(db *gorm.DB, groupID *uint) ([]Story, error) {
 	var stories []Story
 	err := db.
-		// FIXME: Handle nil case properly
-		Where(Story{GroupID: groupID, Status: Draft}).
+		Where("status = ?", int(Draft)).
+		Where("group_id = ?", groupID).
 		Preload(clause.Associations).
 		// TODO: Abstract out the sorting logic
 		Order("pin_order ASC NULLS LAST, title ASC, content ASC").

From a681b9602fd4f204a60d52f674b664cae4e9b371 Mon Sep 17 00:00:00 2001
From: Kelly Wong <kelly@Kellys-MacBook-Air-2.local>
Date: Wed, 20 Mar 2024 22:52:46 +0800
Subject: [PATCH 03/17] fixed merge conflicts 2

---
 go.sum           | 102 -----------------------------------------------
 model/stories.go |   3 --
 2 files changed, 105 deletions(-)

diff --git a/go.sum b/go.sum
index 7976614..e87f6f9 100644
--- a/go.sum
+++ b/go.sum
@@ -44,16 +44,9 @@ github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/
 github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
 github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
 github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw=
-<<<<<<< HEAD
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-=======
 github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
->>>>>>> main
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
@@ -102,81 +95,6 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-<<<<<<< HEAD
-github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
-github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-github.com/zmb3/gogetdoc v0.0.0-20190228002656-b37376c5da6a h1:00UFliGZl2UciXe8o/2iuEsRQ9u7z0rzDTVzuj6EYY0=
-github.com/zmb3/gogetdoc v0.0.0-20190228002656-b37376c5da6a/go.mod h1:ofmGw6LrMypycsiWcyug6516EXpIxSbZ+uI9ppGypfY=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
-golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
-golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
-golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 h1:2O2DON6y3XMJiQRAS1UWU+54aec2uopH3x7MAiqGW6Y=
-golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
-golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
-golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
-golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
-golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
-golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
-golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
-golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/telemetry v0.0.0-20231114163143-69313e640400 h1:brbkEFfGwNGAEkykUOcryE/JiHUMMJouzE0fWWmz/QU=
-golang.org/x/telemetry v0.0.0-20231114163143-69313e640400/go.mod h1:P6hMdmAcoG7FyATwqSr6R/U0n7yeXNP/QXeRlxb1szE=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
-golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20181207195948-8634b1ecd393/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.14.1-0.20231114185516-c9d3e7de13fd h1:Oku7E+OCrXHyst1dG1z10etCTxewCHXNFLRlyMPbh3w=
-golang.org/x/tools v0.14.1-0.20231114185516-c9d3e7de13fd/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
-golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
-golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
-golang.org/x/tools/gopls v0.14.2 h1:sIw6vjZiuQ9S7s0auUUkHlWgsCkKZFWDHmrge8LYsnc=
-golang.org/x/tools/gopls v0.14.2/go.mod h1:o2s+suLlFye+MHxEofsxJPkBQ/kT/ucP4iJV3ohjGEQ=
-golang.org/x/vuln v1.0.1 h1:KUas02EjQK5LTuIx1OylBQdKKZ9jeugs+HiqO5HormU=
-golang.org/x/vuln v1.0.1/go.mod h1:bb2hMwln/tqxg32BNY4CcxHWtHXuYa3SbIBmtsyjxtM=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-=======
 github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
@@ -190,29 +108,10 @@ golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
 golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
 golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
->>>>>>> main
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
-<<<<<<< HEAD
-gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
-gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0=
-gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8=
-gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64=
-gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
-honnef.co/go/tools v0.4.5 h1:YGD4H+SuIOOqsyoLOpZDWcieM28W47/zRO7f+9V3nvo=
-honnef.co/go/tools v0.4.5/go.mod h1:GUV+uIBCLpdf0/v6UhHHG/yzI/z6qPskBeQCjcNB96k=
-mvdan.cc/gofumpt v0.4.0 h1:JVf4NN1mIpHogBj7ABpgOyZc65/UUOkKQFkoURsz4MM=
-mvdan.cc/gofumpt v0.4.0/go.mod h1:PljLOHDeZqgS8opHRKLzp2It2VBuSdteAgqUfzMTxlQ=
-mvdan.cc/xurls/v2 v2.4.0 h1:tzxjVAj+wSBmDcF6zBB7/myTy3gX9xvi8Tyr28AuQgc=
-mvdan.cc/xurls/v2 v2.4.0/go.mod h1:+GEjq9uNjqs8LQfM9nVnM8rff0OQ5Iash5rzX+N1CSg=
-=======
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
@@ -221,4 +120,3 @@ gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
 gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
 gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
 gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
->>>>>>> main
diff --git a/model/stories.go b/model/stories.go
index 6695099..0f08f56 100644
--- a/model/stories.go
+++ b/model/stories.go
@@ -1,10 +1,7 @@
 package model
 
 import (
-	"fmt"
-
 	"github.com/source-academy/stories-backend/internal/database"
-	groupenums "github.com/source-academy/stories-backend/internal/enums/groups"
 	"gorm.io/gorm"
 	"gorm.io/gorm/clause"
 )

From 2844a8916ae367a776c668b1821dfc091692cb02 Mon Sep 17 00:00:00 2001
From: keyansheng <65121402+keyansheng@users.noreply.github.com>
Date: Wed, 20 Mar 2024 22:59:15 +0800
Subject: [PATCH 04/17] Run make format

Running pre-commit hook...
Go code is not formatted, commit blocked!
The following files are not formatted:

./controller/stories/update.go
./internal/auth/middleware.go
./internal/permissions/users/permissions.go
./internal/permissions/users/role.go
./internal/router/router.go
./model/stories.go
./model/usergroups.go
./params/stories/moderate.go

Run 'make format' to format your files and try again.
---
 controller/stories/update.go              |  2 +-
 internal/auth/middleware.go               |  6 +++---
 internal/permissions/users/permissions.go |  8 ++++----
 internal/permissions/users/role.go        |  2 --
 internal/router/router.go                 |  2 +-
 model/stories.go                          | 22 +++++++++++-----------
 model/usergroups.go                       | 17 ++++++++---------
 params/stories/moderate.go                | 12 ++++++------
 8 files changed, 34 insertions(+), 37 deletions(-)

diff --git a/controller/stories/update.go b/controller/stories/update.go
index 61dda32..6988f75 100644
--- a/controller/stories/update.go
+++ b/controller/stories/update.go
@@ -146,4 +146,4 @@ func HandlePublish(w http.ResponseWriter, r *http.Request) error {
 
 	controller.EncodeJSONResponse(w, storyviews.SingleFrom(storyModel))
 	return nil
-}
\ No newline at end of file
+}
diff --git a/internal/auth/middleware.go b/internal/auth/middleware.go
index 85e01db..44317d8 100644
--- a/internal/auth/middleware.go
+++ b/internal/auth/middleware.go
@@ -29,10 +29,10 @@ func MakeMiddlewareFrom(conf *config.Config) func(http.Handler) http.Handler {
 	if conf.Environment == envutils.ENV_DEVELOPMENT {
 		return func(next http.Handler) http.Handler {
 			return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-			 r = injectUserIDToContext(r, 1)
-			 next.ServeHTTP(w, r)
+				r = injectUserIDToContext(r, 1)
+				next.ServeHTTP(w, r)
 			})
-		}		 
+		}
 	}
 
 	keySet := getJWKS(conf.JWKSEndpoint)
diff --git a/internal/permissions/users/permissions.go b/internal/permissions/users/permissions.go
index a5673bb..9cb276d 100644
--- a/internal/permissions/users/permissions.go
+++ b/internal/permissions/users/permissions.go
@@ -13,9 +13,9 @@ const (
 	CanUpdateGroups Permission = "can_update_groups"
 	CanDeleteGroups Permission = "can_delete_groups"
 
-	CanCreateStories Permission = "can_create_stories"
-	CanReadStories   Permission = "can_read_stories"
-	CanUpdateStories Permission = "can_update_stories"
-	CanDeleteStories Permission = "can_delete_stories"
+	CanCreateStories  Permission = "can_create_stories"
+	CanReadStories    Permission = "can_read_stories"
+	CanUpdateStories  Permission = "can_update_stories"
+	CanDeleteStories  Permission = "can_delete_stories"
 	CanPublishStories Permission = "can_publish_stories"
 )
diff --git a/internal/permissions/users/role.go b/internal/permissions/users/role.go
index 0e0c7c4..c0dccc7 100644
--- a/internal/permissions/users/role.go
+++ b/internal/permissions/users/role.go
@@ -19,5 +19,3 @@ func (p RolePermission) IsAuthorized(r *http.Request) bool {
 	}
 	return groupenums.IsRoleGreaterThan(*role, p.Role)
 }
-
-
diff --git a/internal/router/router.go b/internal/router/router.go
index 9103f30..e6b1d91 100644
--- a/internal/router/router.go
+++ b/internal/router/router.go
@@ -62,7 +62,7 @@ func Setup(config *config.Config, injectMiddleWares []func(http.Handler) http.Ha
 				r.Put("/{storyID}/publish", handleAPIError(stories.HandlePublish))
 				r.Delete("/{storyID}", handleAPIError(stories.HandleDelete))
 				r.Post("/", handleAPIError(stories.HandleCreate))
-			})			
+			})
 
 			r.Route("/users", func(r chi.Router) {
 				r.Get("/", handleAPIError(users.HandleList))
diff --git a/model/stories.go b/model/stories.go
index 0f08f56..c44bb69 100644
--- a/model/stories.go
+++ b/model/stories.go
@@ -9,20 +9,20 @@ import (
 type StoryStatus int
 
 const (
-    Draft StoryStatus = iota
-    Published
+	Draft StoryStatus = iota
+	Published
 )
 
 type Story struct {
-    gorm.Model
-    AuthorID   uint
-    Author     User
-    GroupID    *uint
-    Group      Group
-    Title      string
-    Content    string
-    PinOrder   *int
-    Status     StoryStatus
+	gorm.Model
+	AuthorID uint
+	Author   User
+	GroupID  *uint
+	Group    Group
+	Title    string
+	Content  string
+	PinOrder *int
+	Status   StoryStatus
 }
 
 // Passing nil to omit the filtering and get all stories
diff --git a/model/usergroups.go b/model/usergroups.go
index 66cdb83..d40a9ea 100644
--- a/model/usergroups.go
+++ b/model/usergroups.go
@@ -32,20 +32,19 @@ func GetUserGroupByID(db *gorm.DB, userID uint, groupID uint) (UserGroup, error)
 }
 
 func GetUserRoleByID(db *gorm.DB, userID uint) (groupenums.Role, error) {
-    var userGroup UserGroup
+	var userGroup UserGroup
 
-    err := db.Model(&userGroup).
-        Where(UserGroup{UserID: userID}).
-        First(&userGroup).Error
+	err := db.Model(&userGroup).
+		Where(UserGroup{UserID: userID}).
+		First(&userGroup).Error
 
-    if err != nil {
-        return userGroup.Role, database.HandleDBError(err, "userRole")
-    }
+	if err != nil {
+		return userGroup.Role, database.HandleDBError(err, "userRole")
+	}
 
-    return userGroup.Role, nil
+	return userGroup.Role, nil
 }
 
-
 func CreateUserGroup(db *gorm.DB, userGroup *UserGroup) error {
 	err := db.Create(userGroup).Error
 	if err != nil {
diff --git a/params/stories/moderate.go b/params/stories/moderate.go
index e667c8e..1008724 100644
--- a/params/stories/moderate.go
+++ b/params/stories/moderate.go
@@ -1,22 +1,22 @@
 package storyparams
 
 import (
-    "github.com/source-academy/stories-backend/model"
+	"github.com/source-academy/stories-backend/model"
 )
 
 type Publish struct {
-    IsPublished bool `json:"boolean"`
+	IsPublished bool `json:"boolean"`
 }
 
 // Validate validates the Publish params.
 func (params *Publish) Validate() error {
-    // Validation logic can be added here if needed.
-    return nil
+	// Validation logic can be added here if needed.
+	return nil
 }
 
 // ToModel converts Publish params to a Story model.
 func (params *Publish) ToModel() *model.Story {
-	if (params.IsPublished) {
+	if params.IsPublished {
 		return &model.Story{
 			Status: model.Published,
 		}
@@ -25,4 +25,4 @@ func (params *Publish) ToModel() *model.Story {
 			Status: model.Draft,
 		}
 	}
-}
\ No newline at end of file
+}

From 622bc5a2169b87fc011ac924752b77fd7910edf5 Mon Sep 17 00:00:00 2001
From: keyansheng <65121402+keyansheng@users.noreply.github.com>
Date: Wed, 20 Mar 2024 23:08:22 +0800
Subject: [PATCH 05/17] Restore .env.example

---
 .env.example | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)
 create mode 100644 .env.example

diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..042807b
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,18 @@
+HOST=localhost
+PORT=8080
+
+DB_TIMEZONE=Asia/Singapore
+DB_HOSTNAME=localhost
+DB_PORT=5432
+DB_USERNAME=postgres
+DB_PASSWORD=
+DB_NAME=
+
+SENTRY_DSN=
+
+JWKS_ENDPOINT=
+
+# only for non-development mode
+ALLOWED_ORIGIN_1=
+ALLOWED_ORIGIN_2=
+# ... and so on

From feeb54027a5ce8576ee127a2a2789f7137a72124 Mon Sep 17 00:00:00 2001
From: Kelly Wong <kelly@Kellys-MacBook-Air-2.local>
Date: Wed, 20 Mar 2024 23:09:19 +0800
Subject: [PATCH 06/17] fix formatting

---
 controller/stories/update.go              |  2 +-
 internal/auth/middleware.go               |  6 +++---
 internal/permissions/users/permissions.go |  8 ++++----
 internal/permissions/users/role.go        |  2 --
 internal/router/router.go                 |  2 +-
 model/stories.go                          | 22 +++++++++++-----------
 model/usergroups.go                       | 17 ++++++++---------
 params/stories/moderate.go                | 12 ++++++------
 8 files changed, 34 insertions(+), 37 deletions(-)

diff --git a/controller/stories/update.go b/controller/stories/update.go
index 61dda32..6988f75 100644
--- a/controller/stories/update.go
+++ b/controller/stories/update.go
@@ -146,4 +146,4 @@ func HandlePublish(w http.ResponseWriter, r *http.Request) error {
 
 	controller.EncodeJSONResponse(w, storyviews.SingleFrom(storyModel))
 	return nil
-}
\ No newline at end of file
+}
diff --git a/internal/auth/middleware.go b/internal/auth/middleware.go
index 85e01db..44317d8 100644
--- a/internal/auth/middleware.go
+++ b/internal/auth/middleware.go
@@ -29,10 +29,10 @@ func MakeMiddlewareFrom(conf *config.Config) func(http.Handler) http.Handler {
 	if conf.Environment == envutils.ENV_DEVELOPMENT {
 		return func(next http.Handler) http.Handler {
 			return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-			 r = injectUserIDToContext(r, 1)
-			 next.ServeHTTP(w, r)
+				r = injectUserIDToContext(r, 1)
+				next.ServeHTTP(w, r)
 			})
-		}		 
+		}
 	}
 
 	keySet := getJWKS(conf.JWKSEndpoint)
diff --git a/internal/permissions/users/permissions.go b/internal/permissions/users/permissions.go
index a5673bb..9cb276d 100644
--- a/internal/permissions/users/permissions.go
+++ b/internal/permissions/users/permissions.go
@@ -13,9 +13,9 @@ const (
 	CanUpdateGroups Permission = "can_update_groups"
 	CanDeleteGroups Permission = "can_delete_groups"
 
-	CanCreateStories Permission = "can_create_stories"
-	CanReadStories   Permission = "can_read_stories"
-	CanUpdateStories Permission = "can_update_stories"
-	CanDeleteStories Permission = "can_delete_stories"
+	CanCreateStories  Permission = "can_create_stories"
+	CanReadStories    Permission = "can_read_stories"
+	CanUpdateStories  Permission = "can_update_stories"
+	CanDeleteStories  Permission = "can_delete_stories"
 	CanPublishStories Permission = "can_publish_stories"
 )
diff --git a/internal/permissions/users/role.go b/internal/permissions/users/role.go
index 0e0c7c4..c0dccc7 100644
--- a/internal/permissions/users/role.go
+++ b/internal/permissions/users/role.go
@@ -19,5 +19,3 @@ func (p RolePermission) IsAuthorized(r *http.Request) bool {
 	}
 	return groupenums.IsRoleGreaterThan(*role, p.Role)
 }
-
-
diff --git a/internal/router/router.go b/internal/router/router.go
index 9103f30..e6b1d91 100644
--- a/internal/router/router.go
+++ b/internal/router/router.go
@@ -62,7 +62,7 @@ func Setup(config *config.Config, injectMiddleWares []func(http.Handler) http.Ha
 				r.Put("/{storyID}/publish", handleAPIError(stories.HandlePublish))
 				r.Delete("/{storyID}", handleAPIError(stories.HandleDelete))
 				r.Post("/", handleAPIError(stories.HandleCreate))
-			})			
+			})
 
 			r.Route("/users", func(r chi.Router) {
 				r.Get("/", handleAPIError(users.HandleList))
diff --git a/model/stories.go b/model/stories.go
index 0f08f56..c44bb69 100644
--- a/model/stories.go
+++ b/model/stories.go
@@ -9,20 +9,20 @@ import (
 type StoryStatus int
 
 const (
-    Draft StoryStatus = iota
-    Published
+	Draft StoryStatus = iota
+	Published
 )
 
 type Story struct {
-    gorm.Model
-    AuthorID   uint
-    Author     User
-    GroupID    *uint
-    Group      Group
-    Title      string
-    Content    string
-    PinOrder   *int
-    Status     StoryStatus
+	gorm.Model
+	AuthorID uint
+	Author   User
+	GroupID  *uint
+	Group    Group
+	Title    string
+	Content  string
+	PinOrder *int
+	Status   StoryStatus
 }
 
 // Passing nil to omit the filtering and get all stories
diff --git a/model/usergroups.go b/model/usergroups.go
index 66cdb83..d40a9ea 100644
--- a/model/usergroups.go
+++ b/model/usergroups.go
@@ -32,20 +32,19 @@ func GetUserGroupByID(db *gorm.DB, userID uint, groupID uint) (UserGroup, error)
 }
 
 func GetUserRoleByID(db *gorm.DB, userID uint) (groupenums.Role, error) {
-    var userGroup UserGroup
+	var userGroup UserGroup
 
-    err := db.Model(&userGroup).
-        Where(UserGroup{UserID: userID}).
-        First(&userGroup).Error
+	err := db.Model(&userGroup).
+		Where(UserGroup{UserID: userID}).
+		First(&userGroup).Error
 
-    if err != nil {
-        return userGroup.Role, database.HandleDBError(err, "userRole")
-    }
+	if err != nil {
+		return userGroup.Role, database.HandleDBError(err, "userRole")
+	}
 
-    return userGroup.Role, nil
+	return userGroup.Role, nil
 }
 
-
 func CreateUserGroup(db *gorm.DB, userGroup *UserGroup) error {
 	err := db.Create(userGroup).Error
 	if err != nil {
diff --git a/params/stories/moderate.go b/params/stories/moderate.go
index e667c8e..1008724 100644
--- a/params/stories/moderate.go
+++ b/params/stories/moderate.go
@@ -1,22 +1,22 @@
 package storyparams
 
 import (
-    "github.com/source-academy/stories-backend/model"
+	"github.com/source-academy/stories-backend/model"
 )
 
 type Publish struct {
-    IsPublished bool `json:"boolean"`
+	IsPublished bool `json:"boolean"`
 }
 
 // Validate validates the Publish params.
 func (params *Publish) Validate() error {
-    // Validation logic can be added here if needed.
-    return nil
+	// Validation logic can be added here if needed.
+	return nil
 }
 
 // ToModel converts Publish params to a Story model.
 func (params *Publish) ToModel() *model.Story {
-	if (params.IsPublished) {
+	if params.IsPublished {
 		return &model.Story{
 			Status: model.Published,
 		}
@@ -25,4 +25,4 @@ func (params *Publish) ToModel() *model.Story {
 			Status: model.Draft,
 		}
 	}
-}
\ No newline at end of file
+}

From 43ab0dda3aae829bf9de3c1da44ea6f83c82c1fa Mon Sep 17 00:00:00 2001
From: Kelly Wong <kelly@Kellys-MacBook-Air-2.local>
Date: Wed, 20 Mar 2024 23:26:09 +0800
Subject: [PATCH 07/17] fix formatting

---
 go.sum | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/go.sum b/go.sum
index e87f6f9..a63ae4c 100644
--- a/go.sum
+++ b/go.sum
@@ -45,8 +45,11 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
 github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
 github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw=
 github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
@@ -73,12 +76,14 @@ github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6
 github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
 github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
 github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY=
 github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg=
+github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
 github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
 github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
 github.com/rubenv/sql-migrate v1.5.1 h1:WsZo4jPQfjmddDTh/suANP2aKPA7/ekN0LzuuajgQEo=
@@ -99,6 +104,7 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
 golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
 golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -108,11 +114,15 @@ golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
 golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
 golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
+golang.org/x/tools/gopls v0.14.2/go.mod h1:o2s+suLlFye+MHxEofsxJPkBQ/kT/ucP4iJV3ohjGEQ=
+golang.org/x/vuln v1.0.1/go.mod h1:bb2hMwln/tqxg32BNY4CcxHWtHXuYa3SbIBmtsyjxtM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -120,3 +130,6 @@ gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
 gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
 gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
 gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+honnef.co/go/tools v0.4.5/go.mod h1:GUV+uIBCLpdf0/v6UhHHG/yzI/z6qPskBeQCjcNB96k=
+mvdan.cc/gofumpt v0.4.0/go.mod h1:PljLOHDeZqgS8opHRKLzp2It2VBuSdteAgqUfzMTxlQ=
+mvdan.cc/xurls/v2 v2.4.0/go.mod h1:+GEjq9uNjqs8LQfM9nVnM8rff0OQ5Iash5rzX+N1CSg=

From fbd5342912fab705f1540860b28c3316b335ae3c Mon Sep 17 00:00:00 2001
From: keyansheng <65121402+keyansheng@users.noreply.github.com>
Date: Wed, 27 Mar 2024 00:13:19 +0800
Subject: [PATCH 08/17] Run go get

---
 go.sum | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/go.sum b/go.sum
index e87f6f9..a63ae4c 100644
--- a/go.sum
+++ b/go.sum
@@ -45,8 +45,11 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
 github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
 github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw=
 github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
@@ -73,12 +76,14 @@ github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6
 github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
 github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
 github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY=
 github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg=
+github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
 github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
 github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
 github.com/rubenv/sql-migrate v1.5.1 h1:WsZo4jPQfjmddDTh/suANP2aKPA7/ekN0LzuuajgQEo=
@@ -99,6 +104,7 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
 golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
 golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -108,11 +114,15 @@ golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
 golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
 golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
+golang.org/x/tools/gopls v0.14.2/go.mod h1:o2s+suLlFye+MHxEofsxJPkBQ/kT/ucP4iJV3ohjGEQ=
+golang.org/x/vuln v1.0.1/go.mod h1:bb2hMwln/tqxg32BNY4CcxHWtHXuYa3SbIBmtsyjxtM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -120,3 +130,6 @@ gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
 gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
 gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
 gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+honnef.co/go/tools v0.4.5/go.mod h1:GUV+uIBCLpdf0/v6UhHHG/yzI/z6qPskBeQCjcNB96k=
+mvdan.cc/gofumpt v0.4.0/go.mod h1:PljLOHDeZqgS8opHRKLzp2It2VBuSdteAgqUfzMTxlQ=
+mvdan.cc/xurls/v2 v2.4.0/go.mod h1:+GEjq9uNjqs8LQfM9nVnM8rff0OQ5Iash5rzX+N1CSg=

From 2efef3ffff5193863aa5d7b549e39ac76e7ccb62 Mon Sep 17 00:00:00 2001
From: keyansheng <65121402+keyansheng@users.noreply.github.com>
Date: Wed, 27 Mar 2024 00:32:02 +0800
Subject: [PATCH 09/17] Fix migration ordering

---
 ...0230807190030-add_status.sql => 20240320000000-add_status.sql} | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename migrations/{20230807190030-add_status.sql => 20240320000000-add_status.sql} (100%)

diff --git a/migrations/20230807190030-add_status.sql b/migrations/20240320000000-add_status.sql
similarity index 100%
rename from migrations/20230807190030-add_status.sql
rename to migrations/20240320000000-add_status.sql

From ef9b9317df2155a6d313ed6f6b4417c5eb6fad4f Mon Sep 17 00:00:00 2001
From: Kelly Wong <kelly@Kellys-MacBook-Air-2.local>
Date: Wed, 27 Mar 2024 01:58:12 +0800
Subject: [PATCH 10/17] moderation draft 1

---
 controller/stories/stories.go                | 83 ++++++++++++++++++--
 controller/stories/update.go                 | 62 ---------------
 internal/permissiongroups/stories/stories.go |  4 +-
 internal/permissions/users/permissions.go    | 10 +--
 internal/permissions/users/users.go          |  2 +-
 internal/router/router.go                    |  3 +-
 migrations/20230807190031-add_status.sql     |  9 +++
 model/stories.go                             | 75 +++++++++++++++---
 params/stories/moderate.go                   | 28 -------
 params/stories/update.go                     | 16 ++--
 10 files changed, 170 insertions(+), 122 deletions(-)
 create mode 100644 migrations/20230807190031-add_status.sql
 delete mode 100644 params/stories/moderate.go

diff --git a/controller/stories/stories.go b/controller/stories/stories.go
index 4453614..0e0965b 100644
--- a/controller/stories/stories.go
+++ b/controller/stories/stories.go
@@ -4,7 +4,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"net/http"
-
 	"strconv"
 
 	"github.com/go-chi/chi/v5"
@@ -59,7 +58,40 @@ func HandleListPublished(w http.ResponseWriter, r *http.Request) error {
 	if err != nil {
 		logrus.Error(err)
 		return apierrors.ClientForbiddenError{
-			Message: fmt.Sprintf("Error listing stories: %v", err),
+			Message: fmt.Sprintf("Error listing published stories: %v", err),
+		}
+	}
+
+	// Get DB instance
+	db, err := database.GetDBFrom(r)
+	if err != nil {
+		logrus.Error(err)
+		return err
+	}
+
+	// Get group id from context
+	groupID, err := usergroups.GetGroupIDFrom(r)
+	if err != nil {
+		logrus.Error(err)
+		return err
+	}
+
+	stories, err := model.GetAllStoriesByStatus(db, groupID, model.Published)
+	if err != nil {
+		logrus.Error(err)
+		return err
+	}
+
+	controller.EncodeJSONResponse(w, storyviews.ListFrom(stories))
+	return nil
+}
+
+func HandleListPending(w http.ResponseWriter, r *http.Request) error {
+	err := auth.CheckPermissions(r, storypermissiongroups.Moderate())
+	if err != nil {
+		logrus.Error(err)
+		return apierrors.ClientForbiddenError{
+			Message: fmt.Sprintf("Error listing stories to be reviewed: %v", err),
 		}
 	}
 
@@ -77,7 +109,7 @@ func HandleListPublished(w http.ResponseWriter, r *http.Request) error {
 		return err
 	}
 
-	stories, err := model.GetAllPublishedStories(db, groupID)
+	stories, err := model.GetAllStoriesByStatus(db, groupID, model.Pending)
 	if err != nil {
 		logrus.Error(err)
 		return err
@@ -92,10 +124,51 @@ func HandleListDraft(w http.ResponseWriter, r *http.Request) error {
 	if err != nil {
 		logrus.Error(err)
 		return apierrors.ClientForbiddenError{
-			Message: fmt.Sprintf("Error listing stories: %v", err),
+			Message: fmt.Sprintf("Error listing drafts: %v", err),
 		}
 	}
+	userID, err := auth.GetUserIDFrom(r)
+	if err != nil {
+		logrus.Error(err)
+		return err
+	}
+	// Get DB instance
+	db, err := database.GetDBFrom(r)
+	if err != nil {
+		logrus.Error(err)
+		return err
+	}
+
+	// Get group id from context
+	groupID, err := usergroups.GetGroupIDFrom(r)
+	if err != nil {
+		logrus.Error(err)
+		return err
+	}
+
+	stories, err := model.GetAllAuthorStoriesByStatus(db, groupID, userID, model.Draft)
+	if err != nil {
+		logrus.Error(err)
+		return err
+	}
 
+	controller.EncodeJSONResponse(w, storyviews.ListFrom(stories))
+	return nil
+}
+
+func HandleListRejected(w http.ResponseWriter, r *http.Request) error {
+	err := auth.CheckPermissions(r, storypermissiongroups.List())
+	if err != nil {
+		logrus.Error(err)
+		return apierrors.ClientForbiddenError{
+			Message: fmt.Sprintf("Error listing rejected stories: %v", err),
+		}
+	}
+	userID, err := auth.GetUserIDFrom(r)
+	if err != nil {
+		logrus.Error(err)
+		return err
+	}
 	// Get DB instance
 	db, err := database.GetDBFrom(r)
 	if err != nil {
@@ -110,7 +183,7 @@ func HandleListDraft(w http.ResponseWriter, r *http.Request) error {
 		return err
 	}
 
-	stories, err := model.GetAllDraftStories(db, groupID)
+	stories, err := model.GetAllAuthorStoriesByStatus(db, groupID, userID, model.Rejected)
 	if err != nil {
 		logrus.Error(err)
 		return err
diff --git a/controller/stories/update.go b/controller/stories/update.go
index 6988f75..3079e67 100644
--- a/controller/stories/update.go
+++ b/controller/stories/update.go
@@ -85,65 +85,3 @@ func HandleUpdate(w http.ResponseWriter, r *http.Request) error {
 	controller.EncodeJSONResponse(w, storyviews.SingleFrom(storyModel))
 	return nil
 }
-
-func HandlePublish(w http.ResponseWriter, r *http.Request) error {
-	storyIDStr := chi.URLParam(r, "storyID")
-	storyID, err := strconv.Atoi(storyIDStr)
-	if err != nil {
-		return apierrors.ClientBadRequestError{
-			Message: fmt.Sprintf("Invalid storyID: %v", err),
-		}
-	}
-
-	err = auth.CheckPermissions(r, storypermissiongroups.Publish())
-	if err != nil {
-		logrus.Error(err)
-		return apierrors.ClientForbiddenError{
-			Message: fmt.Sprintf("Error moderating story: %v", err),
-		}
-	}
-
-	// Extra params won't do anything, e.g. authorID can't be changed.
-	// TODO: Error on extra params?
-	var params storyparams.Update
-	if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
-		e, ok := err.(*json.UnmarshalTypeError)
-		if !ok {
-			logrus.Error(err)
-			return apierrors.ClientBadRequestError{
-				Message: fmt.Sprintf("Bad JSON parsing: %v", err),
-			}
-		}
-
-		// TODO: Investigate if we should use errors.Wrap instead
-		return apierrors.ClientUnprocessableEntityError{
-			Message: fmt.Sprintf("Invalid JSON format: %s should be a %s.", e.Field, e.Type),
-		}
-	}
-
-	err = params.Validate()
-	if err != nil {
-		logrus.Error(err)
-		return apierrors.ClientUnprocessableEntityError{
-			Message: fmt.Sprintf("JSON validation failed: %v", err),
-		}
-	}
-
-	storyModel := *params.ToModel()
-
-	// Get DB instance
-	db, err := database.GetDBFrom(r)
-	if err != nil {
-		logrus.Error(err)
-		return err
-	}
-
-	err = model.UpdateStory(db, storyID, &storyModel)
-	if err != nil {
-		logrus.Error(err)
-		return err
-	}
-
-	controller.EncodeJSONResponse(w, storyviews.SingleFrom(storyModel))
-	return nil
-}
diff --git a/internal/permissiongroups/stories/stories.go b/internal/permissiongroups/stories/stories.go
index 5d38811..039f753 100644
--- a/internal/permissiongroups/stories/stories.go
+++ b/internal/permissiongroups/stories/stories.go
@@ -20,9 +20,9 @@ func Read() permissions.PermissionGroup {
 		GetRolePermission(userpermissions.CanReadStories)
 }
 
-func Publish() permissions.PermissionGroup {
+func Moderate() permissions.PermissionGroup {
 	return userpermissions.
-		GetRolePermission(userpermissions.CanPublishStories)
+		GetRolePermission(userpermissions.CanModerateStories)
 }
 
 func Update(storyID uint) permissions.PermissionGroup {
diff --git a/internal/permissions/users/permissions.go b/internal/permissions/users/permissions.go
index 9cb276d..0aa325b 100644
--- a/internal/permissions/users/permissions.go
+++ b/internal/permissions/users/permissions.go
@@ -13,9 +13,9 @@ const (
 	CanUpdateGroups Permission = "can_update_groups"
 	CanDeleteGroups Permission = "can_delete_groups"
 
-	CanCreateStories  Permission = "can_create_stories"
-	CanReadStories    Permission = "can_read_stories"
-	CanUpdateStories  Permission = "can_update_stories"
-	CanDeleteStories  Permission = "can_delete_stories"
-	CanPublishStories Permission = "can_publish_stories"
+	CanCreateStories   Permission = "can_create_stories"
+	CanReadStories     Permission = "can_read_stories"
+	CanUpdateStories   Permission = "can_update_stories"
+	CanDeleteStories   Permission = "can_delete_stories"
+	CanModerateStories Permission = "can_moderate_stories"
 )
diff --git a/internal/permissions/users/users.go b/internal/permissions/users/users.go
index d4c4992..e330ced 100644
--- a/internal/permissions/users/users.go
+++ b/internal/permissions/users/users.go
@@ -23,7 +23,7 @@ func GetRolePermission(p Permission) *RolePermission {
 	case
 		// Additional permissions for moderators and administrators
 		CanUpdateStories,
-		CanPublishStories,
+		CanModerateStories,
 		CanDeleteStories:
 		return &RolePermission{
 			Permission: p,
diff --git a/internal/router/router.go b/internal/router/router.go
index e6b1d91..d86a904 100644
--- a/internal/router/router.go
+++ b/internal/router/router.go
@@ -56,10 +56,11 @@ func Setup(config *config.Config, injectMiddleWares []func(http.Handler) http.Ha
 			r.Route("/stories", func(r chi.Router) {
 				r.Get("/", handleAPIError(stories.HandleList))
 				r.Get("/draft", handleAPIError(stories.HandleListDraft))
+				r.Get("/pending", handleAPIError(stories.HandleListPending))
 				r.Get("/published", handleAPIError(stories.HandleListPublished))
+				r.Get("/rejected", handleAPIError(stories.HandleListPublished))
 				r.Get("/{storyID}", handleAPIError(stories.HandleRead))
 				r.Put("/{storyID}", handleAPIError(stories.HandleUpdate))
-				r.Put("/{storyID}/publish", handleAPIError(stories.HandlePublish))
 				r.Delete("/{storyID}", handleAPIError(stories.HandleDelete))
 				r.Post("/", handleAPIError(stories.HandleCreate))
 			})
diff --git a/migrations/20230807190031-add_status.sql b/migrations/20230807190031-add_status.sql
new file mode 100644
index 0000000..821694e
--- /dev/null
+++ b/migrations/20230807190031-add_status.sql
@@ -0,0 +1,9 @@
+-- +migrate Up
+
+ALTER TABLE stories
+    ADD COLUMN status_message TEXT;
+
+-- +migrate Down
+
+ALTER TABLE stories
+    DROP COLUMN status_message;
\ No newline at end of file
diff --git a/model/stories.go b/model/stories.go
index c44bb69..fcb4933 100644
--- a/model/stories.go
+++ b/model/stories.go
@@ -2,6 +2,7 @@ package model
 
 import (
 	"github.com/source-academy/stories-backend/internal/database"
+	groupenums "github.com/source-academy/stories-backend/internal/enums/groups"
 	"gorm.io/gorm"
 	"gorm.io/gorm/clause"
 )
@@ -10,19 +11,22 @@ type StoryStatus int
 
 const (
 	Draft StoryStatus = iota
+	Pending
+	Rejected
 	Published
 )
 
 type Story struct {
 	gorm.Model
-	AuthorID uint
-	Author   User
-	GroupID  *uint
-	Group    Group
-	Title    string
-	Content  string
-	PinOrder *int
-	Status   StoryStatus
+	AuthorID      uint
+	Author        User
+	GroupID       *uint
+	Group         Group
+	Title         string
+	Content       string
+	PinOrder      *int
+	Status        StoryStatus
+	StatusMessage *string
 }
 
 // Passing nil to omit the filtering and get all stories
@@ -59,10 +63,10 @@ func GetAllPublishedStories(db *gorm.DB, groupID *uint) ([]Story, error) {
 	return stories, nil
 }
 
-func GetAllDraftStories(db *gorm.DB, groupID *uint) ([]Story, error) {
+func GetAllPendingStories(db *gorm.DB, groupID *uint) ([]Story, error) {
 	var stories []Story
 	err := db.
-		Where("status = ?", int(Draft)).
+		Where("status = ?", int(Pending)).
 		Where("group_id = ?", groupID).
 		Preload(clause.Associations).
 		// TODO: Abstract out the sorting logic
@@ -75,6 +79,39 @@ func GetAllDraftStories(db *gorm.DB, groupID *uint) ([]Story, error) {
 	return stories, nil
 }
 
+func GetAllStoriesByStatus(db *gorm.DB, groupID *uint, status StoryStatus) ([]Story, error) {
+	var stories []Story
+	err := db.
+		Where("status = ?", int(status)).
+		Where("group_id = ?", groupID).
+		Preload(clause.Associations).
+		// TODO: Abstract out the sorting logic
+		Order("pin_order ASC NULLS LAST, title ASC, content ASC").
+		Find(&stories).
+		Error
+	if err != nil {
+		return stories, database.HandleDBError(err, "story")
+	}
+	return stories, nil
+}
+
+func GetAllAuthorStoriesByStatus(db *gorm.DB, groupID *uint, userID *int, status StoryStatus) ([]Story, error) {
+	var stories []Story
+	err := db.
+		Where("status = ?", int(status)).
+		Where("group_id = ?", groupID).
+		Where("author_id = ?", userID).
+		Preload(clause.Associations).
+		// TODO: Abstract out the sorting logic
+		Order("pin_order ASC NULLS LAST, title ASC, content ASC").
+		Find(&stories).
+		Error
+	if err != nil {
+		return stories, database.HandleDBError(err, "story")
+	}
+	return stories, nil
+}
+
 func GetStoryByID(db *gorm.DB, id int) (Story, error) {
 	var story Story
 	err := db.
@@ -98,8 +135,22 @@ func (s *Story) create(tx *gorm.DB) *gorm.DB {
 }
 
 func CreateStory(db *gorm.DB, story *Story) error {
-	err := db.Transaction(func(tx *gorm.DB) error {
-		return story.create(tx).Error
+	// Check author's role
+	role, err := GetUserRoleByID(db, story.AuthorID)
+	if err != nil {
+		return database.HandleDBError(err, "userRole")
+	}
+	// Set story status based on author's role
+	if role == groupenums.RoleStandard {
+		story.Status = Draft
+	} else {
+		story.Status = Published
+	}
+	err = db.Transaction(func(tx *gorm.DB) error {
+		if err := tx.Create(story).Error; err != nil {
+			return err // Return the error directly
+		}
+		return nil
 	})
 	if err != nil {
 		return database.HandleDBError(err, "story")
diff --git a/params/stories/moderate.go b/params/stories/moderate.go
deleted file mode 100644
index 1008724..0000000
--- a/params/stories/moderate.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package storyparams
-
-import (
-	"github.com/source-academy/stories-backend/model"
-)
-
-type Publish struct {
-	IsPublished bool `json:"boolean"`
-}
-
-// Validate validates the Publish params.
-func (params *Publish) Validate() error {
-	// Validation logic can be added here if needed.
-	return nil
-}
-
-// ToModel converts Publish params to a Story model.
-func (params *Publish) ToModel() *model.Story {
-	if params.IsPublished {
-		return &model.Story{
-			Status: model.Published,
-		}
-	} else {
-		return &model.Story{
-			Status: model.Draft,
-		}
-	}
-}
diff --git a/params/stories/update.go b/params/stories/update.go
index 8eb14fe..be49751 100644
--- a/params/stories/update.go
+++ b/params/stories/update.go
@@ -5,9 +5,11 @@ import (
 )
 
 type Update struct {
-	Title    string `json:"title"`
-	Content  string `json:"content"`
-	PinOrder *int   `json:"pinOrder"`
+	Title         string `json:"title"`
+	Content       string `json:"content"`
+	PinOrder      *int   `json:"pinOrder"`
+	Status        int    `json:"status"`
+	StatusMessage string `json:"statusMessage"`
 }
 
 func (params *Update) Validate() error {
@@ -18,8 +20,10 @@ func (params *Update) Validate() error {
 
 func (params *Update) ToModel() *model.Story {
 	return &model.Story{
-		Title:    params.Title,
-		Content:  params.Content,
-		PinOrder: params.PinOrder,
+		Title:         params.Title,
+		Content:       params.Content,
+		PinOrder:      params.PinOrder,
+		Status:        model.StoryStatus(params.Status),
+		StatusMessage: &params.StatusMessage,
 	}
 }

From 0e3491eb7bad48401b900457a2387468b08598b0 Mon Sep 17 00:00:00 2001
From: Kelly Wong <kelly@Kellys-MacBook-Air-2.local>
Date: Wed, 27 Mar 2024 02:27:59 +0800
Subject: [PATCH 11/17] add validation for create story, added unit test for it

---
 params/stories/create.go      | 13 +++++++++++++
 params/stories/create_test.go | 22 +++++++++++++++++++++-
 2 files changed, 34 insertions(+), 1 deletion(-)

diff --git a/params/stories/create.go b/params/stories/create.go
index 3e8bf80..cc101d5 100644
--- a/params/stories/create.go
+++ b/params/stories/create.go
@@ -1,6 +1,7 @@
 package storyparams
 
 import (
+	"fmt"
 	"github.com/source-academy/stories-backend/model"
 )
 
@@ -13,6 +14,18 @@ type Create struct {
 
 // TODO: Add some validation
 func (params *Create) Validate() error {
+	if params.AuthorID == 0 {
+		return fmt.Errorf("authorId is required and must be non-zero")
+	}
+	if params.Title == "" {
+		return fmt.Errorf("title is required and cannot be empty")
+	}
+	if params.Content == "" {
+		return fmt.Errorf("content is required and cannot be empty")
+	}
+	if params.PinOrder != nil && *params.PinOrder < 0 {
+		return fmt.Errorf("pinOrder, if set, must be non-negative")
+	}
 	return nil
 }
 
diff --git a/params/stories/create_test.go b/params/stories/create_test.go
index f100c4d..8975470 100644
--- a/params/stories/create_test.go
+++ b/params/stories/create_test.go
@@ -7,7 +7,27 @@ import (
 )
 
 func TestValidate(t *testing.T) {
-	t.Run("should do nothing for now", func(t *testing.T) {})
+	negativePinOrder := -1
+	tests := []struct {
+		name    string
+		params  Create
+		wantErr bool
+	}{
+		{"valid input", Create{AuthorID: 1, Title: "Test Title", Content: "Test Content"}, false},
+		{"missing authorId", Create{Title: "Test Title", Content: "Test Content"}, true},
+		{"empty title", Create{AuthorID: 1, Content: "Test Content"}, true},
+		{"empty content", Create{AuthorID: 1, Title: "Test Title"}, true},
+		{"negative pinOrder", Create{AuthorID: 1, Title: "Test Title", Content: "Test Content", PinOrder: &negativePinOrder}, true},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := tt.params.Validate()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
 }
 
 func TestToModel(t *testing.T) {

From e54b1ecac7b84a1700914cc1cfcd64f803707808 Mon Sep 17 00:00:00 2001
From: keyansheng <65121402+keyansheng@users.noreply.github.com>
Date: Wed, 27 Mar 2024 04:13:05 +0800
Subject: [PATCH 12/17] Fix TestCreateStory/can_create_without_group fail

---
 model/stories.go | 11 +++++------
 1 file changed, 5 insertions(+), 6 deletions(-)

diff --git a/model/stories.go b/model/stories.go
index fcb4933..f192d38 100644
--- a/model/stories.go
+++ b/model/stories.go
@@ -136,17 +136,16 @@ func (s *Story) create(tx *gorm.DB) *gorm.DB {
 
 func CreateStory(db *gorm.DB, story *Story) error {
 	// Check author's role
-	role, err := GetUserRoleByID(db, story.AuthorID)
-	if err != nil {
-		return database.HandleDBError(err, "userRole")
-	}
+	role, _ := GetUserRoleByID(db, story.AuthorID)
+	// Based on the TestCreateStory, "can create without group" seems to be the desired behaviour
+	// No group means no userGroup, which means no role, so an error shouldn't be thrown
 	// Set story status based on author's role
-	if role == groupenums.RoleStandard {
+	if !groupenums.IsRoleGreaterThan(role, groupenums.RoleStandard) {
 		story.Status = Draft
 	} else {
 		story.Status = Published
 	}
-	err = db.Transaction(func(tx *gorm.DB) error {
+	err := db.Transaction(func(tx *gorm.DB) error {
 		if err := tx.Create(story).Error; err != nil {
 			return err // Return the error directly
 		}

From 0b49dd967d3608c6d7d21ef8a4973c9c6198f837 Mon Sep 17 00:00:00 2001
From: keyansheng <65121402+keyansheng@users.noreply.github.com>
Date: Wed, 27 Mar 2024 04:22:33 +0800
Subject: [PATCH 13/17] Fix migration ordering

---
 ...0230807190031-add_status.sql => 20240320000001-add_status.sql} | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename migrations/{20230807190031-add_status.sql => 20240320000001-add_status.sql} (100%)

diff --git a/migrations/20230807190031-add_status.sql b/migrations/20240320000001-add_status.sql
similarity index 100%
rename from migrations/20230807190031-add_status.sql
rename to migrations/20240320000001-add_status.sql

From 13f32a4291559136163f2517aa6b5051498afcfb Mon Sep 17 00:00:00 2001
From: keyansheng <65121402+keyansheng@users.noreply.github.com>
Date: Wed, 27 Mar 2024 04:25:14 +0800
Subject: [PATCH 14/17] Fix migration name

---
 ...00001-add_status.sql => 20240320000001-add_status_message.sql} | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename migrations/{20240320000001-add_status.sql => 20240320000001-add_status_message.sql} (100%)

diff --git a/migrations/20240320000001-add_status.sql b/migrations/20240320000001-add_status_message.sql
similarity index 100%
rename from migrations/20240320000001-add_status.sql
rename to migrations/20240320000001-add_status_message.sql

From 7689df9e74a112e27efd67c3e89e0791c1d434e9 Mon Sep 17 00:00:00 2001
From: keyansheng <65121402+keyansheng@users.noreply.github.com>
Date: Wed, 27 Mar 2024 08:55:55 +0800
Subject: [PATCH 15/17] Revert random added and deleted newlines and comments

---
 controller/stories/stories.go | 1 +
 model/stories.go              | 6 +++---
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/controller/stories/stories.go b/controller/stories/stories.go
index 0e0965b..0d33768 100644
--- a/controller/stories/stories.go
+++ b/controller/stories/stories.go
@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"net/http"
+
 	"strconv"
 
 	"github.com/go-chi/chi/v5"
diff --git a/model/stories.go b/model/stories.go
index f192d38..bbeee82 100644
--- a/model/stories.go
+++ b/model/stories.go
@@ -20,11 +20,11 @@ type Story struct {
 	gorm.Model
 	AuthorID      uint
 	Author        User
-	GroupID       *uint
+	GroupID       *uint // null means this is a public story
 	Group         Group
 	Title         string
 	Content       string
-	PinOrder      *int
+	PinOrder      *int // nil if not pinned
 	Status        StoryStatus
 	StatusMessage *string
 }
@@ -154,7 +154,6 @@ func CreateStory(db *gorm.DB, story *Story) error {
 	if err != nil {
 		return database.HandleDBError(err, "story")
 	}
-
 	return nil
 }
 
@@ -184,6 +183,7 @@ func UpdateStory(db *gorm.DB, storyID int, newStory *Story) error {
 					return database.HandleDBError(err, "story")
 				}
 			}
+
 			// Update remaining fields
 			err = tx.
 				Preload(clause.Associations).

From a1f84d02de833b94a91b2a32fd5c41b40215a8c3 Mon Sep 17 00:00:00 2001
From: keyansheng <65121402+keyansheng@users.noreply.github.com>
Date: Fri, 12 Apr 2024 10:43:43 +0800
Subject: [PATCH 16/17] Fix rejected route handler

---
 internal/router/router.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/internal/router/router.go b/internal/router/router.go
index d86a904..5e9071d 100644
--- a/internal/router/router.go
+++ b/internal/router/router.go
@@ -58,7 +58,7 @@ func Setup(config *config.Config, injectMiddleWares []func(http.Handler) http.Ha
 				r.Get("/draft", handleAPIError(stories.HandleListDraft))
 				r.Get("/pending", handleAPIError(stories.HandleListPending))
 				r.Get("/published", handleAPIError(stories.HandleListPublished))
-				r.Get("/rejected", handleAPIError(stories.HandleListPublished))
+				r.Get("/rejected", handleAPIError(stories.HandleListRejected))
 				r.Get("/{storyID}", handleAPIError(stories.HandleRead))
 				r.Put("/{storyID}", handleAPIError(stories.HandleUpdate))
 				r.Delete("/{storyID}", handleAPIError(stories.HandleDelete))

From 54d9a210b7a987cf15576afe15f398870d24dcdc Mon Sep 17 00:00:00 2001
From: keyansheng <65121402+keyansheng@users.noreply.github.com>
Date: Fri, 12 Apr 2024 08:52:32 +0800
Subject: [PATCH 17/17] Run go mod tidy

---
 go.mod |  8 --------
 go.sum | 20 --------------------
 2 files changed, 28 deletions(-)

diff --git a/go.mod b/go.mod
index 3f2174f..df43dc1 100644
--- a/go.mod
+++ b/go.mod
@@ -18,13 +18,11 @@ require (
 )
 
 require (
-	github.com/BurntSushi/toml v1.2.1 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
 	github.com/go-gorp/gorp/v3 v3.1.0 // indirect
 	github.com/go-sql-driver/mysql v1.7.1 // indirect
 	github.com/goccy/go-json v0.10.2 // indirect
-	github.com/google/go-cmp v0.5.9 // indirect
 	github.com/jackc/pgpassfile v1.0.0 // indirect
 	github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
 	github.com/jackc/puddle/v2 v2.2.1 // indirect
@@ -44,11 +42,5 @@ require (
 	golang.org/x/sync v0.6.0 // indirect
 	golang.org/x/sys v0.18.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
-	golang.org/x/tools v0.17.0 // indirect
-	golang.org/x/tools/gopls v0.14.2 // indirect
-	golang.org/x/vuln v1.0.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	honnef.co/go/tools v0.4.5 // indirect
-	mvdan.cc/gofumpt v0.4.0 // indirect
-	mvdan.cc/xurls/v2 v2.4.0 // indirect
 )
diff --git a/go.sum b/go.sum
index a63ae4c..308f708 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,3 @@
-github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
-github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
 github.com/TwiN/go-color v1.4.0 h1:fNbOwOrvup5oj934UragnW0B1WKaAkkB85q19Y7h4ng=
 github.com/TwiN/go-color v1.4.0/go.mod h1:0QTVEPlu+AoCyTrho7bXbVkrCkVpdQr7YF7PYWEtSxM=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -45,11 +43,8 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
 github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
 github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw=
 github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
@@ -76,27 +71,22 @@ github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6
 github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
 github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
 github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
-github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY=
 github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg=
-github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
 github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
 github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
 github.com/rubenv/sql-migrate v1.5.1 h1:WsZo4jPQfjmddDTh/suANP2aKPA7/ekN0LzuuajgQEo=
 github.com/rubenv/sql-migrate v1.5.1/go.mod h1:H38GW8Vqf8F0Su5XignRyaRcbXbJunSWxs+kmzlg0Is=
 github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
 github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
-github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
-github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
 github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
 github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -104,7 +94,6 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
 golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
 golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -114,15 +103,9 @@ golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
 golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
 golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
-golang.org/x/tools/gopls v0.14.2/go.mod h1:o2s+suLlFye+MHxEofsxJPkBQ/kT/ucP4iJV3ohjGEQ=
-golang.org/x/vuln v1.0.1/go.mod h1:bb2hMwln/tqxg32BNY4CcxHWtHXuYa3SbIBmtsyjxtM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -130,6 +113,3 @@ gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
 gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
 gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
 gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
-honnef.co/go/tools v0.4.5/go.mod h1:GUV+uIBCLpdf0/v6UhHHG/yzI/z6qPskBeQCjcNB96k=
-mvdan.cc/gofumpt v0.4.0/go.mod h1:PljLOHDeZqgS8opHRKLzp2It2VBuSdteAgqUfzMTxlQ=
-mvdan.cc/xurls/v2 v2.4.0/go.mod h1:+GEjq9uNjqs8LQfM9nVnM8rff0OQ5Iash5rzX+N1CSg=