Skip to content
This repository has been archived by the owner on Aug 11, 2023. It is now read-only.

feat: Forums CRUD #27

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
3 changes: 3 additions & 0 deletions internal/entity/entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,8 @@ type GroupEdges = ent.GroupEdges
type GroupUser = ent.GroupUser
type GroupUserEdges = ent.GroupUserEdges

type Forum = ent.Forum
type ForumPost = ent.ForumPost

type InstitutionInviteLink = ent.InstitutionInviteLink
type InstitutionInviteLinkEdges = ent.InstitutionInviteLinkEdges
55 changes: 55 additions & 0 deletions internal/entity/forum/forum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package forum

import "errors"

type Role string

func (Role) Values() (kinds []string) {
for _, s := range Roles {
kinds = append(kinds, string(s))
}
return
}

func (fr Role) Validate() error {
switch fr {
case RoleOwner, RoleEducator, RoleMember:
return nil
default:
return errors.New("group role is not valid")
}
}

var (
Roles = []Role{RoleOwner, RoleEducator, RoleMember}

RoleOwner Role = "owner"
RoleEducator Role = "educator"
RoleMember Role = "member"
)
tan-yun-e marked this conversation as resolved.
Show resolved Hide resolved

type Options struct {
Name string
ShortName string
Description string
}

type Option func(*Options)

func Name(n string) Option {
return func(opts *Options) {
opts.Name = n
}
}

func ShortName(n string) Option {
return func(opts *Options) {
opts.ShortName = n
}
}

func Description(d string) Option {
return func(opts *Options) {
opts.Description = d
}
}
48 changes: 48 additions & 0 deletions internal/entity/forumpost/forumpost.go
Copy link
Contributor Author

@tan-yun-e tan-yun-e Jul 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WIP did not mean to stage files on forumpost

Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package forumpost

import "errors"

type Role string

func (Role) Values() (kinds []string) {
for _, s := range Roles {
kinds = append(kinds, string(s))
}
return
}

func (fr Role) Validate() error {
switch fr {
case RoleOwner, RoleEducator, RoleMember:
return nil
default:
return errors.New("group role is not valid")
}
}

var (
Roles = []Role{RoleOwner, RoleEducator, RoleMember}

RoleOwner Role = "owner"
RoleEducator Role = "educator"
RoleMember Role = "member"
)

type Options struct {
Title string
Content string
}

type Option func(*Options)

func Name(n string) Option {
return func(opts *Options) {
opts.Title = n
}
}

func ShortName(n string) Option {
return func(opts *Options) {
opts.Content = n
}
}
46 changes: 46 additions & 0 deletions internal/group/forum/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package forum

import (
"errors"
"net/http"

"github.com/go-chi/render"
"github.com/np-inprove/server/internal/apperror"
)

var (
ErrGroupNotFound = errors.New("group does not exist")
ErrForumShortNameConflict = errors.New("a forum with the desired short name already exists")
ErrUnauthorized = errors.New("not authorized")
)

func mapDomainErr(err error) render.Renderer {
if errors.Is(err, ErrGroupNotFound) {
return &apperror.ErrResponse{
Err: err,
HTTPStatusCode: http.StatusNotFound,
AppErrCode: 40402,
AppErrMessage: "The group specified does not exist",
}
}

if errors.Is(err, ErrForumShortNameConflict) {
return &apperror.ErrResponse{
Err: err,
HTTPStatusCode: http.StatusConflict,
AppErrCode: 40902,
AppErrMessage: "This forum short name is unavailable",
}
}

if errors.Is(err, ErrUnauthorized) {
return &apperror.ErrResponse{
Err: err,
HTTPStatusCode: http.StatusUnauthorized,
AppErrCode: 40301,
AppErrMessage: "Unauthorized",
}
}

return apperror.ErrInternal(err)
}
24 changes: 24 additions & 0 deletions internal/group/forum/forumpost/repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package forumpost

import (
"context"

"github.com/np-inprove/server/internal/entity"
"github.com/np-inprove/server/internal/entity/forumpost"
)

type Reader interface {
FindForumPostsByUser() ([]*entity.ForumPost, error)
FindForumPost() (*entity.ForumPost, error)

FindGroupUser(ctx context.Context, principal, shortName string) (*entity.GroupUser, error)
}

type Writer interface {
CreateForumPost(ctx context.Context, forumID int, opts ...forumpost.Option) (*entity.ForumPost, error)
}

type Repository interface {
Reader
Writer
}
139 changes: 139 additions & 0 deletions internal/group/forum/httphandler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package forum

import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/jwtauth/v5"

"net/http"

