diff --git a/.tools/nvim/__http__/auth/invite-code.graphql.yml b/.tools/nvim/__http__/auth/invite-code.graphql.yml new file mode 100644 index 000000000..e7d7a4774 --- /dev/null +++ b/.tools/nvim/__http__/auth/invite-code.graphql.yml @@ -0,0 +1,41 @@ +--- +label: "Create Invite code" +query: |+ + mutation Auth_createInviteCode($name: String!, $inviteCode: String!) { + auth_createInviteCode(name: $name, inviteCode: $inviteCode) { + id + name + inviteCode + } + } +variables: + { + "name": "kl-inv-test", + "inviteCode": "kl-simple-1234" + } + +--- + +label: "Delete Invite Code" +query: |+ + mutation Auth_deleteInviteCode($inviteCodeId: String!) { + auth_deleteInviteCode(inviteCodeId: $inviteCodeId) + } +variables: + { + "inviteCodeId": "null" + } + +--- + +label: "Verify Invite Code" +query: |+ + mutation Auth_verifyInviteCode($invitationCode: String!) { + auth_verifyInviteCode(invitationCode: $invitationCode) + } +variables: + { + "invitationCode": "kl-simple-1234" + } + +--- diff --git a/apps/auth/internal/app/app.go b/apps/auth/internal/app/app.go index 8bb3b8282..a73d828f4 100644 --- a/apps/auth/internal/app/app.go +++ b/apps/auth/internal/app/app.go @@ -30,6 +30,7 @@ var Module = fx.Module( repos.NewFxMongoRepo[*entities.User]("users", "usr", entities.UserIndexes), repos.NewFxMongoRepo[*entities.AccessToken]("access_tokens", "tkn", entities.AccessTokenIndexes), repos.NewFxMongoRepo[*entities.RemoteLogin]("remote_logins", "rlgn", entities.RemoteTokenIndexes), + repos.NewFxMongoRepo[*entities.InviteCode]("invite_codes", "invcode", entities.InviteCodeIndexes), fx.Provide( func(ev *env.Env, jc *nats.JetstreamClient) (kv.Repo[*entities.VerifyToken], error) { cxt := context.TODO() diff --git a/apps/auth/internal/app/graph/functions.go b/apps/auth/internal/app/graph/functions.go index 40579115b..98f4823b0 100644 --- a/apps/auth/internal/app/graph/functions.go +++ b/apps/auth/internal/app/graph/functions.go @@ -30,3 +30,11 @@ func userModelFromEntity(userEntity *entities.User) *model.User { ProviderGoogle: mapFromProviderDetail(userEntity.ProviderGoogle), } } + +func inviteCodeModelFromEntity(inviteCodeEntity *entities.InviteCode) *model.InviteCode { + return &model.InviteCode{ + ID: inviteCodeEntity.Id, + Name: inviteCodeEntity.Name, + InviteCode: inviteCodeEntity.InviteCode, + } +} diff --git a/apps/auth/internal/app/graph/generated/generated.go b/apps/auth/internal/app/graph/generated/generated.go index 9d87ef920..bb915ebd6 100644 --- a/apps/auth/internal/app/graph/generated/generated.go +++ b/apps/auth/internal/app/graph/generated/generated.go @@ -55,11 +55,19 @@ type ComplexityRoot struct { FindUserByID func(childComplexity int, id repos.ID) int } + InviteCode struct { + ID func(childComplexity int) int + InviteCode func(childComplexity int) int + Name func(childComplexity int) int + } + Mutation struct { AuthChangeEmail func(childComplexity int, email string) int AuthChangePassword func(childComplexity int, currentPassword string, newPassword string) int AuthClearMetadata func(childComplexity int) int + AuthCreateInviteCode func(childComplexity int, name string, inviteCode string) int AuthCreateRemoteLogin func(childComplexity int, secret *string) int + AuthDeleteInviteCode func(childComplexity int, inviteCodeID string) int AuthLogin func(childComplexity int, email string, password string) int AuthLogout func(childComplexity int) int AuthRequestResetPassword func(childComplexity int, email string) int @@ -69,6 +77,7 @@ type ComplexityRoot struct { AuthSetRemoteAuthHeader func(childComplexity int, loginID string, authHeader *string) int AuthSignup func(childComplexity int, name string, email string, password string) int AuthVerifyEmail func(childComplexity int, token string) int + AuthVerifyInviteCode func(childComplexity int, invitationCode string) int OAuthAddLogin func(childComplexity int, provider string, state string, code string) int OAuthLogin func(childComplexity int, provider string, code string, state *string) int } @@ -139,6 +148,9 @@ type MutationResolver interface { AuthChangeEmail(ctx context.Context, email string) (bool, error) AuthResendVerificationEmail(ctx context.Context) (bool, error) AuthChangePassword(ctx context.Context, currentPassword string, newPassword string) (bool, error) + AuthCreateInviteCode(ctx context.Context, name string, inviteCode string) (*model.InviteCode, error) + AuthDeleteInviteCode(ctx context.Context, inviteCodeID string) (bool, error) + AuthVerifyInviteCode(ctx context.Context, invitationCode string) (bool, error) } type QueryResolver interface { AuthMe(ctx context.Context) (*model.User, error) @@ -179,6 +191,27 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Entity.FindUserByID(childComplexity, args["id"].(repos.ID)), true + case "InviteCode.id": + if e.complexity.InviteCode.ID == nil { + break + } + + return e.complexity.InviteCode.ID(childComplexity), true + + case "InviteCode.inviteCode": + if e.complexity.InviteCode.InviteCode == nil { + break + } + + return e.complexity.InviteCode.InviteCode(childComplexity), true + + case "InviteCode.name": + if e.complexity.InviteCode.Name == nil { + break + } + + return e.complexity.InviteCode.Name(childComplexity), true + case "Mutation.auth_changeEmail": if e.complexity.Mutation.AuthChangeEmail == nil { break @@ -210,6 +243,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.AuthClearMetadata(childComplexity), true + case "Mutation.auth_createInviteCode": + if e.complexity.Mutation.AuthCreateInviteCode == nil { + break + } + + args, err := ec.field_Mutation_auth_createInviteCode_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.AuthCreateInviteCode(childComplexity, args["name"].(string), args["inviteCode"].(string)), true + case "Mutation.auth_createRemoteLogin": if e.complexity.Mutation.AuthCreateRemoteLogin == nil { break @@ -222,6 +267,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.AuthCreateRemoteLogin(childComplexity, args["secret"].(*string)), true + case "Mutation.auth_deleteInviteCode": + if e.complexity.Mutation.AuthDeleteInviteCode == nil { + break + } + + args, err := ec.field_Mutation_auth_deleteInviteCode_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.AuthDeleteInviteCode(childComplexity, args["inviteCodeId"].(string)), true + case "Mutation.auth_login": if e.complexity.Mutation.AuthLogin == nil { break @@ -320,6 +377,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.AuthVerifyEmail(childComplexity, args["token"].(string)), true + case "Mutation.auth_verifyInviteCode": + if e.complexity.Mutation.AuthVerifyInviteCode == nil { + break + } + + args, err := ec.field_Mutation_auth_verifyInviteCode_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.AuthVerifyInviteCode(childComplexity, args["invitationCode"].(string)), true + case "Mutation.oAuth_addLogin": if e.complexity.Mutation.OAuthAddLogin == nil { break @@ -712,6 +781,10 @@ type Mutation { auth_changeEmail(email: String!): Boolean! @isLoggedInAndVerified auth_resendVerificationEmail: Boolean! @isLoggedIn auth_changePassword(currentPassword: String!, newPassword: String!): Boolean! @isLoggedInAndVerified + + auth_createInviteCode(name: String!, inviteCode: String!): InviteCode! + auth_deleteInviteCode(inviteCodeId: String!): Boolean! + auth_verifyInviteCode(invitationCode: String!): Boolean! @isLoggedIn } type Session { @@ -722,6 +795,12 @@ type Session { userVerified: Boolean! } +type InviteCode { + id: ID! + name: String! + inviteCode: String! +} + type User @key(fields: "id") { id: ID! name: String! @@ -825,6 +904,30 @@ func (ec *executionContext) field_Mutation_auth_changePassword_args(ctx context. return args, nil } +func (ec *executionContext) field_Mutation_auth_createInviteCode_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 string + if tmp, ok := rawArgs["name"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) + arg0, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["name"] = arg0 + var arg1 string + if tmp, ok := rawArgs["inviteCode"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("inviteCode")) + arg1, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["inviteCode"] = arg1 + return args, nil +} + func (ec *executionContext) field_Mutation_auth_createRemoteLogin_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -840,6 +943,21 @@ func (ec *executionContext) field_Mutation_auth_createRemoteLogin_args(ctx conte return args, nil } +func (ec *executionContext) field_Mutation_auth_deleteInviteCode_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 string + if tmp, ok := rawArgs["inviteCodeId"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("inviteCodeId")) + arg0, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["inviteCodeId"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_auth_login_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -990,6 +1108,21 @@ func (ec *executionContext) field_Mutation_auth_verifyEmail_args(ctx context.Con return args, nil } +func (ec *executionContext) field_Mutation_auth_verifyInviteCode_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 string + if tmp, ok := rawArgs["invitationCode"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("invitationCode")) + arg0, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["invitationCode"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_oAuth_addLogin_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -1266,6 +1399,138 @@ func (ec *executionContext) fieldContext_Entity_findUserByID(ctx context.Context return fc, nil } +func (ec *executionContext) _InviteCode_id(ctx context.Context, field graphql.CollectedField, obj *model.InviteCode) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_InviteCode_id(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(repos.ID) + fc.Result = res + return ec.marshalNID2githubᚗcomᚋkloudliteᚋapiᚋpkgᚋreposᚐID(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_InviteCode_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "InviteCode", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type ID does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _InviteCode_name(ctx context.Context, field graphql.CollectedField, obj *model.InviteCode) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_InviteCode_name(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Name, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_InviteCode_name(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "InviteCode", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _InviteCode_inviteCode(ctx context.Context, field graphql.CollectedField, obj *model.InviteCode) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_InviteCode_inviteCode(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.InviteCode, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_InviteCode_inviteCode(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "InviteCode", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Mutation_auth_setRemoteAuthHeader(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Mutation_auth_setRemoteAuthHeader(ctx, field) if err != nil { @@ -2268,6 +2533,199 @@ func (ec *executionContext) fieldContext_Mutation_auth_changePassword(ctx contex return fc, nil } +func (ec *executionContext) _Mutation_auth_createInviteCode(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_auth_createInviteCode(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().AuthCreateInviteCode(rctx, fc.Args["name"].(string), fc.Args["inviteCode"].(string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*model.InviteCode) + fc.Result = res + return ec.marshalNInviteCode2ᚖgithubᚗcomᚋkloudliteᚋapiᚋappsᚋauthᚋinternalᚋappᚋgraphᚋmodelᚐInviteCode(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_auth_createInviteCode(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_InviteCode_id(ctx, field) + case "name": + return ec.fieldContext_InviteCode_name(ctx, field) + case "inviteCode": + return ec.fieldContext_InviteCode_inviteCode(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type InviteCode", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_auth_createInviteCode_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_auth_deleteInviteCode(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_auth_deleteInviteCode(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().AuthDeleteInviteCode(rctx, fc.Args["inviteCodeId"].(string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_auth_deleteInviteCode(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_auth_deleteInviteCode_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_auth_verifyInviteCode(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_auth_verifyInviteCode(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().AuthVerifyInviteCode(rctx, fc.Args["invitationCode"].(string)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.IsLoggedIn == nil { + return nil, errors.New("directive isLoggedIn is not implemented") + } + return ec.directives.IsLoggedIn(ctx, nil, directive0) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(bool); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be bool`, tmp) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_auth_verifyInviteCode(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_auth_verifyInviteCode_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _OAuthProviderStatus_provider(ctx context.Context, field graphql.CollectedField, obj *model.OAuthProviderStatus) (ret graphql.Marshaler) { fc, err := ec.fieldContext_OAuthProviderStatus_provider(ctx, field) if err != nil { @@ -5585,6 +6043,55 @@ func (ec *executionContext) _Entity(ctx context.Context, sel ast.SelectionSet) g return out } +var inviteCodeImplementors = []string{"InviteCode"} + +func (ec *executionContext) _InviteCode(ctx context.Context, sel ast.SelectionSet, obj *model.InviteCode) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, inviteCodeImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("InviteCode") + case "id": + out.Values[i] = ec._InviteCode_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "name": + out.Values[i] = ec._InviteCode_name(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "inviteCode": + out.Values[i] = ec._InviteCode_inviteCode(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var mutationImplementors = []string{"Mutation"} func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { @@ -5703,6 +6210,27 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } + case "auth_createInviteCode": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_auth_createInviteCode(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "auth_deleteInviteCode": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_auth_deleteInviteCode(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "auth_verifyInviteCode": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_auth_verifyInviteCode(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -6544,6 +7072,20 @@ func (ec *executionContext) marshalNID2githubᚗcomᚋkloudliteᚋapiᚋpkgᚋre return res } +func (ec *executionContext) marshalNInviteCode2githubᚗcomᚋkloudliteᚋapiᚋappsᚋauthᚋinternalᚋappᚋgraphᚋmodelᚐInviteCode(ctx context.Context, sel ast.SelectionSet, v model.InviteCode) graphql.Marshaler { + return ec._InviteCode(ctx, sel, &v) +} + +func (ec *executionContext) marshalNInviteCode2ᚖgithubᚗcomᚋkloudliteᚋapiᚋappsᚋauthᚋinternalᚋappᚋgraphᚋmodelᚐInviteCode(ctx context.Context, sel ast.SelectionSet, v *model.InviteCode) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._InviteCode(ctx, sel, v) +} + func (ec *executionContext) unmarshalNJson2map(ctx context.Context, v interface{}) (map[string]interface{}, error) { res, err := graphql.UnmarshalMap(v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/apps/auth/internal/app/graph/model/models_gen.go b/apps/auth/internal/app/graph/model/models_gen.go index c7ac1de43..3f425364b 100644 --- a/apps/auth/internal/app/graph/model/models_gen.go +++ b/apps/auth/internal/app/graph/model/models_gen.go @@ -6,6 +6,12 @@ import ( "github.com/kloudlite/api/pkg/repos" ) +type InviteCode struct { + ID repos.ID `json:"id"` + Name string `json:"name"` + InviteCode string `json:"inviteCode"` +} + type OAuthProviderStatus struct { Provider string `json:"provider"` Enabled bool `json:"enabled"` diff --git a/apps/auth/internal/app/graph/schema.graphqls b/apps/auth/internal/app/graph/schema.graphqls index 70af7ae00..d0f396adf 100644 --- a/apps/auth/internal/app/graph/schema.graphqls +++ b/apps/auth/internal/app/graph/schema.graphqls @@ -46,6 +46,10 @@ type Mutation { auth_changeEmail(email: String!): Boolean! @isLoggedInAndVerified auth_resendVerificationEmail: Boolean! @isLoggedIn auth_changePassword(currentPassword: String!, newPassword: String!): Boolean! @isLoggedInAndVerified + + auth_createInviteCode(name: String!, inviteCode: String!): InviteCode! + auth_deleteInviteCode(inviteCodeId: String!): Boolean! + auth_verifyInviteCode(invitationCode: String!): Boolean! @isLoggedIn } type Session { @@ -56,6 +60,12 @@ type Session { userVerified: Boolean! } +type InviteCode { + id: ID! + name: String! + inviteCode: String! +} + type User @key(fields: "id") { id: ID! name: String! diff --git a/apps/auth/internal/app/graph/schema.resolvers.go b/apps/auth/internal/app/graph/schema.resolvers.go index cbe83e93e..6978d0ab6 100644 --- a/apps/auth/internal/app/graph/schema.resolvers.go +++ b/apps/auth/internal/app/graph/schema.resolvers.go @@ -6,10 +6,11 @@ package graph import ( "context" + "github.com/kloudlite/api/pkg/errors" + "github.com/kloudlite/api/apps/auth/internal/app/graph/generated" "github.com/kloudlite/api/apps/auth/internal/app/graph/model" "github.com/kloudlite/api/common" - "github.com/kloudlite/api/pkg/errors" fn "github.com/kloudlite/api/pkg/functions" httpServer "github.com/kloudlite/api/pkg/http-server" "github.com/kloudlite/api/pkg/repos" @@ -182,6 +183,33 @@ func (r *mutationResolver) AuthChangePassword(ctx context.Context, currentPasswo return r.d.ChangePassword(ctx, sess.UserId, currentPassword, newPassword) } +// AuthCreateInviteCode is the resolver for the auth_createInviteCode field. +func (r *mutationResolver) AuthCreateInviteCode(ctx context.Context, name string, inviteCode string) (*model.InviteCode, error) { + inv, err := r.d.CreateInviteCode(ctx, name, inviteCode) + if err != nil { + return nil, errors.NewE(err) + } + return inviteCodeModelFromEntity(inv), nil +} + +// AuthDeleteInviteCode is the resolver for the auth_deleteInviteCode field. +func (r *mutationResolver) AuthDeleteInviteCode(ctx context.Context, inviteCodeID string) (bool, error) { + if err := r.d.DeleteInviteCode(ctx, inviteCodeID); err != nil { + return false, errors.NewE(err) + } + return true, nil +} + +// AuthVerifyInviteCode is the resolver for the auth_verifyInviteCode field. +func (r *mutationResolver) AuthVerifyInviteCode(ctx context.Context, invitationCode string) (bool, error) { + sess, err := GetUserSession(ctx) + if err != nil { + return false, errors.NewEf(err, "while getting user session") + } + + return r.d.VerifyInviteCode(ctx, sess.UserId, invitationCode) +} + // AuthMe is the resolver for the auth_me field. func (r *queryResolver) AuthMe(ctx context.Context) (*model.User, error) { sess, err := GetUserSession(ctx) diff --git a/apps/auth/internal/domain/domain.go b/apps/auth/internal/domain/domain.go index 973c0a857..7419a7bab 100644 --- a/apps/auth/internal/domain/domain.go +++ b/apps/auth/internal/domain/domain.go @@ -32,6 +32,16 @@ type Domain interface { OauthRequestLogin(ctx context.Context, provider string, state string) (string, error) OauthLogin(ctx context.Context, provider string, state string, code string) (*common.AuthSession, error) OauthAddLogin(ctx context.Context, userId repos.ID, provider string, state string, code string) (bool, error) + + /// Invite code + //ListInviteCodes(ctx context.Context) ([]*entities.InviteCode, error) + //GetInviteCode(ctx context.Context, name string) (*entities.InviteCode, error) + + CreateInviteCode(ctx context.Context, name string, inviteCode string) (*entities.InviteCode, error) + DeleteInviteCode(ctx context.Context, invCodeId string) error + //UpdateInviteCode(ctx context.Context, invCode entities.InviteCode) (*entities.InviteCode, error) + + VerifyInviteCode(ctx context.Context, userId repos.ID, invitationCode string) (bool, error) } type Messenger interface { diff --git a/apps/auth/internal/domain/impl.go b/apps/auth/internal/domain/impl.go index 15385b58d..0d12e3c09 100644 --- a/apps/auth/internal/domain/impl.go +++ b/apps/auth/internal/domain/impl.go @@ -8,6 +8,7 @@ import ( "encoding/json" "fmt" "github.com/kloudlite/api/apps/auth/internal/entities" + "github.com/kloudlite/api/apps/auth/internal/env" "strings" "time" @@ -48,11 +49,14 @@ type domainI struct { commsClient comms.CommsClient verifyTokenRepo kv.Repo[*entities.VerifyToken] resetTokenRepo kv.Repo[*entities.ResetPasswordToken] + inviteCodeRepo repos.DbRepo[*entities.InviteCode] logger logging.Logger github Github gitlab Gitlab google Google remoteLoginRepo repos.DbRepo[*entities.RemoteLogin] + + envVars *env.Env } func (d *domainI) SetRemoteLoginAuthHeader(ctx context.Context, loginId repos.ID, authHeader string) error { @@ -196,6 +200,7 @@ func (d *domainI) SignUp(ctx context.Context, name string, email string, passwor Email: email, Password: hex.EncodeToString(sum[:]), Verified: false, + Approved: false, Metadata: nil, Joined: time.Now(), PasswordSalt: salt, @@ -678,11 +683,13 @@ func fxDomain( remoteLoginRepo repos.DbRepo[*entities.RemoteLogin], verifyTokenRepo kv.Repo[*entities.VerifyToken], resetTokenRepo kv.Repo[*entities.ResetPasswordToken], + inviteCodeRepo repos.DbRepo[*entities.InviteCode], github Github, gitlab Gitlab, google Google, logger logging.Logger, commsClient comms.CommsClient, + ev *env.Env, ) Domain { return &domainI{ remoteLoginRepo: remoteLoginRepo, @@ -691,9 +698,11 @@ func fxDomain( accessTokenRepo: accessTokenRepo, verifyTokenRepo: verifyTokenRepo, resetTokenRepo: resetTokenRepo, + inviteCodeRepo: inviteCodeRepo, github: github, gitlab: gitlab, google: google, logger: logger, + envVars: ev, } } diff --git a/apps/auth/internal/domain/invite-code.go b/apps/auth/internal/domain/invite-code.go new file mode 100644 index 000000000..f9704fc39 --- /dev/null +++ b/apps/auth/internal/domain/invite-code.go @@ -0,0 +1,66 @@ +package domain + +import ( + "context" + "github.com/kloudlite/api/apps/auth/internal/entities" + "github.com/kloudlite/api/pkg/errors" + "github.com/kloudlite/api/pkg/repos" +) + +func (d *domainI) findInviteCodeData(ctx context.Context, inviteCode string) (*entities.InviteCode, error) { + inv, err := d.inviteCodeRepo.FindOne(ctx, repos.Filter{ + "inviteCode": inviteCode, + }) + if err != nil { + return nil, errors.NewE(err) + } + if inv == nil { + return nil, errors.Newf("no invite with code=%q found", inviteCode) + } + return inv, nil +} + +func (d *domainI) CreateInviteCode(ctx context.Context, name string, inviteCode string) (*entities.InviteCode, error) { + inv, err := d.inviteCodeRepo.Create(ctx, &entities.InviteCode{ + Name: name, + InviteCode: inviteCode, + }) + if err != nil { + return nil, errors.NewE(err) + } + + return inv, nil +} + +func (d *domainI) DeleteInviteCode(ctx context.Context, invCodeId string) error { + err := d.inviteCodeRepo.DeleteOne( + ctx, + repos.Filter{"id": invCodeId}, + ) + if err != nil { + return errors.NewE(err) + } + return nil +} + +func (d *domainI) VerifyInviteCode(ctx context.Context, userId repos.ID, invitationCode string) (bool, error) { + user, err := d.userRepo.FindById(ctx, userId) + if err != nil { + return false, errors.NewE(err) + } + + inv, err := d.findInviteCodeData(ctx, invitationCode) + if err != nil { + return false, errors.NewE(err) + } + + if inv.InviteCode == invitationCode { + user.Approved = true + } + + user, err = d.userRepo.UpdateById(ctx, userId, user) + if err != nil { + return false, errors.NewE(err) + } + return true, nil +} diff --git a/apps/auth/internal/entities/entities.go b/apps/auth/internal/entities/entities.go index 719a1e700..73e3cf1c5 100644 --- a/apps/auth/internal/entities/entities.go +++ b/apps/auth/internal/entities/entities.go @@ -45,6 +45,7 @@ type User struct { Metadata UserMetadata `json:"metadata"` Joined time.Time `json:"joined"` PasswordSalt string `json:"password_salt" graphql:"ignore"` + Approved bool `json:"approved" graphql:"noinput"` } var UserIndexes = []repos.IndexField{ @@ -126,3 +127,18 @@ var RemoteTokenIndexes = []repos.IndexField{ Unique: true, }, } + +type InviteCode struct { + repos.BaseEntity `bson:",inline"` + Name string `json:"name"` + InviteCode string `json:"inviteCode"` +} + +var InviteCodeIndexes = []repos.IndexField{ + { + Field: []repos.IndexKey{ + {Key: "id", Value: repos.IndexAsc}, + }, + Unique: true, + }, +} diff --git a/go.sum b/go.sum index acfd5b569..576244309 100644 --- a/go.sum +++ b/go.sum @@ -161,8 +161,6 @@ github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLA github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/kloudlite/container-registry-authorizer v0.0.0-20231021122509-161dc30fde55 h1:YnZh3TL6AG4EfoInx1/L5zcPHd2QxgLKseJB1KtHjdQ= github.com/kloudlite/container-registry-authorizer v0.0.0-20231021122509-161dc30fde55/go.mod h1:GZj3wZmIw/qCciclRhgQTgmGiqe8wxoVzMXQjbOfnbc= -github.com/kloudlite/operator v1.0.4-0.20240520060449-53a58ea322db h1:uJlT4QTPZ5QxL61GebHUwu9HDjjcJb5+Xy57cg9yoso= -github.com/kloudlite/operator v1.0.4-0.20240520060449-53a58ea322db/go.mod h1:CxZ24OQx30my/xzxOnjQLmUCcEvVhzAuYtGblRhXnhg= github.com/kloudlite/operator v1.0.4-0.20240524130112-c32c133c28cc h1:/A6XGjylgXUyCPq0Yl7PlxOT62YOK46Lu7PHiRUwMqc= github.com/kloudlite/operator v1.0.4-0.20240524130112-c32c133c28cc/go.mod h1:sz3ByFoE3ngJC+ai+BZLP5GAfoeLmgkyBLMEcWv7WcI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=