mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-05 16:52:07 +00:00
fix: projects (#221)
* feat: projects and project grants seperated * fix: tests * fix: add mock
This commit is contained in:
@@ -41,33 +41,33 @@ func (repo *ProjectRepo) ReactivateProject(ctx context.Context, id string) (*pro
|
||||
return repo.ProjectEvents.ReactivateProject(ctx, id)
|
||||
}
|
||||
|
||||
func (repo *ProjectRepo) SearchGrantedProjects(ctx context.Context, request *proj_model.GrantedProjectSearchRequest) (*proj_model.GrantedProjectSearchResponse, error) {
|
||||
func (repo *ProjectRepo) SearchProjects(ctx context.Context, request *proj_model.ProjectViewSearchRequest) (*proj_model.ProjectViewSearchResponse, error) {
|
||||
request.EnsureLimit(repo.SearchLimit)
|
||||
|
||||
permissions := auth.GetPermissionsFromCtx(ctx)
|
||||
if !auth.HasGlobalPermission(permissions) {
|
||||
ids := auth.GetPermissionCtxIDs(permissions)
|
||||
request.Queries = append(request.Queries, &proj_model.GrantedProjectSearchQuery{Key: proj_model.GRANTEDPROJECTSEARCHKEY_PROJECTID, Method: global_model.SEARCHMETHOD_IN, Value: ids})
|
||||
request.Queries = append(request.Queries, &proj_model.ProjectViewSearchQuery{Key: proj_model.PROJECTSEARCHKEY_PROJECTID, Method: global_model.SEARCHMETHOD_IN, Value: ids})
|
||||
}
|
||||
|
||||
projects, count, err := repo.View.SearchGrantedProjects(request)
|
||||
projects, count, err := repo.View.SearchProjects(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &proj_model.GrantedProjectSearchResponse{
|
||||
return &proj_model.ProjectViewSearchResponse{
|
||||
Offset: request.Offset,
|
||||
Limit: request.Limit,
|
||||
TotalResult: uint64(count),
|
||||
Result: model.GrantedProjectsToModel(projects),
|
||||
Result: model.ProjectsToModel(projects),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (repo *ProjectRepo) GetGrantedProjectGrantByIDs(ctx context.Context, projectID, grantID string) (project *proj_model.GrantedProjectView, err error) {
|
||||
p, err := repo.View.GrantedProjectGrantByIDs(projectID, grantID)
|
||||
func (repo *ProjectRepo) ProjectGrantViewByID(ctx context.Context, grantID string) (project *proj_model.ProjectGrantView, err error) {
|
||||
p, err := repo.View.ProjectGrantByID(grantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return model.GrantedProjectToModel(p), nil
|
||||
return model.ProjectGrantToModel(p), nil
|
||||
}
|
||||
|
||||
func (repo *ProjectRepo) ProjectMemberByID(ctx context.Context, projectID, userID string) (member *proj_model.ProjectMember, err error) {
|
||||
@@ -180,6 +180,20 @@ func (repo *ProjectRepo) ProjectGrantByID(ctx context.Context, projectID, appID
|
||||
return repo.ProjectEvents.ProjectGrantByIDs(ctx, projectID, appID)
|
||||
}
|
||||
|
||||
func (repo *ProjectRepo) SearchProjectGrants(ctx context.Context, request *proj_model.ProjectGrantViewSearchRequest) (*proj_model.ProjectGrantViewSearchResponse, error) {
|
||||
request.EnsureLimit(repo.SearchLimit)
|
||||
projects, count, err := repo.View.SearchProjectGrants(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &proj_model.ProjectGrantViewSearchResponse{
|
||||
Offset: request.Offset,
|
||||
Limit: request.Limit,
|
||||
TotalResult: uint64(count),
|
||||
Result: model.ProjectGrantsToModel(projects),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (repo *ProjectRepo) AddProjectGrant(ctx context.Context, app *proj_model.ProjectGrant) (*proj_model.ProjectGrant, error) {
|
||||
return repo.ProjectEvents.AddProjectGrant(ctx, app)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,8 @@ type EventstoreRepos struct {
|
||||
|
||||
func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, eventstore eventstore.Eventstore, repos EventstoreRepos) []spooler.Handler {
|
||||
return []spooler.Handler{
|
||||
&GrantedProject{handler: handler{view, bulkLimit, configs.cycleDuration("GrantedProject"), errorCount}, eventstore: eventstore, projectEvents: repos.ProjectEvents, orgEvents: repos.OrgEvents},
|
||||
&Project{handler: handler{view, bulkLimit, configs.cycleDuration("Project"), errorCount}, eventstore: eventstore},
|
||||
&ProjectGrant{handler: handler{view, bulkLimit, configs.cycleDuration("ProjectGrant"), errorCount}, eventstore: eventstore, projectEvents: repos.ProjectEvents, orgEvents: repos.OrgEvents},
|
||||
&ProjectRole{handler: handler{view, bulkLimit, configs.cycleDuration("ProjectRole"), errorCount}, projectEvents: repos.ProjectEvents},
|
||||
&ProjectMember{handler: handler{view, bulkLimit, configs.cycleDuration("ProjectMember"), errorCount}, userEvents: repos.UserEvents},
|
||||
&ProjectGrantMember{handler: handler{view, bulkLimit, configs.cycleDuration("ProjectGrantMember"), errorCount}, userEvents: repos.UserEvents},
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/caos/logging"
|
||||
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/eventstore/spooler"
|
||||
proj_event "github.com/caos/zitadel/internal/project/repository/eventsourcing"
|
||||
es_model "github.com/caos/zitadel/internal/project/repository/eventsourcing/model"
|
||||
view_model "github.com/caos/zitadel/internal/project/repository/view/model"
|
||||
)
|
||||
|
||||
type Project struct {
|
||||
handler
|
||||
eventstore eventstore.Eventstore
|
||||
}
|
||||
|
||||
const (
|
||||
projectTable = "management.projects"
|
||||
)
|
||||
|
||||
func (p *Project) MinimumCycleDuration() time.Duration { return p.cycleDuration }
|
||||
|
||||
func (p *Project) ViewModel() string {
|
||||
return projectTable
|
||||
}
|
||||
|
||||
func (p *Project) EventQuery() (*models.SearchQuery, error) {
|
||||
sequence, err := p.view.GetLatestProjectSequence()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return proj_event.ProjectQuery(sequence), nil
|
||||
}
|
||||
|
||||
func (p *Project) Process(event *models.Event) (err error) {
|
||||
project := new(view_model.ProjectView)
|
||||
switch event.Type {
|
||||
case es_model.ProjectAdded:
|
||||
project.AppendEvent(event)
|
||||
case es_model.ProjectChanged,
|
||||
es_model.ProjectDeactivated,
|
||||
es_model.ProjectReactivated:
|
||||
project, err = p.view.ProjectByID(event.AggregateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = project.AppendEvent(event)
|
||||
default:
|
||||
return p.view.ProcessedProjectSequence(event.Sequence)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.view.PutProject(project)
|
||||
}
|
||||
|
||||
func (p *Project) OnError(event *models.Event, err error) error {
|
||||
logging.LogWithFields("SPOOL-dLsop3", "id", event.AggregateID).WithError(err).Warn("something went wrong in projecthandler")
|
||||
return spooler.HandleError(event, err, p.view.GetLatestProjectFailedEvent, p.view.ProcessedProjectFailedEvent, p.view.ProcessedProjectSequence, p.errorCountUntilSkip)
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
view_model "github.com/caos/zitadel/internal/project/repository/view/model"
|
||||
)
|
||||
|
||||
type GrantedProject struct {
|
||||
type ProjectGrant struct {
|
||||
handler
|
||||
eventstore eventstore.Eventstore
|
||||
projectEvents *proj_event.ProjectEventstore
|
||||
@@ -25,44 +25,32 @@ type GrantedProject struct {
|
||||
}
|
||||
|
||||
const (
|
||||
grantedProjectTable = "management.granted_projects"
|
||||
grantedProjectTable = "management.project_grants"
|
||||
)
|
||||
|
||||
func (p *GrantedProject) MinimumCycleDuration() time.Duration { return p.cycleDuration }
|
||||
func (p *ProjectGrant) MinimumCycleDuration() time.Duration { return p.cycleDuration }
|
||||
|
||||
func (p *GrantedProject) ViewModel() string {
|
||||
func (p *ProjectGrant) ViewModel() string {
|
||||
return grantedProjectTable
|
||||
}
|
||||
|
||||
func (p *GrantedProject) EventQuery() (*models.SearchQuery, error) {
|
||||
sequence, err := p.view.GetLatestGrantedProjectSequence()
|
||||
func (p *ProjectGrant) EventQuery() (*models.SearchQuery, error) {
|
||||
sequence, err := p.view.GetLatestProjectGrantSequence()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return proj_event.ProjectQuery(sequence), nil
|
||||
}
|
||||
|
||||
func (p *GrantedProject) Process(event *models.Event) (err error) {
|
||||
grantedProject := new(view_model.GrantedProjectView)
|
||||
func (p *ProjectGrant) Process(event *models.Event) (err error) {
|
||||
grantedProject := new(view_model.ProjectGrantView)
|
||||
switch event.Type {
|
||||
case es_model.ProjectAdded:
|
||||
grantedProject.AppendEvent(event)
|
||||
case es_model.ProjectChanged:
|
||||
grantedProject, err = p.view.GrantedProjectByIDs(event.AggregateID, event.ResourceOwner)
|
||||
project, err := p.view.ProjectByID(event.AggregateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = grantedProject.AppendEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.updateExistingProjects(grantedProject)
|
||||
case es_model.ProjectDeactivated, es_model.ProjectReactivated:
|
||||
grantedProject, err = p.view.GrantedProjectByIDs(event.AggregateID, event.ResourceOwner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = grantedProject.AppendEvent(event)
|
||||
p.updateExistingProjects(project)
|
||||
case es_model.ProjectGrantAdded:
|
||||
err = grantedProject.AppendEvent(event)
|
||||
if err != nil {
|
||||
@@ -85,7 +73,7 @@ func (p *GrantedProject) Process(event *models.Event) (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
grantedProject, err = p.view.GrantedProjectByIDs(event.AggregateID, grant.GrantedOrgID)
|
||||
grantedProject, err = p.view.ProjectGrantByID(grant.GrantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -96,38 +84,38 @@ func (p *GrantedProject) Process(event *models.Event) (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.view.DeleteGrantedProject(event.AggregateID, grant.GrantedOrgID, event.Sequence)
|
||||
return p.view.DeleteProjectGrant(grant.GrantID, event.Sequence)
|
||||
default:
|
||||
return p.view.ProcessedGrantedProjectSequence(event.Sequence)
|
||||
return p.view.ProcessedProjectGrantSequence(event.Sequence)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.view.PutGrantedProject(grantedProject)
|
||||
return p.view.PutProjectGrant(grantedProject)
|
||||
}
|
||||
|
||||
func (p *GrantedProject) fillOrgData(grantedProject *view_model.GrantedProjectView, org *org_model.Org) {
|
||||
func (p *ProjectGrant) fillOrgData(grantedProject *view_model.ProjectGrantView, org *org_model.Org) {
|
||||
grantedProject.OrgDomain = org.Domain
|
||||
grantedProject.OrgName = org.Name
|
||||
}
|
||||
|
||||
func (p *GrantedProject) getProject(projectID string) (*proj_model.Project, error) {
|
||||
func (p *ProjectGrant) getProject(projectID string) (*proj_model.Project, error) {
|
||||
return p.projectEvents.ProjectByID(context.Background(), projectID)
|
||||
}
|
||||
|
||||
func (p *GrantedProject) updateExistingProjects(project *view_model.GrantedProjectView) {
|
||||
projects, err := p.view.GrantedProjectsByID(project.ProjectID)
|
||||
func (p *ProjectGrant) updateExistingProjects(project *view_model.ProjectView) {
|
||||
projects, err := p.view.ProjectGrantsByProjectID(project.ProjectID)
|
||||
if err != nil {
|
||||
logging.LogWithFields("SPOOL-los03", "id", project.ProjectID).WithError(err).Warn("could not update existing projects")
|
||||
}
|
||||
for _, existing := range projects {
|
||||
existing.Name = project.Name
|
||||
err := p.view.PutGrantedProject(existing)
|
||||
err := p.view.PutProjectGrant(existing)
|
||||
logging.LogWithFields("SPOOL-sjwi3", "id", existing.ProjectID).WithError(err).Warn("could not update existing project")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *GrantedProject) OnError(event *models.Event, err error) error {
|
||||
func (p *ProjectGrant) OnError(event *models.Event, err error) error {
|
||||
logging.LogWithFields("SPOOL-is8wa", "id", event.AggregateID).WithError(err).Warn("something went wrong in granted projecthandler")
|
||||
return spooler.HandleError(event, err, p.view.GetLatestGrantedProjectFailedEvent, p.view.ProcessedGrantedProjectFailedEvent, p.view.ProcessedGrantedProjectSequence, p.errorCountUntilSkip)
|
||||
return spooler.HandleError(event, err, p.view.GetLatestProjectGrantFailedEvent, p.view.ProcessedProjectGrantFailedEvent, p.view.ProcessedProjectGrantSequence, p.errorCountUntilSkip)
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
proj_model "github.com/caos/zitadel/internal/project/model"
|
||||
"github.com/caos/zitadel/internal/project/repository/view"
|
||||
"github.com/caos/zitadel/internal/project/repository/view/model"
|
||||
global_view "github.com/caos/zitadel/internal/view"
|
||||
)
|
||||
|
||||
const (
|
||||
grantedProjectTable = "management.granted_projects"
|
||||
)
|
||||
|
||||
func (v *View) GrantedProjectByIDs(projectID, orgID string) (*model.GrantedProjectView, error) {
|
||||
return view.GrantedProjectByIDs(v.Db, grantedProjectTable, projectID, orgID)
|
||||
}
|
||||
|
||||
func (v *View) GrantedProjectGrantByIDs(projectID, grantID string) (*model.GrantedProjectView, error) {
|
||||
return view.GrantedProjectGrantByIDs(v.Db, grantedProjectTable, projectID, grantID)
|
||||
}
|
||||
|
||||
func (v *View) GrantedProjectsByID(projectID string) ([]*model.GrantedProjectView, error) {
|
||||
return view.GrantedProjectsByID(v.Db, grantedProjectTable, projectID)
|
||||
}
|
||||
|
||||
func (v *View) SearchGrantedProjects(request *proj_model.GrantedProjectSearchRequest) ([]*model.GrantedProjectView, int, error) {
|
||||
return view.SearchGrantedProjects(v.Db, grantedProjectTable, request)
|
||||
}
|
||||
|
||||
func (v *View) PutGrantedProject(project *model.GrantedProjectView) error {
|
||||
err := view.PutGrantedProject(v.Db, grantedProjectTable, project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedGrantedProjectSequence(project.Sequence)
|
||||
}
|
||||
|
||||
func (v *View) DeleteGrantedProject(projectID, orgID string, eventSequence uint64) error {
|
||||
err := view.DeleteGrantedProject(v.Db, grantedProjectTable, projectID, orgID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return v.ProcessedGrantedProjectSequence(eventSequence)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestGrantedProjectSequence() (uint64, error) {
|
||||
return v.latestSequence(grantedProjectTable)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedGrantedProjectSequence(eventSequence uint64) error {
|
||||
return v.saveCurrentSequence(grantedProjectTable, eventSequence)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestGrantedProjectFailedEvent(sequence uint64) (*global_view.FailedEvent, error) {
|
||||
return v.latestFailedEvent(grantedProjectTable, sequence)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedGrantedProjectFailedEvent(failedEvent *global_view.FailedEvent) error {
|
||||
return v.saveFailedEvent(failedEvent)
|
||||
}
|
||||
52
internal/management/repository/eventsourcing/view/project.go
Normal file
52
internal/management/repository/eventsourcing/view/project.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
proj_model "github.com/caos/zitadel/internal/project/model"
|
||||
"github.com/caos/zitadel/internal/project/repository/view"
|
||||
"github.com/caos/zitadel/internal/project/repository/view/model"
|
||||
global_view "github.com/caos/zitadel/internal/view"
|
||||
)
|
||||
|
||||
const (
|
||||
projectTable = "management.projects"
|
||||
)
|
||||
|
||||
func (v *View) ProjectByID(projectID string) (*model.ProjectView, error) {
|
||||
return view.ProjectByID(v.Db, projectTable, projectID)
|
||||
}
|
||||
|
||||
func (v *View) SearchProjects(request *proj_model.ProjectViewSearchRequest) ([]*model.ProjectView, int, error) {
|
||||
return view.SearchProjects(v.Db, projectTable, request)
|
||||
}
|
||||
|
||||
func (v *View) PutProject(project *model.ProjectView) error {
|
||||
err := view.PutProject(v.Db, projectTable, project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedProjectSequence(project.Sequence)
|
||||
}
|
||||
|
||||
func (v *View) DeleteProject(projectID string, eventSequence uint64) error {
|
||||
err := view.DeleteProject(v.Db, projectTable, projectID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return v.ProcessedProjectSequence(eventSequence)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestProjectSequence() (uint64, error) {
|
||||
return v.latestSequence(projectTable)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedProjectSequence(eventSequence uint64) error {
|
||||
return v.saveCurrentSequence(projectTable, eventSequence)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestProjectFailedEvent(sequence uint64) (*global_view.FailedEvent, error) {
|
||||
return v.latestFailedEvent(projectTable, sequence)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedProjectFailedEvent(failedEvent *global_view.FailedEvent) error {
|
||||
return v.saveFailedEvent(failedEvent)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
proj_model "github.com/caos/zitadel/internal/project/model"
|
||||
"github.com/caos/zitadel/internal/project/repository/view"
|
||||
"github.com/caos/zitadel/internal/project/repository/view/model"
|
||||
global_view "github.com/caos/zitadel/internal/view"
|
||||
)
|
||||
|
||||
const (
|
||||
grantedProjectTable = "management.project_grants"
|
||||
)
|
||||
|
||||
func (v *View) ProjectGrantByID(grantID string) (*model.ProjectGrantView, error) {
|
||||
return view.ProjectGrantByID(v.Db, grantedProjectTable, grantID)
|
||||
}
|
||||
|
||||
func (v *View) ProjectGrantByProjectAndOrg(projectID, orgID string) (*model.ProjectGrantView, error) {
|
||||
return view.ProjectGrantByProjectAndOrg(v.Db, grantedProjectTable, projectID, orgID)
|
||||
}
|
||||
|
||||
func (v *View) ProjectGrantsByProjectID(projectID string) ([]*model.ProjectGrantView, error) {
|
||||
return view.ProjectGrantsByProjectID(v.Db, grantedProjectTable, projectID)
|
||||
}
|
||||
|
||||
func (v *View) SearchProjectGrants(request *proj_model.ProjectGrantViewSearchRequest) ([]*model.ProjectGrantView, int, error) {
|
||||
return view.SearchProjectGrants(v.Db, grantedProjectTable, request)
|
||||
}
|
||||
|
||||
func (v *View) PutProjectGrant(project *model.ProjectGrantView) error {
|
||||
err := view.PutProjectGrant(v.Db, grantedProjectTable, project)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedProjectGrantSequence(project.Sequence)
|
||||
}
|
||||
|
||||
func (v *View) DeleteProjectGrant(grantID string, eventSequence uint64) error {
|
||||
err := view.DeleteProjectGrant(v.Db, grantedProjectTable, grantID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return v.ProcessedProjectGrantSequence(eventSequence)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestProjectGrantSequence() (uint64, error) {
|
||||
return v.latestSequence(grantedProjectTable)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedProjectGrantSequence(eventSequence uint64) error {
|
||||
return v.saveCurrentSequence(grantedProjectTable, eventSequence)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestProjectGrantFailedEvent(sequence uint64) (*global_view.FailedEvent, error) {
|
||||
return v.latestFailedEvent(grantedProjectTable, sequence)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedProjectGrantFailedEvent(failedEvent *global_view.FailedEvent) error {
|
||||
return v.saveFailedEvent(failedEvent)
|
||||
}
|
||||
@@ -12,8 +12,9 @@ type ProjectRepository interface {
|
||||
UpdateProject(ctx context.Context, project *model.Project) (*model.Project, error)
|
||||
DeactivateProject(ctx context.Context, id string) (*model.Project, error)
|
||||
ReactivateProject(ctx context.Context, id string) (*model.Project, error)
|
||||
SearchGrantedProjects(ctx context.Context, request *model.GrantedProjectSearchRequest) (*model.GrantedProjectSearchResponse, error)
|
||||
GetGrantedProjectGrantByIDs(ctx context.Context, projectID, grantID string) (*model.GrantedProjectView, error)
|
||||
SearchProjects(ctx context.Context, request *model.ProjectViewSearchRequest) (*model.ProjectViewSearchResponse, error)
|
||||
SearchProjectGrants(ctx context.Context, request *model.ProjectGrantViewSearchRequest) (*model.ProjectGrantViewSearchResponse, error)
|
||||
ProjectGrantViewByID(ctx context.Context, grantID string) (*model.ProjectGrantView, error)
|
||||
|
||||
ProjectMemberByID(ctx context.Context, projectID, userID string) (*model.ProjectMember, error)
|
||||
AddProjectMember(ctx context.Context, member *model.ProjectMember) (*model.ProjectMember, error)
|
||||
|
||||
Reference in New Issue
Block a user