Skip to content

Commit

Permalink
Merge pull request #1161 from ictsc/admin-invitation-code
Browse files Browse the repository at this point in the history
feat: implement admin.v1.InvitationService
  • Loading branch information
tosuke authored Feb 1, 2025
2 parents 93c5744 + b127d4f commit 485b7ad
Show file tree
Hide file tree
Showing 16 changed files with 861 additions and 10 deletions.
1 change: 1 addition & 0 deletions backend/.golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ linters:
- ireturn
- gofumpt
- tagliatelle
- funlen

issues:
exclude-use-default: false
Expand Down
12 changes: 6 additions & 6 deletions backend/pkg/pgxutil/open.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,16 +84,16 @@ func defaultAttributeGetter(_ context.Context, _ otelsql.Method, query string, _
}

var (
queryReMap = map[string]*regexp.Regexp{
"SELECT": regexp.MustCompile(`(?i)^\s*SELECT\s+.*\s+FROM\s+([^\s]+)`),
"INSERT": regexp.MustCompile(`(?i)^\s*INSERT\s+INTO\s+([^\s]+)`),
"UPDATE": regexp.MustCompile(`(?i)^\s*UPDATE\s+([^\s]+)`),
"DELETE": regexp.MustCompile(`(?i)^\s*DELETE\s+FROM\s+([^\s]+)`),
queryRegexMap = map[string]*regexp.Regexp{
"SELECT": regexp.MustCompile(`(?ims)^\s*SELECT\s+[\s\S]*?\sFROM\s+([^\s]+)`),
"UPDATE": regexp.MustCompile(`(?ims)^\s*UPDATE\s+([^\s]+)`),
"DELETE": regexp.MustCompile(`(?ims)^\s*DELETE\s+FROM\s+([^\s]+)`),
"INSERT": regexp.MustCompile(`(?ims)^\s*INSERT\s+INTO\s+([^\s]+)`),
}
)

func summarizeQuery(query string) string {
for key, re := range queryReMap {
for key, re := range queryRegexMap {
if match := re.FindStringSubmatch(query); match != nil {
table := strings.Trim(match[1], `"`)
return fmt.Sprintf("%s %s", key, table)
Expand Down
13 changes: 13 additions & 0 deletions backend/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,16 @@ COMMENT ON COLUMN teams.id IS 'チーム ID';
COMMENT ON COLUMN teams.code IS 'チーム番号';
COMMENT ON COLUMN teams.name IS 'チーム名';
COMMENT ON COLUMN teams.organization IS 'チームの所属組織名';

CREATE TABLE invitation_codes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
code VARCHAR(255) NOT NULL UNIQUE,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE invitation_codes IS '招待コード';
COMMENT ON COLUMN invitation_codes.id IS '招待コード ID';
COMMENT ON COLUMN invitation_codes.team_id IS 'チーム ID';
COMMENT ON COLUMN invitation_codes.code IS '招待コード';
COMMENT ON COLUMN invitation_codes.expires_at IS '有効期限';
2 changes: 2 additions & 0 deletions backend/scoreserver/admin/auth/policy.csv
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ p, role:admin, teams, get
p, role:admin, teams, create
p, role:admin, teams, update
p, role:admin, teams, delete
p, role:admin, invitation_codes, list
p, role:admin, invitation_codes, create
89 changes: 89 additions & 0 deletions backend/scoreserver/admin/invitation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package admin

import (
"context"

"connectrpc.com/connect"
adminv1 "github.com/ictsc/ictsc-regalia/backend/pkg/proto/admin/v1"
"github.com/ictsc/ictsc-regalia/backend/pkg/proto/admin/v1/adminv1connect"
"github.com/ictsc/ictsc-regalia/backend/scoreserver/admin/auth"
"github.com/ictsc/ictsc-regalia/backend/scoreserver/domain"
"github.com/ictsc/ictsc-regalia/backend/scoreserver/infra/pg"
"google.golang.org/protobuf/types/known/timestamppb"
)

type InvitationServiceHandler struct {
adminv1connect.UnimplementedInvitationServiceHandler
Enforcer *auth.Enforcer
ListWorkflow *domain.InvitationCodeListWorkflow
CreateWorkflow *domain.InvitationCodeCreateWorkflow
}

func NewInvitationServiceHandler(enforcer *auth.Enforcer, repo *pg.Repository) *InvitationServiceHandler {
return &InvitationServiceHandler{
UnimplementedInvitationServiceHandler: adminv1connect.UnimplementedInvitationServiceHandler{},

Enforcer: enforcer,
ListWorkflow: &domain.InvitationCodeListWorkflow{Lister: repo},
CreateWorkflow: &domain.InvitationCodeCreateWorkflow{
TeamGetter: repo,
RunTx: func(ctx context.Context, f func(eff domain.InvitationCodeCreator) error) error {
return repo.RunTx(ctx, func(tx *pg.RepositoryTx) error { return f(tx) })
},
},
}
}

var _ adminv1connect.InvitationServiceHandler = (*InvitationServiceHandler)(nil)

func (h *InvitationServiceHandler) ListInvitationCodes(
ctx context.Context,
req *connect.Request[adminv1.ListInvitationCodesRequest],
) (*connect.Response[adminv1.ListInvitationCodesResponse], error) {
if err := enforce(ctx, h.Enforcer, "invitation_codes", "list"); err != nil {
return nil, err
}

ics, err := h.ListWorkflow.Run(ctx)
if err != nil {
return nil, connectError(err)
}

protoICs := make([]*adminv1.InvitationCode, 0, len(ics))
for _, ic := range ics {
protoICs = append(protoICs, convertInvitationCode(ic))
}

return connect.NewResponse(&adminv1.ListInvitationCodesResponse{
InvitationCodes: protoICs,
}), nil
}

func (h *InvitationServiceHandler) CreateInvitationCode(
ctx context.Context,
req *connect.Request[adminv1.CreateInvitationCodeRequest],
) (*connect.Response[adminv1.CreateInvitationCodeResponse], error) {
if err := enforce(ctx, h.Enforcer, "invitation_codes", "create"); err != nil {
return nil, err
}

code, err := h.CreateWorkflow.Run(ctx, domain.InvitationCodeCreateInput{
TeamCode: int(req.Msg.GetInvitationCode().GetTeamCode()),
ExpiresAt: req.Msg.GetInvitationCode().GetExpiresAt().AsTime(),
})
if err != nil {
return nil, connectError(err)
}

return connect.NewResponse(&adminv1.CreateInvitationCodeResponse{
InvitationCode: convertInvitationCode(code),
}), nil
}

func convertInvitationCode(ic *domain.InvitationCode) *adminv1.InvitationCode {
return &adminv1.InvitationCode{
Code: ic.Code(),
TeamCode: int64(ic.Team().Code()),
ExpiresAt: timestamppb.New(ic.ExpiresAt()),
}
}
86 changes: 86 additions & 0 deletions backend/scoreserver/admin/invitation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package admin_test

import (
"context"
"net/http"
"testing"
"time"

"connectrpc.com/connect"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/ictsc/ictsc-regalia/backend/pkg/pgtest"
adminv1 "github.com/ictsc/ictsc-regalia/backend/pkg/proto/admin/v1"
"github.com/ictsc/ictsc-regalia/backend/pkg/proto/admin/v1/adminv1connect"
"github.com/ictsc/ictsc-regalia/backend/scoreserver/admin"
"github.com/ictsc/ictsc-regalia/backend/scoreserver/infra/pg"
"google.golang.org/protobuf/types/known/timestamppb"
)

func TestAdminInvitationService_Create(t *testing.T) {
t.Parallel()

now := time.Now()

cases := map[string]struct {
in *adminv1.CreateInvitationCodeRequest
wants *adminv1.CreateInvitationCodeResponse
wantCode connect.Code
}{
"ok": {
in: &adminv1.CreateInvitationCodeRequest{
InvitationCode: &adminv1.InvitationCode{
TeamCode: 1,
ExpiresAt: timestamppb.New(now.Add(24 * time.Hour)),
},
},
wants: &adminv1.CreateInvitationCodeResponse{
InvitationCode: &adminv1.InvitationCode{
TeamCode: 1,
ExpiresAt: timestamppb.New(now.Add(24 * time.Hour)),
},
},
},
}

for name, tt := range cases {
t.Run(name, func(t *testing.T) {
t.Parallel()

ctx := context.Background()

enforcer := setupEnforcer(t)

db, ok := pgtest.SetupDB(t)
if !ok {
t.FailNow()
}
teamFixtures(db)

mux := http.NewServeMux()
mux.Handle(adminv1connect.NewInvitationServiceHandler(admin.NewInvitationServiceHandler(
enforcer, pg.NewRepository(db),
)))

server := setupServer(t, mux)
client := adminv1connect.NewInvitationServiceClient(server.Client(), server.URL)

resp, err := client.CreateInvitationCode(ctx, connect.NewRequest(tt.in))
assertCode(t, tt.wantCode, err)
if err != nil {
return
}

if diff := cmp.Diff(
resp.Msg, tt.wants,
cmpopts.IgnoreUnexported(
adminv1.CreateInvitationCodeResponse{},
adminv1.InvitationCode{},
timestamppb.Timestamp{}),
cmpopts.IgnoreFields(adminv1.CreateInvitationCodeResponse{}, "InvitationCode.Code"),
); diff != "" {
t.Errorf("(-got, +want)\n%s", diff)
}
})
}
}
4 changes: 4 additions & 0 deletions backend/scoreserver/admin/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ func New(ctx context.Context, cfg config.AdminAPI, db *sqlx.DB) (http.Handler, e
NewTeamServiceHandler(enforcer, repo),
connect.WithInterceptors(interceptors...),
))
mux.Handle(adminv1connect.NewInvitationServiceHandler(
NewInvitationServiceHandler(enforcer, repo),
connect.WithInterceptors(interceptors...),
))

checker := grpchealth.NewStaticChecker("admin.v1.TeamService")
mux.Handle(grpchealth.NewHandler(checker))
Expand Down
12 changes: 12 additions & 0 deletions backend/scoreserver/domain/clock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package domain

import "time"

type Clock func() time.Time

func (c Clock) Now() time.Time {
if c == nil {
return time.Now()
}
return c()
}
4 changes: 4 additions & 0 deletions backend/scoreserver/domain/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ func NewError(typ ErrType, err error) error {
}
}

func WrapAsInternal(err error, msg string) error {
return NewError(ErrTypeInternal, errors.WrapWithDepth(1, err, msg))
}

func (e *Error) Error() string {
return e.typ.String() + ": " + e.err.Error()
}
Expand Down
Loading

0 comments on commit 485b7ad

Please sign in to comment.