8000 ギルドへの招待を追加 by keito0tada · Pull Request #36 · comb19/chat_back · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

ギルドへの招待を追加 #36

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
-->

## Issue
<!-- Closes #1 >
<!-- Closes #1 -->
7 changes: 7 additions & 0 deletions api/cmd/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ func SetupRouter() *gin.Engine {
guildUseCase := usecase.NewGuildUseCase(guildPersistence, userGuildsPersistence, channelPersistence, userChannelsPersistence)
guildHandler := handler.NewGuildHandler(guildUseCase)

guildInvitationPersistence := persistence.NewGuildInvitationPersistence(db)
guildInvitationUsecase := usecase.NewGuildInvitationUsecase(guildInvitationPersistence, userGuildsPersistence)
guildInvitationHandler := handler.NewGuildInvitationHandler(guildInvitationUsecase)

router := gin.Default()

router.Use(cors.New(cors.Config{
Expand Down Expand Up @@ -168,6 +172,9 @@ func SetupRouter() *gin.Engine {
authorized.GET("/guilds/:guildID/channels", guildHandler.HandleGetChannelsOfGuild)
authorized.POST("/guilds/:guildID/channels", guildHandler.HandleCreateChannelInGuild)
authorized.GET("/guilds/:guildID/users", func(ctx *gin.Context) {})

authorized.POST("/invitations/guilds/", guildInvitationHandler.CreateGuildInvitation)
authorized.GET("/invitations/guilds/:invitationID", guildInvitationHandler.VerifyGuildInvitation)
}

router.POST("/users", userHandler.HandleCreateUserByClerk)
Expand Down
12 changes: 12 additions & 0 deletions api/domain/model/guild_invitation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package model

import "time"

type GuildInvitation struct {
ID string `gorm:"type:uuid;default:gen_random_uuid();primaryKey"`
OwnerID string
GuildID string
Expiration time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
11 changes: 11 additions & 0 deletions api/domain/repository/guild_inviation_repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package repository

import (
"chat_back/domain/model"
"time"
)

type GuildInviationRepository interface {
Insert(ownerID, guildID string, expiration time.Time) (*model.GuildInvitation, error)
Find(id string) (*model.GuildInvitation, error)
}
47 changes: 47 additions & 0 deletions api/infrastructure/persistence/guild_invitation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package persistence

import (
"chat_back/domain/model"
"chat_back/domain/repository"
"errors"
"time"

"gorm.io/gorm"
)

type guildInvitationPersistence struct {
db *gorm.DB
}

func NewGuildInvitationPersistence(db *gorm.DB) repository.GuildInviationRepository {
return &guildInvitationPersistence{
db: db,
}
}

func (gip guildInvitationPersistence) Insert(ownerID, guildID string, expiration time.Time) (*model.GuildInvitation, error) {
guildInvitation := model.GuildInvitation{
OwnerID: ownerID,
GuildID: guildID,
Expiration: expiration,
}
result := gip.db.Create(&guildInvitation)
if err := result.Error; err != nil {
return nil, err
}

return &guildInvitation, nil
}

func (gip guildInvitationPersistence) Find(id string) (*model.GuildInvitation, error) {
var guildInvitation model.GuildInvitation
result := gip.db.Where("id = ?", id).First(&guildInvitation)
if err := result.Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}

return &guildInvitation, nil
}
94 changes: 94 additions & 0 deletions api/interface/handler/guild_invitation.go
5D40
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package handler

import (
"chat_back/interface/types"
"chat_back/usecase"
"fmt"
"log/slog"
"net/http"

"github.com/clerk/clerk-sdk-go/v2"
"github.com/gin-gonic/gin"
)

type GuildInvitationHandler interface {
CreateGuildInvitation(ctx *gin.Context)
VerifyGuildInvitation(ctx *gin.Context)
}

type guildInvitationHandler struct {
guildInvitationUsecase usecase.GuildInvitationUsecase
}

func NewGuildInvitationHandler(guildInvitationUsecase usecase.GuildInvitationUsecase) GuildInvitationHandler {
return &guildInvitationHandler{
guildInvitationUsecase: guildInvitationUsecase,
}
}

func (gih guildInvitationHandler) CreateGuildInvitation(ctx *gin.Context) {
slog.DebugContext(ctx, "CreateGuildInvitation")

var requestGuildInvitation types.RequestGuildInvitation
if err := ctx.BindJSON(&requestGuildInvitation); err != nil {
return
}

tempUser, ok := ctx.Get("user")
if !ok {
ctx.Status(http.StatusUnauthorized)
return
}
user, ok := tempUser.(*clerk.User)
if !ok {
ctx.Status(http.StatusInternalServerError)
return
}

guildInvitation, err := gih.guildInvitationUsecase.CreateGuildInvitation(user.ID, requestGuildInvitation.GuildID)
if err != nil {
ctx.Status(http.StatusInternalServerError)
return
}

ctx.JSON(http.StatusCreated, types.ResponseGuildInvitation{
ID: guildInvitation.ID,
OwnerID: guildInvitation.OwnerID,
GuildID: guildInvitation.GuildID,
Expiration: guildInvitation.Expiration,
URL: fmt.Sprintf("/invitations/guilds/%s", guildInvitation.ID),
})
}

func (gih guildInvitationHandler) VerifyGuildInvitation(ctx *gin.Context) {
slog.DebugContext(ctx, "VerifyGuildInvitation")

var guildInvitationUri types.GuildInvitationURI
if err := ctx.BindUri(&guildInvitationUri); err != nil {
return
}

tempUser, ok := ctx.Get("user")
if !ok {
ctx.Status(http.StatusUnauthorized)
return
}
user, ok := tempUser.(*clerk.User)
if !ok {
ctx.Status(http.StatusInternalServerError)
return
}

verified, err := gih.guildInvitationUsecase.VerifyGuildInvitation(guildInvitationUri.ID, user.ID)
if err != nil {
ctx.Status(http.StatusBadRequest)
return
}
if verified {
ctx.Status(http.StatusAccepted)
return
} else {
ctx.Status(http.StatusBadRequest)
return
}
}
19 changes: 19 additions & 0 deletions api/interface/types/guild_invitation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package types

import "time"

type GuildInvitationURI struct {
ID string `uri:"invitationID" binding:"required,uuid"`
}

type RequestGuildInvitation struct {
GuildID string `json:"guild_id"`
}

type ResponseGuildInvitation struct {
ID string `json:"id"`
OwnerID string `json:"owner_id"`
GuildID string `json:"guild_id"`
Expiration time.Time `json:"expiration"`
URL string `json:"url"`
}
67 changes: 67 additions & 0 deletions api/test/guild_invitation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package test

import (
"bytes"
"chat_back/cmd/app"
"chat_back/interface/types"
"chat_back/test/utils"
"encoding/json"
"io"
"log"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
)

func TestCreateGuildInvitation(t *testing.T) {
const ownerID = "user_2wtyaUmmGOlugFuP6h89SS4Pej3"
const inviteeID = "user_2xBp8XPtjCCnc7RLAICKTpzRrfB"
const guildID = "6b3075b7-d72b-4c41-983f-71213b16e1d7"

ownerToken, err := utils.FetchClerkToken(ownerID)
if err != nil {
log.Fatal(err)
return
}
inviteeToken, err := utils.FetchClerkToken(inviteeID)
if err != nil {
log.Fatal(err)
return
}

router := app.SetupRouter()

requestGuildInvitation := types.RequestGuildInvitation{
GuildID: guildID,
}
reqBody, err := json.Marshal(requestGuildInvitation)
if err != nil {
log.Fatal(err)
}

w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/invitations/guilds/", bytes.NewReader(reqBody))
req.Header.Set("Authorization", "Bearer "+*ownerToken)
router.ServeHTTP(w, req)

res := w.Result()
body, _ := io.ReadAll(res.Body)
var responseGuildInvitation types.ResponseGuildInvitation
if err := json.Unmarshal(body, &responseGuildInvitation); err != nil {
log.Fatal(err)
return
}

assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, guildID, responseGuildInvitation.GuildID)
assert.Equal(t, ownerID, responseGuildInvitation.OwnerID)

w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", responseGuildInvitation.URL, nil)
req.Header.Set("Authorization", "Bearer "+*inviteeToken)
router.ServeHTTP(w, req)

assert.Equal(t, http.StatusAccepted, w.Code)
}
57 changes: 57 additions & 0 deletions api/usecase/guild_invitation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package usecase

import (
"chat_back/domain/model"
"chat_back/domain/repository"
"time"
)

const expireDays = 1

type GuildInvitationUsecase interface {
CreateGuildInvitation(ownerID, guildID string) (*model.GuildInvitation, error)
VerifyGuildInvitation(id, userID string) (bool, error)
}

type guildInvitationUsecase struct {
guildInvitationRepository repository.GuildInviationRepository
userGuildRepository repository.UserGuildsRepository
}

func NewGuildInvitationUsecase(guildInvitationRepository repository.GuildInviationRepository, userGuildRepository repository.UserGuildsRepository) GuildInvitationUsecase {
return guildInvitationUsecase{
guildInvitationRepository: guildInvitationRepository,
userGuildRepository: userGuildRepository,
}
}

func (giu guildInvitationUsecase) CreateGuildInvitation(ownerID, guildID string) (*model.GuildInvitation, error) {
return giu.guildInvitationRepository.Insert(ownerID, guildID, time.Now())
}

func (giu guildInvitationUsecase) VerifyGuildInvitation(id, userID string) (bool, error) {
guildInvitation, err := giu.guildInvitationRepository.Find(id)
if err != nil {
return false, err
}
if guildInvitation == nil {
return false, nil
}
if !guildInvitation.Expiration.Before(time.Now().AddDate(0, 0, expireDays)) {
return false, nil
}

userGuild, err := giu.userGuildRepository.Find(userID, guildInvitation.GuildID)
if err != nil {
return false, err
}
if userGuild != nil {
return true, err
}

_, err = giu.userGuildRepository.Insert(userID, guildInvitation.GuildID)
if err != nil {
return false, err
}
return true, nil
}
12 changes: 12 additions & 0 deletions db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ CREATE TABLE "user_guilds" (
FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE
);

CREATE TABLE "guild_invitations" (
"id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"owner_id" varchar(32) NOT NULL,
"guild_id" UUID NOT NULL,
"expiration" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE
);

CREATE OR REPLACE FUNCTION update_timestamp()
RETURNS TRIGGER AS $$
BEGIN
Expand Down
0