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
27 changes: 27 additions & 0 deletions internal/entity/forum/forum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package forum

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
}
}
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)
}
144 changes: 144 additions & 0 deletions internal/group/forum/httphandler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
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("/{forumShortName}", a.DeleteForum)
r.Put("/{forumShortName}", 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()

shortName := chi.URLParam(r, "forumShortName")

forums, err := h.service.ListForums(r.Context(), email, shortName)
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()
shortName := chi.URLParam(r, "forumShortName")

res, err := h.service.CreateForum(r.Context(), email, shortName,
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, "forumShortName")
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, "forumShortName")
token := r.Context().Value(jwtauth.TokenCtxKey)
email := token.(jwt.Token).Subject()
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(), email, 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 {
FindForumsByGroupAndInstitution(ctx context.Context, principal string, instShortName 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)
FindGroupUserWithGroup(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
}
137 changes: 137 additions & 0 deletions internal/group/forum/repository_ent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package forum

import (
"context"
"fmt"

"github.com/np-inprove/server/internal/ent"
entforum "github.com/np-inprove/server/internal/ent/forum"
entgroup "github.com/np-inprove/server/internal/ent/group"
"github.com/np-inprove/server/internal/ent/groupuser"
entinstitution "github.com/np-inprove/server/internal/ent/institution"
"github.com/np-inprove/server/internal/ent/predicate"
"github.com/np-inprove/server/internal/ent/user"
"github.com/np-inprove/server/internal/entity"
"github.com/np-inprove/server/internal/entity/forum"
)

type entRepository struct {
client *ent.Client
}

func NewEntRepository(e *ent.Client) Repository {
return &entRepository{client: e}
}

func (e entRepository) FindForumsByGroupAndInstitution(ctx context.Context, principal string, instShortname string) ([]*entity.Forum, error) {
forum, err := e.client.Forum.Query().
Where(
entforum.HasGroupWith(
entgroup.HasInstitutionWith(
entinstitution.ShortName(instShortname),
),
),
).
All(ctx)
if err != nil {
return nil, fmt.Errorf("failed to find forums by group and institution: %w", err)
}

return forum, nil
}

func (e entRepository) FindForumByGroupIDAndShortName(ctx context.Context, groupID int, shortName string) (*entity.Forum, error) {
forum, err := e.client.Forum.Query().
Where(
entforum.HasGroupWith(
entgroup.ID(groupID),
),
entforum.ShortName(shortName),
).
Only(ctx)
if err != nil {
return nil, fmt.Errorf("failed to find forums by user email: %w", err)
}

return forum, nil
}

func (e entRepository) FindForum(ctx context.Context, shortName string) (*entity.Forum, error) {
inst, err := e.client.Forum.Query().Where(
predicate.Forum(
entgroup.HasForumsWith(
entforum.ShortName(shortName),
),
),
).Only(ctx)
if err != nil {
return nil, fmt.Errorf("failed to find forum: %w", err)
}
return inst, nil
}

func (e entRepository) FindUserWithGroups(ctx context.Context, principal string) (*entity.User, error) {
usr, err := e.client.User.Query().
Where(
user.Email(principal),
).WithGroups().
Only(ctx)
if err != nil {
return nil, fmt.Errorf("failed to find user with group: %w", err)
}

return usr, nil
}

func (e entRepository) FindGroupUserWithGroup(ctx context.Context, principal string, shortName string) (*entity.GroupUser, error) {
grpusr, err := e.client.GroupUser.Query().
Where(
groupuser.HasUserWith(user.Email(principal)),
groupuser.HasGroupWith(entgroup.ShortName(shortName)),
).Only(ctx)
if err != nil {
return nil, fmt.Errorf("failed to find group user: %w", err)
}

return grpusr, nil
}

func (e entRepository) CreateForum(ctx context.Context, groupID int, opts ...forum.Option) (*entity.Forum, error) {
var options forum.Options
for _, opt := range opts {
opt(&options)
}

forum, err := e.client.Forum.
Create().
SetName(options.Name).
SetShortName(options.ShortName).
SetDescription(options.Description).
SetGroupID(groupID).
Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed to create group: %w", err)
}
return forum, nil
}

func (e entRepository) UpdateForum(ctx context.Context, id int, name, shortName, description string) (*entity.Forum, error) {
forum, err := e.client.Forum.UpdateOneID(id).
SetName(name).
SetShortName(shortName).
SetDescription(description).
Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed to update forum: %w", err)
}

return forum, nil
}

func (e entRepository) DeleteForum(ctx context.Context, id int) error {
err := e.client.Forum.DeleteOneID(id).Exec(ctx)
if err != nil {
return fmt.Errorf("failed to delete forum: %w", err)
}
return err
}
Loading
Loading