"github.com/go-chi/render"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/np-inprove/server/internal/apperror"
"github.com/np-inprove/server/internal/config"
"github.com/np-inprove/server/internal/middleware"
"github.com/np-inprove/server/internal/payload"
)

type httpHandler struct {
service UseCase
cfg *config.Config
jwt *jwtauth.JWTAuth
}

func NewHTTPHandler(u UseCase, c *config.Config, j *jwtauth.JWTAuth) chi.Router {
a := &httpHandler{u, c, j}
r := chi.NewRouter()

// Authenticated routes
r.Group(func(r chi.Router) {
r.Use(jwtauth.Verify(j, func(r *http.Request) string {
c, err := r.Cookie(c.AppJWTCookieName())
if err != nil {
return ""
}
return c.Value
}))

r.Use(middleware.Authenticator)

r.Get("/", a.ListForums)
r.Post("/", a.CreateForum)
r.Delete("/{shortName}", a.DeleteForum)
r.Put("/{shortName}", a.UpdateForum)
})

return r
}

func (h httpHandler) ListForums(w http.ResponseWriter, r *http.Request) {
token := r.Context().Value(jwtauth.TokenCtxKey)
email := token.(jwt.Token).Subject()

forums, err := h.service.ListPrincipalForums(r.Context(), email)
tan-yun-e marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
_ = render.Render(w, r, mapDomainErr(err))
return
}

res := make([]render.Renderer, len(forums))
for i, item := range forums {
res[i] = payload.Forum{
ID: item.ID,
ShortName: item.ShortName,
Name: item.Name,
Description: item.Description,
}
}

_ = render.RenderList(w, r, res)
}

func (h httpHandler) CreateForum(w http.ResponseWriter, r *http.Request) {
p := &payload.CreateForumRequest{}
if err := render.Decode(r, p); err != nil {
_ = render.Render(w, r, apperror.ErrBadRequest(err))
return
}

if v := p.Validate(); !v.Validate() {
_ = render.Render(w, r, apperror.ErrValidation(v.Errors))
return
}

token := r.Context().Value(jwtauth.TokenCtxKey)
email := token.(jwt.Token).Subject()

res, err := h.service.CreateForum(r.Context(), email,
p.Name,
p.ShortName,
p.Description,
)

if err != nil {
_ = render.Render(w, r, mapDomainErr(err))
return
}

_ = render.Render(w, r, payload.Forum{
ID: res.ID,
ShortName: res.ShortName,
Name: res.Name,
Description: res.Description,
})
}

func (h httpHandler) DeleteForum(w http.ResponseWriter, r *http.Request) {
path := chi.URLParam(r, "shortName")
token := r.Context().Value(jwtauth.TokenCtxKey)
email := token.(jwt.Token).Subject()

if err := h.service.DeleteForum(r.Context(), email, path); err != nil {
_ = render.Render(w, r, mapDomainErr(err))
return
}

render.NoContent(w, r)
}

func (h httpHandler) UpdateForum(w http.ResponseWriter, r *http.Request) {
shortName := chi.URLParam(r, "shortName")
p := payload.UpdateForumRequest{}
if err := render.Decode(r, p); err != nil {
_ = render.Render(w, r, apperror.ErrBadRequest(err))
return
}

forum, err := h.service.UpdateForum(r.Context(), shortName,
p.Name,
p.ShortName,
p.Description,
)
if err != nil {
_ = render.Render(w, r, payload.Forum{
ID: forum.ID,
Name: forum.Name,
ShortName: forum.ShortName,
Description: forum.Description,
})
}
}
28 changes: 28 additions & 0 deletions internal/group/forum/repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package forum

import (
"context"

"github.com/np-inprove/server/internal/entity"
"github.com/np-inprove/server/internal/entity/forum"
)

type Reader interface {
FindForumsByGroup(ctx context.Context, principal string) ([]*entity.Forum, error)
FindForumByGroupIDAndShortName(ctx context.Context, groupID int, shortName string) (*entity.Forum, error)
FindForum(ctx context.Context, shortName string) (*entity.Forum, error)

FindUserWithGroups(ctx context.Context, principal string) (*entity.User, error)
FindGroupUser(ctx context.Context, principal, shortName string) (*entity.GroupUser, error)
}

type Writer interface {
CreateForum(ctx context.Context, groupID int, opts ...forum.Option) (*entity.Forum, error)
UpdateForum(ctx context.Context, forumID int, name, shortName, description string) (*entity.Forum, error)
DeleteForum(ctx context.Context, id int) error
}

type Repository interface {
Reader
Writer
}
Loading
Loading