Rewrite identity and access management

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2025-12-03 19:23:15 +01:00
parent 4ed3f5a067
commit 74fc3b8cd1
201 changed files with 32895 additions and 23649 deletions

View File

@@ -0,0 +1,94 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package connect_v1
import (
"context"
"errors"
"fmt"
"net/http"
"go.gearno.de/kit/httpserver"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/securetoken"
)
var (
apiKeyContextKey = &ctxKey{name: "api_key"}
)
func APIKeyFromContext(ctx context.Context) *coredata.UserAPIKey {
apiKey, _ := ctx.Value(apiKeyContextKey).(*coredata.UserAPIKey)
return apiKey
}
func NewAPIKeyMiddleware(svc *iam.Service) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
session := SessionFromContext(ctx)
if session != nil {
httpserver.RenderError(w, http.StatusBadRequest, errors.New("api key authentication cannot be used with session authentication"))
return
}
tokenValue, err := securetoken.Get(r, "")
if err != nil {
next.ServeHTTP(w, r)
return
}
keyID, err := gid.ParseGID(tokenValue)
if err != nil {
next.ServeHTTP(w, r)
return
}
apiKey, err := svc.APIKeyService.GetAPIKey(ctx, keyID)
if err != nil {
var errUserAPIKeyNotFound *iam.ErrUserAPIKeyNotFound
var errUserAPIKeyExpired *iam.ErrUserAPIKeyExpired
if errors.As(err, &errUserAPIKeyNotFound) || errors.As(err, &errUserAPIKeyExpired) {
next.ServeHTTP(w, r)
return
}
panic(fmt.Errorf("cannot get user API key: %w", err))
}
user, err := svc.AccountService.GetIdentity(ctx, apiKey.UserID)
if err != nil {
var errUserNotFound *iam.ErrUserNotFound
if errors.As(err, &errUserNotFound) {
next.ServeHTTP(w, r)
return
}
panic(fmt.Errorf("cannot get user: %w", err))
}
ctx = context.WithValue(ctx, apiKeyContextKey, apiKey)
ctx = context.WithValue(ctx, identityContextKey, user)
next.ServeHTTP(w, r.WithContext(ctx))
},
)
}
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package connect_v1
type (
ctxKey struct{ name string }
)

View File

@@ -0,0 +1,36 @@
schema: ["schema.graphql"]
exec:
filename: "schema/schema.go"
package: "schema"
model:
filename: "types/types.go"
package: "types"
resolver:
layout: "follow-schema"
dir: "."
package: "connect_v1"
filename_template: "v1_resolver.go"
autobind: []
call_argument_directives_with_null: true
directives:
mustBeAuthorized:
skip_runtime: false
models:
ID:
model:
- "go.probo.inc/probo/pkg/server/gqlutils/types/gid.GIDScalar"
Datetime:
model:
- "github.com/99designs/gqlgen/graphql.Time"
CursorKey:
model:
- "go.probo.inc/probo/pkg/server/gqlutils/types/cursor.CursorKeyScalar"
EmailAddr:
model:
- "go.probo.inc/probo/pkg/server/gqlutils/types/mail.AddrScalar"

View File

@@ -0,0 +1,102 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package connect_v1
import (
"context"
"fmt"
"net/http"
"github.com/99designs/gqlgen/graphql"
"github.com/vektah/gqlparser/v2/gqlerror"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/securecookie"
"go.probo.inc/probo/pkg/server/api/connect/v1/schema"
"go.probo.inc/probo/pkg/server/api/connect/v1/types"
"go.probo.inc/probo/pkg/server/gqlutils"
)
var (
ErrForbidden = &gqlerror.Error{
Message: "You are not authorized to access this resource",
Extensions: map[string]any{
"code": "FORBIDDEN",
},
}
ErrUnauthorized = &gqlerror.Error{
Message: "You are not authorized to access this resource",
Extensions: map[string]any{
"code": "UNAUTHORIZED",
},
}
ErrAlreadyAuthenticated = &gqlerror.Error{
Message: "authentication not allowed for this resource/action",
Extensions: map[string]any{
"code": "ALREADY_AUTHENTICATED",
},
}
)
func SessionDirective(ctx context.Context, obj any, next graphql.Resolver, required types.SessionRequirement) (any, error) {
session := SessionFromContext(ctx)
switch required {
case types.SessionRequirementOptional:
case types.SessionRequirementPresent:
if session == nil {
return nil, ErrUnauthorized
}
case types.SessionRequirementNone:
if session != nil {
return nil, ErrAlreadyAuthenticated
}
}
return next(ctx)
}
func IsViewerDirective(ctx context.Context, obj any, next graphql.Resolver) (any, error) {
identity := UserFromContext(ctx)
resolvedIdentity, ok := obj.(*types.Identity)
if !ok {
panic(fmt.Errorf("@isViewer called on non-identity object: %T", obj))
}
if identity.ID != resolvedIdentity.ID {
return nil, ErrForbidden
}
return next(ctx)
}
func NewGraphQLHandler(svc *iam.Service, logger *log.Logger, cookieConfig securecookie.Config) http.Handler {
config := schema.Config{
Resolvers: &Resolver{
iam: svc,
cookieConfig: cookieConfig,
},
Directives: schema.DirectiveRoot{
Session: SessionDirective,
IsViewer: IsViewerDirective,
},
}
es := schema.NewExecutableSchema(config)
gqlh := gqlutils.NewHandler(es, logger)
return gqlh
}

View File

@@ -0,0 +1,51 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package connect_v1
import (
"context"
"net/http"
)
var (
httpResponseWriterKey = &ctxKey{name: "http_response_writer"}
httpRequestKey = &ctxKey{name: "http_request"}
)
func HTTPContextMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
ctx := WithHTTPContext(r.Context(), w, r)
next.ServeHTTP(w, r.WithContext(ctx))
},
)
}
func WithHTTPContext(ctx context.Context, w http.ResponseWriter, r *http.Request) context.Context {
ctx = context.WithValue(ctx, httpResponseWriterKey, w)
ctx = context.WithValue(ctx, httpRequestKey, r)
return ctx
}
func HTTPResponseWriterFromContext(ctx context.Context) http.ResponseWriter {
return ctx.Value(httpResponseWriterKey).(http.ResponseWriter)
}
func HTTPRequestFromContext(ctx context.Context) *http.Request {
return ctx.Value(httpRequestKey).(*http.Request)
}

View File

@@ -0,0 +1,66 @@
//go:generate go run github.com/99designs/gqlgen generate
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package connect_v1
import (
"time"
"github.com/go-chi/chi/v5"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/baseurl"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/securecookie"
)
type (
Resolver struct {
iam *iam.Service
cookieConfig securecookie.Config
}
)
func (r *Resolver) sessionCookieConfig(maxAge time.Duration) securecookie.Config {
return securecookie.Config{
Name: r.cookieConfig.Name,
Secret: r.cookieConfig.Secret,
Secure: r.cookieConfig.Secure,
HTTPOnly: r.cookieConfig.HTTPOnly,
SameSite: r.cookieConfig.SameSite,
Path: r.cookieConfig.Path,
Domain: r.cookieConfig.Domain,
MaxAge: int(maxAge.Seconds()),
}
}
func NewMux(logger *log.Logger, svc *iam.Service, cookieConfig securecookie.Config, baseURL *baseurl.BaseURL) *chi.Mux {
r := chi.NewMux()
r.Use(HTTPContextMiddleware)
sessionMiddleware := NewSessionMiddleware(svc, cookieConfig)
graphqlHandler := NewGraphQLHandler(svc, logger, cookieConfig)
samlHandler := NewSAMLHandler(svc, cookieConfig, baseURL)
router := r.With(sessionMiddleware)
router.Handle("/graphql", graphqlHandler)
router.Get("/saml/2.0/metadata", samlHandler.MetadataHandler)
router.Post("/saml/2.0/consume", samlHandler.ConsumeHandler)
router.Get("/saml/2.0/{samlConfigID}", samlHandler.LoginHandler)
return r
}

View File

@@ -0,0 +1,96 @@
package connect_v1
import (
"errors"
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"go.gearno.de/kit/httpserver"
"go.probo.inc/probo/pkg/baseurl"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/securecookie"
)
type SAMLHandler struct {
iam *iam.Service
cookieConfig securecookie.Config
baseURL *baseurl.BaseURL
}
func NewSAMLHandler(iam *iam.Service, cookieConfig securecookie.Config, baseURL *baseurl.BaseURL) *SAMLHandler {
return &SAMLHandler{iam: iam, cookieConfig: cookieConfig, baseURL: baseURL}
}
func (h *SAMLHandler) MetadataHandler(w http.ResponseWriter, r *http.Request) {
metadataXML, err := h.iam.SAMLService.GenerateSpMetadata()
if err != nil {
panic(fmt.Errorf("cannot generate metadata: %w", err))
}
w.Header().Set("Content-Type", "application/samlmetadata+xml")
w.WriteHeader(http.StatusOK)
w.Write(metadataXML)
}
func (h *SAMLHandler) ConsumeHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
err := r.ParseForm()
if err != nil {
httpserver.RenderError(w, http.StatusBadRequest, errors.New("cannot parse form"))
return
}
samlResponse := r.FormValue("SAMLResponse")
relayState := r.FormValue("RelayState")
configID, err := gid.ParseGID(relayState)
if err != nil {
httpserver.RenderError(w, http.StatusBadRequest, errors.New("invalid relay state"))
return
}
user, membership, err := h.iam.SAMLService.HandleAssertion(ctx, samlResponse, configID)
if err != nil {
httpserver.RenderError(w, http.StatusUnauthorized, err)
return
}
session := SessionFromContext(ctx)
if session == nil {
h.iam.AuthService.OpenSessionWithoutPassword(ctx, user.ID, membership.OrganizationID)
}
// TODO open or update the organization session
securecookie.Set(w, h.cookieConfig, session.ID.String())
redirectURL := h.baseURL.WithPath("/organizations/" + membership.OrganizationID.String()).MustString()
http.Redirect(w, r, redirectURL, http.StatusFound)
}
func (h *SAMLHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
samlConfigIDParam := chi.URLParam(r, "samlConfigID")
if samlConfigIDParam == "" {
httpserver.RenderError(w, http.StatusBadRequest, errors.New("missing SAML config ID"))
return
}
samlConfigID, err := gid.ParseGID(samlConfigIDParam)
if err != nil {
httpserver.RenderError(w, http.StatusBadRequest, errors.New("invalid SAML config ID"))
return
}
url, err := h.iam.SAMLService.InitiateLogin(ctx, samlConfigID)
if err != nil {
panic(fmt.Errorf("cannot initiate SAML login: %w", err))
}
http.Redirect(w, r, url.String(), http.StatusFound)
}

View File

@@ -0,0 +1,769 @@
directive @goField(
forceResolver: Boolean
name: String
omittable: Boolean
) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION
directive @goModel(
model: String
models: [String!]
) on OBJECT | INPUT_OBJECT | SCALAR | ENUM | INTERFACE | UNION
directive @goEnum(value: String) on ENUM_VALUE
directive @session(required: SessionRequirement!) on FIELD_DEFINITION
directive @isViewer on FIELD_DEFINITION
scalar CursorKey
scalar Datetime
scalar Upload
scalar EmailAddr
enum SessionRequirement {
PRESENT
NONE
OPTIONAL
}
enum OrderDirection
@goModel(model: "go.probo.inc/probo/pkg/page.OrderDirection") {
ASC @goEnum(value: "go.probo.inc/probo/pkg/page.OrderDirectionAsc")
DESC @goEnum(value: "go.probo.inc/probo/pkg/page.OrderDirectionDesc")
}
enum SessionOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.SessionOrderField") {
CREATED_AT
@goEnum(value: "go.probo.inc/probo/pkg/coredata.SessionOrderFieldCreatedAt")
EXPIRED_AT
@goEnum(value: "go.probo.inc/probo/pkg/coredata.SessionOrderFieldExpiredAt")
UPDATED_AT
@goEnum(value: "go.probo.inc/probo/pkg/coredata.SessionOrderFieldUpdatedAt")
}
input SessionOrder {
direction: OrderDirection!
field: SessionOrderField!
}
interface Node {
id: ID!
}
type Query {
node(id: ID!): Node @session(required: PRESENT)
viewer: Identity @session(required: PRESENT)
checkSSOAvailability(email: String!): SSOAvailability!
@session(required: NONE)
}
type Mutation {
signIn(input: SignInInput!): SignInPayload! @session(required: NONE)
signUp(input: SignUpInput!): SignUpPayload! @session(required: NONE)
signOut: SignOutPayload! @session(required: PRESENT)
signUpFromInvitation(
input: SignUpFromInvitationInput!
): SignUpFromInvitationPayload! @session(required: NONE)
forgotPassword(input: ForgotPasswordInput!): ForgotPasswordPayload!
@session(required: NONE)
resetPassword(input: ResetPasswordInput!): ResetPasswordPayload!
@session(required: NONE)
verifyEmail(input: VerifyEmailInput!): VerifyEmailPayload!
@session(required: OPTIONAL)
changePassword(input: ChangePasswordInput!): ChangePasswordPayload!
@session(required: PRESENT)
changeEmail(input: ChangeEmailInput!): ChangeEmailPayload!
@session(required: PRESENT)
updateIdentityProfile(
input: UpdateIdentityProfileInput!
): UpdateIdentityProfilePayload! @session(required: PRESENT)
revokeSession(input: RevokeSessionInput!): RevokeSessionPayload!
@session(required: PRESENT)
revokeAllSessions: RevokeAllSessionsPayload! @session(required: PRESENT)
createPersonalAPIKey(
input: CreatePersonalAPIKeyInput!
): CreatePersonalAPIKeyPayload! @session(required: PRESENT)
updatePersonalAPIKey(
input: UpdatePersonalAPIKeyInput!
): UpdatePersonalAPIKeyPayload! @session(required: PRESENT)
revokePersonalAPIKey(
input: RevokePersonalAPIKeyInput!
): RevokePersonalAPIKeyPayload! @session(required: PRESENT)
createOrganization(
input: CreateOrganizationInput!
): CreateOrganizationPayload! @session(required: PRESENT)
updateOrganization(
input: UpdateOrganizationInput!
): UpdateOrganizationPayload! @session(required: PRESENT)
deleteOrganization(
input: DeleteOrganizationInput!
): DeleteOrganizationPayload! @session(required: PRESENT)
inviteMember(input: InviteMemberInput!): InviteMemberPayload!
@session(required: PRESENT)
deleteInvitation(input: DeleteInvitationInput!): DeleteInvitationPayload!
@session(required: PRESENT)
removeMember(input: RemoveMemberInput!): RemoveMemberPayload!
@session(required: PRESENT)
acceptInvitation(input: AcceptInvitationInput!): AcceptInvitationPayload!
@session(required: PRESENT)
createSAMLConfiguration(
input: CreateSAMLConfigurationInput!
): CreateSAMLConfigurationPayload! @session(required: PRESENT)
updateSAMLConfiguration(
input: UpdateSAMLConfigurationInput!
): UpdateSAMLConfigurationPayload! @session(required: PRESENT)
deleteSAMLConfiguration(
input: DeleteSAMLConfigurationInput!
): DeleteSAMLConfigurationPayload! @session(required: PRESENT)
}
type Identity implements Node {
id: ID!
email: EmailAddr!
emailVerified: Boolean!
createdAt: Datetime!
updatedAt: Datetime!
memberships(
first: Int
after: CursorKey
last: Int
before: CursorKey
): MembershipConnection! @goField(forceResolver: true) @isViewer
pendingInvitations(
first: Int
after: CursorKey
last: Int
before: CursorKey
): InvitationConnection! @goField(forceResolver: true) @isViewer
sessions(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: SessionOrder
): SessionConnection! @goField(forceResolver: true) @isViewer
personalAPIKeys(
first: Int
after: CursorKey
last: Int
before: CursorKey
): PersonalAPIKeyConnection! @goField(forceResolver: true) @isViewer
profileFor(organizationId: ID!): IdentityProfile @isViewer
}
type IdentityProfile implements Node {
id: ID!
displayName: String!
firstName: String
lastName: String
jobTitle: String
department: String
phoneNumber: String
avatarUrl: String
manager: IdentityProfile
timezone: String
locale: String
customAttributes: [CustomAttribute!]!
provisionedBy: ProvisioningSource!
externalId: String
identity: Identity!
organization: Organization!
createdAt: Datetime!
updatedAt: Datetime!
}
type CustomAttribute {
key: String!
value: String!
}
type Organization implements Node {
id: ID!
name: String!
logoUrl: String @goField(forceResolver: true)
horizontalLogoUrl: String @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
members(
first: Int
after: CursorKey
last: Int
before: CursorKey
): MembershipConnection! @goField(forceResolver: true)
invitations(
first: Int
after: CursorKey
last: Int
before: CursorKey
status: InvitationStatus
): InvitationConnection! @goField(forceResolver: true)
samlConfigurations(
first: Int
after: CursorKey
last: Int
before: CursorKey
): SAMLConfigurationConnection! @goField(forceResolver: true)
availableApplications: [Application!]!
}
type Membership implements Node {
id: ID!
createdAt: Datetime!
profile: IdentityProfile!
identity: Identity! @goField(forceResolver: true)
organization: Organization! @goField(forceResolver: true)
permissions: [Permission!]!
provisionedBy: ProvisioningSource!
active: Boolean!
lastSyncedAt: Datetime
}
type Invitation implements Node {
id: ID!
email: EmailAddr!
expiresAt: Datetime!
acceptedAt: Datetime
createdAt: Datetime!
status: InvitationStatus!
}
type InvitationProfile {
displayName: String!
firstName: String
lastName: String
jobTitle: String
department: String
}
type Session implements Node {
id: ID!
ipAddress: String!
userAgent: String!
updatedAt: Datetime!
createdAt: Datetime!
expiresAt: Datetime!
}
type PersonalAPIKey implements Node {
id: ID!
name: String!
lastUsedAt: Datetime
expiresAt: Datetime!
createdAt: Datetime!
scopes: [TokenScope!]!
organizations: [Organization!]!
}
type Permission implements Node {
id: ID!
createdAt: Datetime!
application: Application!
accessLevel: AccessLevel!
organization: Organization!
principalType: PrincipalType!
principalId: ID!
}
type PermissionGrant {
application: Application!
accessLevel: AccessLevel!
}
type Application {
id: ApplicationId!
name: String!
description: String!
availableAccessLevels: [AccessLevel!]!
}
type SessionPolicy {
maxSessionDurationHours: Int!
idleTimeoutMinutes: Int!
maxConcurrentSessions: Int
requireReauthForSensitiveActions: Boolean!
}
type SAMLConfiguration implements Node {
id: ID!
emailDomain: String!
enabled: Boolean!
enforcementPolicy: SAMLEnforcementPolicy!
domainVerified: Boolean!
domainVerifiedAt: Datetime
domainVerificationToken: String
idpEntityId: String!
idpSsoUrl: String!
idpCertificate: String!
autoSignupEnabled: Boolean!
createdAt: Datetime!
updatedAt: Datetime!
spMetadataUrl: String!
testLoginUrl: String!
attributeMappings: SAMLAttributeMappings!
defaultPermissions: [PermissionGrant!]!
}
type SAMLAttributeMappings {
email: String!
firstName: String!
lastName: String!
role: String!
}
type SSOAvailability {
available: Boolean!
samlConfigId: ID
organizationId: ID
}
enum ApplicationId {
CONSOLE
COMPLIANCE
RISK
VENDOR
DOCUMENTS
TRUST_CENTER
SETTINGS
API
}
enum AccessLevel {
READ
WRITE
ADMIN
}
enum PrincipalType {
IDENTITY
SERVICE_ACCOUNT
}
enum InvitationStatus
@goModel(model: "go.probo.inc/probo/pkg/coredata.InvitationStatus") {
PENDING
@goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationStatusPending")
ACCEPTED
@goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationStatusAccepted")
EXPIRED
@goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationStatusExpired")
}
enum SAMLEnforcementPolicy
@goModel(model: "go.probo.inc/probo/pkg/coredata.SAMLEnforcementPolicy") {
OFF @goEnum(value: "go.probo.inc/probo/pkg/coredata.SAMLEnforcementPolicyOff")
OPTIONAL
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.SAMLEnforcementPolicyOptional"
)
REQUIRED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.SAMLEnforcementPolicyRequired"
)
}
enum AuthMethod {
PASSWORD
SAML
RECOVERY_CODE
}
enum TokenScope {
READ_ORGANIZATION
WRITE_ORGANIZATION
READ_COMPLIANCE
WRITE_COMPLIANCE
READ_RISK
WRITE_RISK
READ_VENDOR
WRITE_VENDOR
READ_DOCUMENTS
WRITE_DOCUMENTS
READ_TRUST_CENTER
WRITE_TRUST_CENTER
ADMIN
}
enum ProvisioningSource {
MANUAL
INVITATION
SAML
}
type MembershipConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/connect/v1/types.MembershipConnection"
) {
edges: [MembershipEdge!]!
pageInfo: PageInfo!
totalCount: Int! @goField(forceResolver: true)
}
type MembershipEdge {
node: Membership!
cursor: CursorKey!
}
type InvitationConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/connect/v1/types.InvitationConnection"
) {
edges: [InvitationEdge!]!
pageInfo: PageInfo!
totalCount: Int! @goField(forceResolver: true)
}
type InvitationEdge {
node: Invitation!
cursor: CursorKey!
}
type SessionConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/connect/v1/types.SessionConnection"
) {
edges: [SessionEdge!]!
pageInfo: PageInfo!
totalCount: Int! @goField(forceResolver: true)
}
type SessionEdge {
node: Session!
cursor: CursorKey!
}
type PersonalAPIKeyConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/connect/v1/types.PersonalAPIKeyConnection"
) {
edges: [PersonalAPIKeyEdge!]!
pageInfo: PageInfo!
totalCount: Int! @goField(forceResolver: true)
}
type PersonalAPIKeyEdge {
node: PersonalAPIKey!
cursor: CursorKey!
}
type SAMLConfigurationConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/connect/v1/types.SAMLConfigurationConnection"
) {
edges: [SAMLConfigurationEdge!]!
pageInfo: PageInfo!
totalCount: Int! @goField(forceResolver: true)
}
type SAMLConfigurationEdge {
node: SAMLConfiguration!
cursor: CursorKey!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: CursorKey
endCursor: CursorKey
}
input SignInInput {
email: EmailAddr!
password: String!
}
input SignUpInput {
email: EmailAddr!
password: String!
fullName: String!
}
input SignUpFromInvitationInput {
token: String!
password: String!
}
input ForgotPasswordInput {
email: EmailAddr!
}
input ResetPasswordInput {
token: String!
password: String!
}
input VerifyEmailInput {
token: String!
}
input ChangePasswordInput {
currentPassword: String!
newPassword: String!
}
input ChangeEmailInput {
newEmail: EmailAddr!
password: String!
}
input DeactivateAccountInput {
password: String!
}
input DeleteAccountInput {
password: String!
confirmation: String!
}
input UpdateIdentityProfileInput {
membershipId: ID!
displayName: String
firstName: String
lastName: String
jobTitle: String
department: String
phoneNumber: String
timezone: String
locale: String
}
input RevokeSessionInput {
sessionId: ID!
}
input CreatePersonalAPIKeyInput {
name: String!
expiresAt: Datetime!
organizationIds: [ID!]!
}
input UpdatePersonalAPIKeyInput {
tokenId: ID!
name: String
description: String
}
input RevokePersonalAPIKeyInput {
tokenId: ID!
}
input CreateOrganizationInput {
name: String!
logoFile: Upload
horizontalLogoFile: Upload
}
input UpdateOrganizationInput {
organizationId: ID!
name: String
logoFile: Upload @goField(omittable: true)
horizontalLogoFile: Upload @goField(omittable: true)
}
input DeleteOrganizationInput {
organizationId: ID!
}
input SessionPolicyInput {
maxSessionDurationHours: Int
idleTimeoutMinutes: Int
maxConcurrentSessions: Int
requireReauthForSensitiveActions: Boolean
}
input AddIPAllowlistEntryInput {
organizationId: ID!
cidr: String!
description: String
}
input RemoveIPAllowlistEntryInput {
entryId: ID!
}
input InviteMemberInput {
organizationId: ID!
email: EmailAddr!
fullName: String!
}
input RemoveMemberInput {
organizationId: ID!
membershipId: ID!
}
input InvitationProfileInput {
displayName: String!
firstName: String
lastName: String
jobTitle: String
department: String
}
input AcceptInvitationInput {
invitationId: ID!
}
input DeleteInvitationInput {
organizationId: ID!
invitationId: ID!
}
input CreateSAMLConfigurationInput {
organizationId: ID!
emailDomain: String!
idpEntityId: String!
idpSsoUrl: String!
idpCertificate: String!
autoSignupEnabled: Boolean!
attributeMappings: SAMLAttributeMappingsInput
}
input SAMLAttributeMappingsInput {
email: String
firstName: String
lastName: String
role: String
}
input UpdateSAMLConfigurationInput {
organizationId: ID!
samlConfigurationId: ID!
idpEntityId: String
idpSsoUrl: String
idpCertificate: String
autoSignupEnabled: Boolean
enforcementPolicy: SAMLEnforcementPolicy
attributeMappings: SAMLAttributeMappingsInput
}
input DeleteSAMLConfigurationInput {
organizationId: ID!
samlConfigurationId: ID!
}
type SignInPayload {
identity: Identity
}
type SignUpPayload {
identity: Identity
}
type SignOutPayload {
success: Boolean!
}
type SignUpFromInvitationPayload {
identity: Identity
}
type ForgotPasswordPayload {
success: Boolean!
}
type ResetPasswordPayload {
success: Boolean!
}
type VerifyEmailPayload {
success: Boolean!
}
type ChangePasswordPayload {
success: Boolean!
}
type ChangeEmailPayload {
success: Boolean!
}
type DeactivateAccountPayload {
success: Boolean!
}
type DeleteAccountPayload {
success: Boolean!
}
type UpdateIdentityProfilePayload {
profile: IdentityProfile
}
type RevokeSessionPayload {
success: Boolean!
}
type RevokeAllSessionsPayload {
revokedCount: Int!
}
type CreatePersonalAPIKeyPayload {
personalAPIKeyEdge: PersonalAPIKeyEdge!
token: String!
}
type UpdatePersonalAPIKeyPayload {
personalAPIKey: PersonalAPIKey
}
type RevokePersonalAPIKeyPayload {
success: Boolean!
}
type CreateOrganizationPayload {
organization: Organization
membershipEdge: MembershipEdge!
}
type UpdateOrganizationPayload {
organization: Organization
}
type DeleteOrganizationPayload {
deletedOrganizationId: ID!
}
type InviteMemberPayload {
invitationEdge: InvitationEdge!
}
type RemoveMemberPayload {
deletedMembershipId: ID!
}
type AcceptInvitationPayload {
membershipEdge: MembershipEdge!
}
type DeleteInvitationPayload {
deletedInvitationId: ID!
}
type CreateSAMLConfigurationPayload {
samlConfigurationEdge: SAMLConfigurationEdge!
}
type UpdateSAMLConfigurationPayload {
samlConfiguration: SAMLConfiguration
}
type DeleteSAMLConfigurationPayload {
deletedSamlConfigurationId: ID!
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,123 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package connect_v1
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"go.gearno.de/kit/httpserver"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/securecookie"
)
var (
identityContextKey = &ctxKey{name: "identity"}
sessionContextKey = &ctxKey{name: "session"}
)
func SessionFromContext(ctx context.Context) *coredata.Session {
session, _ := ctx.Value(sessionContextKey).(*coredata.Session)
return session
}
func UserFromContext(ctx context.Context) *coredata.User {
user, _ := ctx.Value(identityContextKey).(*coredata.User)
return user
}
func NewSessionMiddleware(svc *iam.Service, cookieConfig securecookie.Config) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
apiKey := APIKeyFromContext(ctx)
if apiKey != nil {
httpserver.RenderError(w, http.StatusBadRequest, errors.New("session authentication cannot be used with API key authentication"))
return
}
cookieValue, err := securecookie.Get(r, cookieConfig)
if err != nil {
next.ServeHTTP(w, r)
return
}
sessionID, err := gid.ParseGID(cookieValue)
if err != nil {
securecookie.Clear(w, cookieConfig)
next.ServeHTTP(w, r)
return
}
session, err := svc.SessionService.GetSession(ctx, sessionID)
if err != nil {
var errSessionNotFound *iam.ErrSessionNotFound
var errSessionExpired *iam.ErrSessionExpired
if errors.As(err, &errSessionNotFound) || errors.As(err, &errSessionExpired) {
securecookie.Clear(w, cookieConfig)
next.ServeHTTP(w, r)
return
}
panic(fmt.Errorf("cannot get session: %w", err))
}
user, err := svc.AccountService.GetIdentity(ctx, session.UserID)
if err != nil {
var errUserNotFound *iam.ErrUserNotFound
if errors.As(err, &errUserNotFound) {
securecookie.Clear(w, cookieConfig)
next.ServeHTTP(w, r)
return
}
panic(fmt.Errorf("cannot get user: %w", err))
}
userAgent := r.UserAgent()
// TODO: will work well when no layer 7 proxy is in front of the server
var ipAddress net.IP
if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
ipAddress = net.ParseIP(host)
} else {
ipAddress = net.ParseIP(r.RemoteAddr)
}
err = svc.SessionService.UpdateSessionInfo(ctx, session.ID, userAgent, ipAddress)
if err != nil {
panic(fmt.Errorf("cannot update session info: %w", err))
}
ctx = context.WithValue(ctx, sessionContextKey, session)
ctx = context.WithValue(ctx, identityContextKey, user)
next.ServeHTTP(w, r.WithContext(ctx))
err = svc.SessionService.UpdateSessionData(ctx, session.ID, session.Data)
if err != nil {
panic(fmt.Errorf("cannot update session data: %w", err))
}
},
)
}
}

View File

@@ -0,0 +1,27 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package types
import "go.probo.inc/probo/pkg/coredata"
func NewIdentity(identity *coredata.User) *Identity {
return &Identity{
ID: identity.ID,
Email: identity.EmailAddress,
EmailVerified: identity.EmailAddressVerified,
CreatedAt: identity.CreatedAt,
UpdatedAt: identity.UpdatedAt,
}
}

View File

@@ -0,0 +1,74 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package types
import (
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
InvitationOrderBy OrderBy[coredata.InvitationOrderField]
InvitationConnection struct {
TotalCount int
Edges []*InvitationEdge
PageInfo PageInfo
Resolver any
ParentID gid.GID
Filters *coredata.InvitationFilter
}
)
func NewInvitationConnection(
p *page.Page[*coredata.Invitation, coredata.InvitationOrderField],
resolver any,
parentID gid.GID,
filters *coredata.InvitationFilter,
) *InvitationConnection {
edges := make([]*InvitationEdge, len(p.Data))
for i, invitation := range p.Data {
edges[i] = NewInvitationEdge(invitation, p.Cursor.OrderBy.Field)
}
return &InvitationConnection{
Edges: edges,
PageInfo: *NewPageInfo(p),
Resolver: resolver,
ParentID: parentID,
Filters: filters,
}
}
func NewInvitationEdge(invitation *coredata.Invitation, orderField coredata.InvitationOrderField) *InvitationEdge {
return &InvitationEdge{
Node: NewInvitation(invitation),
Cursor: invitation.CursorKey(orderField),
}
}
func NewInvitation(invitation *coredata.Invitation) *Invitation {
return &Invitation{
ID: invitation.ID,
Email: invitation.Email,
ExpiresAt: invitation.ExpiresAt,
AcceptedAt: invitation.AcceptedAt,
CreatedAt: invitation.CreatedAt,
Status: invitation.Status,
}
}

View File

@@ -0,0 +1,71 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package types
import (
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
MembershipOrderBy OrderBy[coredata.MembershipOrderField]
MembershipConnection struct {
TotalCount int
Edges []*MembershipEdge
PageInfo PageInfo
Resolver any
ParentID gid.GID
}
)
func NewMembershipConnection(
p *page.Page[*coredata.Membership, coredata.MembershipOrderField],
resolver any,
parentID gid.GID,
) *MembershipConnection {
edges := make([]*MembershipEdge, len(p.Data))
for i, membership := range p.Data {
edges[i] = NewMembershipEdge(membership, p.Cursor.OrderBy.Field)
}
return &MembershipConnection{
Edges: edges,
PageInfo: *NewPageInfo(p),
Resolver: resolver,
ParentID: parentID,
}
}
func NewMembershipEdge(membership *coredata.Membership, orderField coredata.MembershipOrderField) *MembershipEdge {
return &MembershipEdge{
Node: NewMembership(membership),
Cursor: membership.CursorKey(orderField),
}
}
func NewMembership(membership *coredata.Membership) *Membership {
return &Membership{
ID: membership.ID,
CreatedAt: membership.CreatedAt,
// Permissions: membership.Permissions,
// ProvisionedBy: membership.ProvisionedBy,
// Active: membership.Active,
// LastSyncedAt: membership.LastSyncedAt,
}
}

View File

@@ -0,0 +1,24 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package types
import "go.probo.inc/probo/pkg/page"
type (
OrderBy[T page.OrderField] struct {
Field T
Direction page.OrderDirection
}
)

View File

@@ -0,0 +1,32 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package types
import (
"go.probo.inc/probo/pkg/coredata"
)
type (
OrganizationOrderBy OrderBy[coredata.OrganizationOrderField]
)
func NewOrganization(organization *coredata.Organization) *Organization {
return &Organization{
ID: organization.ID,
Name: organization.Name,
CreatedAt: organization.CreatedAt,
UpdatedAt: organization.UpdatedAt,
}
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package types
import (
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/server/gqlutils/types/pageinfo"
)
func NewPageInfo[T page.Paginable[O], O page.OrderField](p *page.Page[T, O]) *PageInfo {
data := pageinfo.NewPageInfo(p)
return &PageInfo{
HasNextPage: data.HasNextPage,
HasPreviousPage: data.HasPreviousPage,
StartCursor: data.StartCursor,
EndCursor: data.EndCursor,
}
}

View File

@@ -0,0 +1,69 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package types
import (
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
PersonalAPIKeyOrderBy OrderBy[coredata.UserAPIKeyOrderField]
PersonalAPIKeyConnection struct {
TotalCount int
Edges []*PersonalAPIKeyEdge
PageInfo PageInfo
Resolver any
ParentID gid.GID
}
)
func NewPersonalAPIKeyConnection(
p *page.Page[*coredata.UserAPIKey, coredata.UserAPIKeyOrderField],
resolver any,
parentID gid.GID,
) *PersonalAPIKeyConnection {
edges := make([]*PersonalAPIKeyEdge, len(p.Data))
for i, personalAPIKey := range p.Data {
edges[i] = NewPersonalAPIKeyEdge(personalAPIKey, p.Cursor.OrderBy.Field)
}
return &PersonalAPIKeyConnection{
Edges: edges,
PageInfo: *NewPageInfo(p),
Resolver: resolver,
ParentID: parentID,
}
}
func NewPersonalAPIKeyEdge(personalAPIKey *coredata.UserAPIKey, orderField coredata.UserAPIKeyOrderField) *PersonalAPIKeyEdge {
return &PersonalAPIKeyEdge{
Node: NewPersonalAPIKey(personalAPIKey),
Cursor: personalAPIKey.CursorKey(orderField),
}
}
func NewPersonalAPIKey(personalAPIKey *coredata.UserAPIKey) *PersonalAPIKey {
return &PersonalAPIKey{
ID: personalAPIKey.ID,
Name: personalAPIKey.Name,
ExpiresAt: personalAPIKey.ExpiresAt,
CreatedAt: personalAPIKey.CreatedAt,
}
}

View File

@@ -0,0 +1,82 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package types
import (
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
SAMLConfigurationOrderBy OrderBy[coredata.SAMLConfigurationOrderField]
SAMLConfigurationConnection struct {
TotalCount int
Edges []*SAMLConfigurationEdge
PageInfo PageInfo
Resolver any
ParentID gid.GID
}
)
func NewSAMLConfigurationConnection(
p *page.Page[*coredata.SAMLConfiguration, coredata.SAMLConfigurationOrderField],
resolver any,
parentID gid.GID,
) *SAMLConfigurationConnection {
edges := make([]*SAMLConfigurationEdge, len(p.Data))
for i, samlConfiguration := range p.Data {
edges[i] = NewSAMLConfigurationEdge(samlConfiguration, p.Cursor.OrderBy.Field)
}
return &SAMLConfigurationConnection{
Edges: edges,
PageInfo: *NewPageInfo(p),
Resolver: resolver,
ParentID: parentID,
}
}
func NewSAMLConfigurationEdge(samlConfiguration *coredata.SAMLConfiguration, orderField coredata.SAMLConfigurationOrderField) *SAMLConfigurationEdge {
return &SAMLConfigurationEdge{
Node: NewSAMLConfiguration(samlConfiguration),
Cursor: samlConfiguration.CursorKey(orderField),
}
}
func NewSAMLConfiguration(samlConfiguration *coredata.SAMLConfiguration) *SAMLConfiguration {
return &SAMLConfiguration{
ID: samlConfiguration.ID,
EmailDomain: samlConfiguration.EmailDomain,
EnforcementPolicy: samlConfiguration.EnforcementPolicy,
DomainVerified: samlConfiguration.DomainVerified,
DomainVerifiedAt: samlConfiguration.DomainVerifiedAt,
DomainVerificationToken: samlConfiguration.DomainVerificationToken,
IdpEntityID: samlConfiguration.IdPEntityID,
IdpSsoURL: samlConfiguration.IdPSsoURL,
IdpCertificate: samlConfiguration.IdPCertificate,
CreatedAt: samlConfiguration.CreatedAt,
UpdatedAt: samlConfiguration.UpdatedAt,
AttributeMappings: &SAMLAttributeMappings{
Email: samlConfiguration.AttributeEmail,
FirstName: samlConfiguration.AttributeFirstname,
LastName: samlConfiguration.AttributeLastname,
Role: samlConfiguration.AttributeRole,
},
}
}

View File

@@ -0,0 +1,71 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package types
import (
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
SessionOrderBy OrderBy[coredata.SessionOrderField]
SessionConnection struct {
TotalCount int
Edges []*SessionEdge
PageInfo PageInfo
Resolver any
ParentID gid.GID
}
)
func NewSessionConnection(
p *page.Page[*coredata.Session, coredata.SessionOrderField],
resolver any,
parentID gid.GID,
) *SessionConnection {
edges := make([]*SessionEdge, len(p.Data))
for i, session := range p.Data {
edges[i] = NewSessionEdge(session, p.Cursor.OrderBy.Field)
}
return &SessionConnection{
Edges: edges,
PageInfo: *NewPageInfo(p),
Resolver: resolver,
ParentID: parentID,
}
}
func NewSessionEdge(session *coredata.Session, orderField coredata.SessionOrderField) *SessionEdge {
return &SessionEdge{
Node: NewSession(session),
Cursor: session.CursorKey(orderField),
}
}
func NewSession(session *coredata.Session) *Session {
return &Session{
ID: session.ID,
IPAddress: session.IPAddress.String(),
UserAgent: session.UserAgent,
UpdatedAt: session.UpdatedAt,
CreatedAt: session.CreatedAt,
ExpiresAt: session.ExpiredAt,
}
}

View File

@@ -0,0 +1,966 @@
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
package types
import (
"bytes"
"fmt"
"io"
"strconv"
"time"
"github.com/99designs/gqlgen/graphql"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/mail"
"go.probo.inc/probo/pkg/page"
)
type Node interface {
IsNode()
GetID() gid.GID
}
type AcceptInvitationInput struct {
InvitationID gid.GID `json:"invitationId"`
}
type AcceptInvitationPayload struct {
MembershipEdge *MembershipEdge `json:"membershipEdge"`
}
type AddIPAllowlistEntryInput struct {
OrganizationID gid.GID `json:"organizationId"`
Cidr string `json:"cidr"`
Description *string `json:"description,omitempty"`
}
type Application struct {
ID ApplicationID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
AvailableAccessLevels []AccessLevel `json:"availableAccessLevels"`
}
type ChangeEmailInput struct {
NewEmail mail.Addr `json:"newEmail"`
Password string `json:"password"`
}
type ChangeEmailPayload struct {
Success bool `json:"success"`
}
type ChangePasswordInput struct {
CurrentPassword string `json:"currentPassword"`
NewPassword string `json:"newPassword"`
}
type ChangePasswordPayload struct {
Success bool `json:"success"`
}
type CreateOrganizationInput struct {
Name string `json:"name"`
LogoFile *graphql.Upload `json:"logoFile,omitempty"`
HorizontalLogoFile *graphql.Upload `json:"horizontalLogoFile,omitempty"`
}
type CreateOrganizationPayload struct {
Organization *Organization `json:"organization,omitempty"`
MembershipEdge *MembershipEdge `json:"membershipEdge"`
}
type CreatePersonalAPIKeyInput struct {
Name string `json:"name"`
ExpiresAt time.Time `json:"expiresAt"`
OrganizationIds []gid.GID `json:"organizationIds"`
}
type CreatePersonalAPIKeyPayload struct {
PersonalAPIKeyEdge *PersonalAPIKeyEdge `json:"personalAPIKeyEdge"`
Token string `json:"token"`
}
type CreateSAMLConfigurationInput struct {
OrganizationID gid.GID `json:"organizationId"`
EmailDomain string `json:"emailDomain"`
IdpEntityID string `json:"idpEntityId"`
IdpSsoURL string `json:"idpSsoUrl"`
IdpCertificate string `json:"idpCertificate"`
AutoSignupEnabled bool `json:"autoSignupEnabled"`
AttributeMappings *SAMLAttributeMappingsInput `json:"attributeMappings,omitempty"`
}
type CreateSAMLConfigurationPayload struct {
SamlConfigurationEdge *SAMLConfigurationEdge `json:"samlConfigurationEdge"`
}
type CustomAttribute struct {
Key string `json:"key"`
Value string `json:"value"`
}
type DeactivateAccountInput struct {
Password string `json:"password"`
}
type DeactivateAccountPayload struct {
Success bool `json:"success"`
}
type DeleteAccountInput struct {
Password string `json:"password"`
Confirmation string `json:"confirmation"`
}
type DeleteAccountPayload struct {
Success bool `json:"success"`
}
type DeleteInvitationInput struct {
OrganizationID gid.GID `json:"organizationId"`
InvitationID gid.GID `json:"invitationId"`
}
type DeleteInvitationPayload struct {
DeletedInvitationID gid.GID `json:"deletedInvitationId"`
}
type DeleteOrganizationInput struct {
OrganizationID gid.GID `json:"organizationId"`
}
type DeleteOrganizationPayload struct {
DeletedOrganizationID gid.GID `json:"deletedOrganizationId"`
}
type DeleteSAMLConfigurationInput struct {
OrganizationID gid.GID `json:"organizationId"`
SamlConfigurationID gid.GID `json:"samlConfigurationId"`
}
type DeleteSAMLConfigurationPayload struct {
DeletedSamlConfigurationID gid.GID `json:"deletedSamlConfigurationId"`
}
type ForgotPasswordInput struct {
Email mail.Addr `json:"email"`
}
type ForgotPasswordPayload struct {
Success bool `json:"success"`
}
type Identity struct {
ID gid.GID `json:"id"`
Email mail.Addr `json:"email"`
EmailVerified bool `json:"emailVerified"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Memberships *MembershipConnection `json:"memberships"`
PendingInvitations *InvitationConnection `json:"pendingInvitations"`
Sessions *SessionConnection `json:"sessions"`
PersonalAPIKeys *PersonalAPIKeyConnection `json:"personalAPIKeys"`
ProfileFor *IdentityProfile `json:"profileFor,omitempty"`
}
func (Identity) IsNode() {}
func (this Identity) GetID() gid.GID { return this.ID }
type IdentityProfile struct {
ID gid.GID `json:"id"`
DisplayName string `json:"displayName"`
FirstName *string `json:"firstName,omitempty"`
LastName *string `json:"lastName,omitempty"`
JobTitle *string `json:"jobTitle,omitempty"`
Department *string `json:"department,omitempty"`
PhoneNumber *string `json:"phoneNumber,omitempty"`
AvatarURL *string `json:"avatarUrl,omitempty"`
Manager *IdentityProfile `json:"manager,omitempty"`
Timezone *string `json:"timezone,omitempty"`
Locale *string `json:"locale,omitempty"`
CustomAttributes []*CustomAttribute `json:"customAttributes"`
ProvisionedBy ProvisioningSource `json:"provisionedBy"`
ExternalID *string `json:"externalId,omitempty"`
Identity *Identity `json:"identity"`
Organization *Organization `json:"organization"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (IdentityProfile) IsNode() {}
func (this IdentityProfile) GetID() gid.GID { return this.ID }
type Invitation struct {
ID gid.GID `json:"id"`
Email mail.Addr `json:"email"`
ExpiresAt time.Time `json:"expiresAt"`
AcceptedAt *time.Time `json:"acceptedAt,omitempty"`
CreatedAt time.Time `json:"createdAt"`
Status coredata.InvitationStatus `json:"status"`
}
func (Invitation) IsNode() {}
func (this Invitation) GetID() gid.GID { return this.ID }
type InvitationEdge struct {
Node *Invitation `json:"node"`
Cursor page.CursorKey `json:"cursor"`
}
type InvitationProfile struct {
DisplayName string `json:"displayName"`
FirstName *string `json:"firstName,omitempty"`
LastName *string `json:"lastName,omitempty"`
JobTitle *string `json:"jobTitle,omitempty"`
Department *string `json:"department,omitempty"`
}
type InvitationProfileInput struct {
DisplayName string `json:"displayName"`
FirstName *string `json:"firstName,omitempty"`
LastName *string `json:"lastName,omitempty"`
JobTitle *string `json:"jobTitle,omitempty"`
Department *string `json:"department,omitempty"`
}
type InviteMemberInput struct {
OrganizationID gid.GID `json:"organizationId"`
Email mail.Addr `json:"email"`
FullName string `json:"fullName"`
}
type InviteMemberPayload struct {
InvitationEdge *InvitationEdge `json:"invitationEdge"`
}
type Membership struct {
ID gid.GID `json:"id"`
CreatedAt time.Time `json:"createdAt"`
Profile *IdentityProfile `json:"profile"`
Identity *Identity `json:"identity"`
Organization *Organization `json:"organization"`
Permissions []*Permission `json:"permissions"`
ProvisionedBy ProvisioningSource `json:"provisionedBy"`
Active bool `json:"active"`
LastSyncedAt *time.Time `json:"lastSyncedAt,omitempty"`
}
func (Membership) IsNode() {}
func (this Membership) GetID() gid.GID { return this.ID }
type MembershipEdge struct {
Node *Membership `json:"node"`
Cursor page.CursorKey `json:"cursor"`
}
type Mutation struct {
}
type Organization struct {
ID gid.GID `json:"id"`
Name string `json:"name"`
LogoURL *string `json:"logoUrl,omitempty"`
HorizontalLogoURL *string `json:"horizontalLogoUrl,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Members *MembershipConnection `json:"members"`
Invitations *InvitationConnection `json:"invitations"`
SamlConfigurations *SAMLConfigurationConnection `json:"samlConfigurations"`
AvailableApplications []*Application `json:"availableApplications"`
}
func (Organization) IsNode() {}
func (this Organization) GetID() gid.GID { return this.ID }
type PageInfo struct {
HasNextPage bool `json:"hasNextPage"`
HasPreviousPage bool `json:"hasPreviousPage"`
StartCursor *page.CursorKey `json:"startCursor,omitempty"`
EndCursor *page.CursorKey `json:"endCursor,omitempty"`
}
type Permission struct {
ID gid.GID `json:"id"`
CreatedAt time.Time `json:"createdAt"`
Application *Application `json:"application"`
AccessLevel AccessLevel `json:"accessLevel"`
Organization *Organization `json:"organization"`
PrincipalType PrincipalType `json:"principalType"`
PrincipalID gid.GID `json:"principalId"`
}
func (Permission) IsNode() {}
func (this Permission) GetID() gid.GID { return this.ID }
type PermissionGrant struct {
Application *Application `json:"application"`
AccessLevel AccessLevel `json:"accessLevel"`
}
type PersonalAPIKey struct {
ID gid.GID `json:"id"`
Name string `json:"name"`
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
ExpiresAt time.Time `json:"expiresAt"`
CreatedAt time.Time `json:"createdAt"`
Scopes []TokenScope `json:"scopes"`
Organizations []*Organization `json:"organizations"`
}
func (PersonalAPIKey) IsNode() {}
func (this PersonalAPIKey) GetID() gid.GID { return this.ID }
type PersonalAPIKeyEdge struct {
Node *PersonalAPIKey `json:"node"`
Cursor page.CursorKey `json:"cursor"`
}
type Query struct {
}
type RemoveIPAllowlistEntryInput struct {
EntryID gid.GID `json:"entryId"`
}
type RemoveMemberInput struct {
OrganizationID gid.GID `json:"organizationId"`
MembershipID gid.GID `json:"membershipId"`
}
type RemoveMemberPayload struct {
DeletedMembershipID gid.GID `json:"deletedMembershipId"`
}
type ResetPasswordInput struct {
Token string `json:"token"`
Password string `json:"password"`
}
type ResetPasswordPayload struct {
Success bool `json:"success"`
}
type RevokeAllSessionsPayload struct {
RevokedCount int `json:"revokedCount"`
}
type RevokePersonalAPIKeyInput struct {
TokenID gid.GID `json:"tokenId"`
}
type RevokePersonalAPIKeyPayload struct {
Success bool `json:"success"`
}
type RevokeSessionInput struct {
SessionID gid.GID `json:"sessionId"`
}
type RevokeSessionPayload struct {
Success bool `json:"success"`
}
type SAMLAttributeMappings struct {
Email string `json:"email"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Role string `json:"role"`
}
type SAMLAttributeMappingsInput struct {
Email *string `json:"email,omitempty"`
FirstName *string `json:"firstName,omitempty"`
LastName *string `json:"lastName,omitempty"`
Role *string `json:"role,omitempty"`
}
type SAMLConfiguration struct {
ID gid.GID `json:"id"`
EmailDomain string `json:"emailDomain"`
Enabled bool `json:"enabled"`
EnforcementPolicy coredata.SAMLEnforcementPolicy `json:"enforcementPolicy"`
DomainVerified bool `json:"domainVerified"`
DomainVerifiedAt *time.Time `json:"domainVerifiedAt,omitempty"`
DomainVerificationToken *string `json:"domainVerificationToken,omitempty"`
IdpEntityID string `json:"idpEntityId"`
IdpSsoURL string `json:"idpSsoUrl"`
IdpCertificate string `json:"idpCertificate"`
AutoSignupEnabled bool `json:"autoSignupEnabled"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
SpMetadataURL string `json:"spMetadataUrl"`
TestLoginURL string `json:"testLoginUrl"`
AttributeMappings *SAMLAttributeMappings `json:"attributeMappings"`
DefaultPermissions []*PermissionGrant `json:"defaultPermissions"`
}
func (SAMLConfiguration) IsNode() {}
func (this SAMLConfiguration) GetID() gid.GID { return this.ID }
type SAMLConfigurationEdge struct {
Node *SAMLConfiguration `json:"node"`
Cursor page.CursorKey `json:"cursor"`
}
type SSOAvailability struct {
Available bool `json:"available"`
SamlConfigID *gid.GID `json:"samlConfigId,omitempty"`
OrganizationID *gid.GID `json:"organizationId,omitempty"`
}
type Session struct {
ID gid.GID `json:"id"`
IPAddress string `json:"ipAddress"`
UserAgent string `json:"userAgent"`
UpdatedAt time.Time `json:"updatedAt"`
CreatedAt time.Time `json:"createdAt"`
ExpiresAt time.Time `json:"expiresAt"`
}
func (Session) IsNode() {}
func (this Session) GetID() gid.GID { return this.ID }
type SessionEdge struct {
Node *Session `json:"node"`
Cursor page.CursorKey `json:"cursor"`
}
type SessionOrder struct {
Direction page.OrderDirection `json:"direction"`
Field coredata.SessionOrderField `json:"field"`
}
type SessionPolicy struct {
MaxSessionDurationHours int `json:"maxSessionDurationHours"`
IdleTimeoutMinutes int `json:"idleTimeoutMinutes"`
MaxConcurrentSessions *int `json:"maxConcurrentSessions,omitempty"`
RequireReauthForSensitiveActions bool `json:"requireReauthForSensitiveActions"`
}
type SessionPolicyInput struct {
MaxSessionDurationHours *int `json:"maxSessionDurationHours,omitempty"`
IdleTimeoutMinutes *int `json:"idleTimeoutMinutes,omitempty"`
MaxConcurrentSessions *int `json:"maxConcurrentSessions,omitempty"`
RequireReauthForSensitiveActions *bool `json:"requireReauthForSensitiveActions,omitempty"`
}
type SignInInput struct {
Email mail.Addr `json:"email"`
Password string `json:"password"`
}
type SignInPayload struct {
Identity *Identity `json:"identity,omitempty"`
}
type SignOutPayload struct {
Success bool `json:"success"`
}
type SignUpFromInvitationInput struct {
Token string `json:"token"`
Password string `json:"password"`
}
type SignUpFromInvitationPayload struct {
Identity *Identity `json:"identity,omitempty"`
}
type SignUpInput struct {
Email mail.Addr `json:"email"`
Password string `json:"password"`
FullName string `json:"fullName"`
}
type SignUpPayload struct {
Identity *Identity `json:"identity,omitempty"`
}
type UpdateIdentityProfileInput struct {
MembershipID gid.GID `json:"membershipId"`
DisplayName *string `json:"displayName,omitempty"`
FirstName *string `json:"firstName,omitempty"`
LastName *string `json:"lastName,omitempty"`
JobTitle *string `json:"jobTitle,omitempty"`
Department *string `json:"department,omitempty"`
PhoneNumber *string `json:"phoneNumber,omitempty"`
Timezone *string `json:"timezone,omitempty"`
Locale *string `json:"locale,omitempty"`
}
type UpdateIdentityProfilePayload struct {
Profile *IdentityProfile `json:"profile,omitempty"`
}
type UpdateOrganizationInput struct {
OrganizationID gid.GID `json:"organizationId"`
Name *string `json:"name,omitempty"`
LogoFile graphql.Omittable[*graphql.Upload] `json:"logoFile,omitempty"`
HorizontalLogoFile graphql.Omittable[*graphql.Upload] `json:"horizontalLogoFile,omitempty"`
}
type UpdateOrganizationPayload struct {
Organization *Organization `json:"organization,omitempty"`
}
type UpdatePersonalAPIKeyInput struct {
TokenID gid.GID `json:"tokenId"`
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
}
type UpdatePersonalAPIKeyPayload struct {
PersonalAPIKey *PersonalAPIKey `json:"personalAPIKey,omitempty"`
}
type UpdateSAMLConfigurationInput struct {
OrganizationID gid.GID `json:"organizationId"`
SamlConfigurationID gid.GID `json:"samlConfigurationId"`
IdpEntityID *string `json:"idpEntityId,omitempty"`
IdpSsoURL *string `json:"idpSsoUrl,omitempty"`
IdpCertificate *string `json:"idpCertificate,omitempty"`
AutoSignupEnabled *bool `json:"autoSignupEnabled,omitempty"`
EnforcementPolicy *coredata.SAMLEnforcementPolicy `json:"enforcementPolicy,omitempty"`
AttributeMappings *SAMLAttributeMappingsInput `json:"attributeMappings,omitempty"`
}
type UpdateSAMLConfigurationPayload struct {
SamlConfiguration *SAMLConfiguration `json:"samlConfiguration,omitempty"`
}
type VerifyEmailInput struct {
Token string `json:"token"`
}
type VerifyEmailPayload struct {
Success bool `json:"success"`
}
type AccessLevel string
const (
AccessLevelRead AccessLevel = "READ"
AccessLevelWrite AccessLevel = "WRITE"
AccessLevelAdmin AccessLevel = "ADMIN"
)
var AllAccessLevel = []AccessLevel{
AccessLevelRead,
AccessLevelWrite,
AccessLevelAdmin,
}
func (e AccessLevel) IsValid() bool {
switch e {
case AccessLevelRead, AccessLevelWrite, AccessLevelAdmin:
return true
}
return false
}
func (e AccessLevel) String() string {
return string(e)
}
func (e *AccessLevel) UnmarshalGQL(v any) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = AccessLevel(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid AccessLevel", str)
}
return nil
}
func (e AccessLevel) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
func (e *AccessLevel) UnmarshalJSON(b []byte) error {
s, err := strconv.Unquote(string(b))
if err != nil {
return err
}
return e.UnmarshalGQL(s)
}
func (e AccessLevel) MarshalJSON() ([]byte, error) {
var buf bytes.Buffer
e.MarshalGQL(&buf)
return buf.Bytes(), nil
}
type ApplicationID string
const (
ApplicationIDConsole ApplicationID = "CONSOLE"
ApplicationIDCompliance ApplicationID = "COMPLIANCE"
ApplicationIDRisk ApplicationID = "RISK"
ApplicationIDVendor ApplicationID = "VENDOR"
ApplicationIDDocuments ApplicationID = "DOCUMENTS"
ApplicationIDTrustCenter ApplicationID = "TRUST_CENTER"
ApplicationIDSettings ApplicationID = "SETTINGS"
ApplicationIDAPI ApplicationID = "API"
)
var AllApplicationID = []ApplicationID{
ApplicationIDConsole,
ApplicationIDCompliance,
ApplicationIDRisk,
ApplicationIDVendor,
ApplicationIDDocuments,
ApplicationIDTrustCenter,
ApplicationIDSettings,
ApplicationIDAPI,
}
func (e ApplicationID) IsValid() bool {
switch e {
case ApplicationIDConsole, ApplicationIDCompliance, ApplicationIDRisk, ApplicationIDVendor, ApplicationIDDocuments, ApplicationIDTrustCenter, ApplicationIDSettings, ApplicationIDAPI:
return true
}
return false
}
func (e ApplicationID) String() string {
return string(e)
}
func (e *ApplicationID) UnmarshalGQL(v any) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = ApplicationID(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid ApplicationId", str)
}
return nil
}
func (e ApplicationID) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
func (e *ApplicationID) UnmarshalJSON(b []byte) error {
s, err := strconv.Unquote(string(b))
if err != nil {
return err
}
return e.UnmarshalGQL(s)
}
func (e ApplicationID) MarshalJSON() ([]byte, error) {
var buf bytes.Buffer
e.MarshalGQL(&buf)
return buf.Bytes(), nil
}
type AuthMethod string
const (
AuthMethodPassword AuthMethod = "PASSWORD"
AuthMethodSaml AuthMethod = "SAML"
AuthMethodRecoveryCode AuthMethod = "RECOVERY_CODE"
)
var AllAuthMethod = []AuthMethod{
AuthMethodPassword,
AuthMethodSaml,
AuthMethodRecoveryCode,
}
func (e AuthMethod) IsValid() bool {
switch e {
case AuthMethodPassword, AuthMethodSaml, AuthMethodRecoveryCode:
return true
}
return false
}
func (e AuthMethod) String() string {
return string(e)
}
func (e *AuthMethod) UnmarshalGQL(v any) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = AuthMethod(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid AuthMethod", str)
}
return nil
}
func (e AuthMethod) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
func (e *AuthMethod) UnmarshalJSON(b []byte) error {
s, err := strconv.Unquote(string(b))
if err != nil {
return err
}
return e.UnmarshalGQL(s)
}
func (e AuthMethod) MarshalJSON() ([]byte, error) {
var buf bytes.Buffer
e.MarshalGQL(&buf)
return buf.Bytes(), nil
}
type PrincipalType string
const (
PrincipalTypeIdentity PrincipalType = "IDENTITY"
PrincipalTypeServiceAccount PrincipalType = "SERVICE_ACCOUNT"
)
var AllPrincipalType = []PrincipalType{
PrincipalTypeIdentity,
PrincipalTypeServiceAccount,
}
func (e PrincipalType) IsValid() bool {
switch e {
case PrincipalTypeIdentity, PrincipalTypeServiceAccount:
return true
}
return false
}
func (e PrincipalType) String() string {
return string(e)
}
func (e *PrincipalType) UnmarshalGQL(v any) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = PrincipalType(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid PrincipalType", str)
}
return nil
}
func (e PrincipalType) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
func (e *PrincipalType) UnmarshalJSON(b []byte) error {
s, err := strconv.Unquote(string(b))
if err != nil {
return err
}
return e.UnmarshalGQL(s)
}
func (e PrincipalType) MarshalJSON() ([]byte, error) {
var buf bytes.Buffer
e.MarshalGQL(&buf)
return buf.Bytes(), nil
}
type ProvisioningSource string
const (
ProvisioningSourceManual ProvisioningSource = "MANUAL"
ProvisioningSourceInvitation ProvisioningSource = "INVITATION"
ProvisioningSourceSaml ProvisioningSource = "SAML"
)
var AllProvisioningSource = []ProvisioningSource{
ProvisioningSourceManual,
ProvisioningSourceInvitation,
ProvisioningSourceSaml,
}
func (e ProvisioningSource) IsValid() bool {
switch e {
case ProvisioningSourceManual, ProvisioningSourceInvitation, ProvisioningSourceSaml:
return true
}
return false
}
func (e ProvisioningSource) String() string {
return string(e)
}
func (e *ProvisioningSource) UnmarshalGQL(v any) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = ProvisioningSource(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid ProvisioningSource", str)
}
return nil
}
func (e ProvisioningSource) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
func (e *ProvisioningSource) UnmarshalJSON(b []byte) error {
s, err := strconv.Unquote(string(b))
if err != nil {
return err
}
return e.UnmarshalGQL(s)
}
func (e ProvisioningSource) MarshalJSON() ([]byte, error) {
var buf bytes.Buffer
e.MarshalGQL(&buf)
return buf.Bytes(), nil
}
type SessionRequirement string
const (
SessionRequirementPresent SessionRequirement = "PRESENT"
SessionRequirementNone SessionRequirement = "NONE"
SessionRequirementOptional SessionRequirement = "OPTIONAL"
)
var AllSessionRequirement = []SessionRequirement{
SessionRequirementPresent,
SessionRequirementNone,
SessionRequirementOptional,
}
func (e SessionRequirement) IsValid() bool {
switch e {
case SessionRequirementPresent, SessionRequirementNone, SessionRequirementOptional:
return true
}
return false
}
func (e SessionRequirement) String() string {
return string(e)
}
func (e *SessionRequirement) UnmarshalGQL(v any) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = SessionRequirement(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid SessionRequirement", str)
}
return nil
}
func (e SessionRequirement) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
func (e *SessionRequirement) UnmarshalJSON(b []byte) error {
s, err := strconv.Unquote(string(b))
if err != nil {
return err
}
return e.UnmarshalGQL(s)
}
func (e SessionRequirement) MarshalJSON() ([]byte, error) {
var buf bytes.Buffer
e.MarshalGQL(&buf)
return buf.Bytes(), nil
}
type TokenScope string
const (
TokenScopeReadOrganization TokenScope = "READ_ORGANIZATION"
TokenScopeWriteOrganization TokenScope = "WRITE_ORGANIZATION"
TokenScopeReadCompliance TokenScope = "READ_COMPLIANCE"
TokenScopeWriteCompliance TokenScope = "WRITE_COMPLIANCE"
TokenScopeReadRisk TokenScope = "READ_RISK"
TokenScopeWriteRisk TokenScope = "WRITE_RISK"
TokenScopeReadVendor TokenScope = "READ_VENDOR"
TokenScopeWriteVendor TokenScope = "WRITE_VENDOR"
TokenScopeReadDocuments TokenScope = "READ_DOCUMENTS"
TokenScopeWriteDocuments TokenScope = "WRITE_DOCUMENTS"
TokenScopeReadTrustCenter TokenScope = "READ_TRUST_CENTER"
TokenScopeWriteTrustCenter TokenScope = "WRITE_TRUST_CENTER"
TokenScopeAdmin TokenScope = "ADMIN"
)
var AllTokenScope = []TokenScope{
TokenScopeReadOrganization,
TokenScopeWriteOrganization,
TokenScopeReadCompliance,
TokenScopeWriteCompliance,
TokenScopeReadRisk,
TokenScopeWriteRisk,
TokenScopeReadVendor,
TokenScopeWriteVendor,
TokenScopeReadDocuments,
TokenScopeWriteDocuments,
TokenScopeReadTrustCenter,
TokenScopeWriteTrustCenter,
TokenScopeAdmin,
}
func (e TokenScope) IsValid() bool {
switch e {
case TokenScopeReadOrganization, TokenScopeWriteOrganization, TokenScopeReadCompliance, TokenScopeWriteCompliance, TokenScopeReadRisk, TokenScopeWriteRisk, TokenScopeReadVendor, TokenScopeWriteVendor, TokenScopeReadDocuments, TokenScopeWriteDocuments, TokenScopeReadTrustCenter, TokenScopeWriteTrustCenter, TokenScopeAdmin:
return true
}
return false
}
func (e TokenScope) String() string {
return string(e)
}
func (e *TokenScope) UnmarshalGQL(v any) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = TokenScope(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid TokenScope", str)
}
return nil
}
func (e TokenScope) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
func (e *TokenScope) UnmarshalJSON(b []byte) error {
s, err := strconv.Unquote(string(b))
if err != nil {
return err
}
return e.UnmarshalGQL(s)
}
func (e TokenScope) MarshalJSON() ([]byte, error) {
var buf bytes.Buffer
e.MarshalGQL(&buf)
return buf.Bytes(), nil
}

View File

@@ -0,0 +1,930 @@
package connect_v1
// This file will be automatically regenerated based on the schema, any resolver
// implementations
// will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.84
import (
"context"
"errors"
"fmt"
"time"
"github.com/vektah/gqlparser/v2/gqlerror"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/securecookie"
"go.probo.inc/probo/pkg/server/api/connect/v1/schema"
"go.probo.inc/probo/pkg/server/api/connect/v1/types"
"go.probo.inc/probo/pkg/server/gqlutils"
"go.probo.inc/probo/pkg/server/gqlutils/types/cursor"
)
// Memberships is the resolver for the memberships field.
func (r *identityResolver) Memberships(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.MembershipConnection, error) {
pageOrderBy := page.OrderBy[coredata.MembershipOrderField]{
Field: coredata.MembershipOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
cursor := cursor.NewCursor(first, after, last, before, pageOrderBy)
page, err := r.iam.AccountService.ListMemberships(ctx, obj.ID, cursor)
if err != nil {
panic(fmt.Errorf("cannot list memberships: %w", err))
}
return types.NewMembershipConnection(page, r, obj.ID), nil
}
// PendingInvitations is the resolver for the pendingInvitations field.
func (r *identityResolver) PendingInvitations(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.InvitationConnection, error) {
pageOrderBy := page.OrderBy[coredata.InvitationOrderField]{
Field: coredata.InvitationOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
cursor := cursor.NewCursor(first, after, last, before, pageOrderBy)
page, err := r.iam.AccountService.ListPendingInvitations(ctx, obj.ID, cursor)
if err != nil {
panic(fmt.Errorf("cannot list pending invitations: %w", err))
}
return types.NewInvitationConnection(page, r, obj.ID, nil), nil
}
// Sessions is the resolver for the sessions field.
func (r *identityResolver) Sessions(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.SessionOrder) (*types.SessionConnection, error) {
pageOrderBy := page.OrderBy[coredata.SessionOrderField]{
Field: coredata.SessionOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.SessionOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := cursor.NewCursor(first, after, last, before, pageOrderBy)
page, err := r.iam.AccountService.ListSessions(ctx, obj.ID, cursor)
if err != nil {
panic(fmt.Errorf("cannot list sessions: %w", err))
}
return types.NewSessionConnection(page, r, obj.ID), nil
}
// PersonalAPIKeys is the resolver for the personalAPIKeys field.
func (r *identityResolver) PersonalAPIKeys(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.PersonalAPIKeyConnection, error) {
pageOrderBy := page.OrderBy[coredata.UserAPIKeyOrderField]{
Field: coredata.UserAPIKeyOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
cursor := cursor.NewCursor(first, after, last, before, pageOrderBy)
page, err := r.iam.AccountService.ListPersonalAPIKeys(ctx, obj.ID, cursor)
if err != nil {
panic(fmt.Errorf("cannot list personal api keys: %w", err))
}
return types.NewPersonalAPIKeyConnection(page, r, obj.ID), nil
}
// TotalCount is the resolver for the totalCount field.
func (r *invitationConnectionResolver) TotalCount(ctx context.Context, obj *types.InvitationConnection) (int, error) {
switch obj.Resolver.(type) {
case *organizationResolver:
count, err := r.iam.OrganizationService.CountInvitations(ctx, obj.ParentID, obj.Filters)
if err != nil {
panic(fmt.Errorf("cannot count invitations: %w", err))
}
return count, nil
case *identityResolver:
count, err := r.iam.AccountService.CountPendingInvitations(ctx, obj.ParentID)
if err != nil {
panic(fmt.Errorf("cannot count invitations: %w", err))
}
return count, nil
}
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
}
// Identity is the resolver for the identity field.
func (r *membershipResolver) Identity(ctx context.Context, obj *types.Membership) (*types.Identity, error) {
identity, err := r.iam.AccountService.GetIdentityForMembership(ctx, obj.ID)
if err != nil {
panic(fmt.Errorf("cannot get identity: %w", err))
}
return types.NewIdentity(identity), nil
}
// Organization is the resolver for the organization field.
func (r *membershipResolver) Organization(ctx context.Context, obj *types.Membership) (*types.Organization, error) {
organization, err := r.iam.OrganizationService.GetOrganizationForMembership(ctx, obj.ID)
if err != nil {
panic(fmt.Errorf("cannot get organization for membership: %w", err))
}
return types.NewOrganization(organization), nil
}
// TotalCount is the resolver for the totalCount field.
func (r *membershipConnectionResolver) TotalCount(ctx context.Context, obj *types.MembershipConnection) (int, error) {
switch obj.Resolver.(type) {
case *identityResolver:
count, err := r.iam.AccountService.CountMemberships(ctx, obj.ParentID)
if err != nil {
panic(fmt.Errorf("cannot count memberships: %w", err))
}
return count, nil
}
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
}
// SignIn is the resolver for the signIn field.
func (r *mutationResolver) SignIn(ctx context.Context, input types.SignInInput) (*types.SignInPayload, error) {
user, session, err := r.iam.AuthService.OpenSessionWithPassword(ctx, input.Email, input.Password)
if err != nil {
var ErrInvalidCredentials *iam.ErrInvalidCredentials
if errors.As(err, &ErrInvalidCredentials) {
return nil, &gqlerror.Error{
Message: err.Error(),
Extensions: map[string]any{
"code": "INVALID_CREDENTIALS",
},
}
}
// TODO handle error properly here
panic(fmt.Errorf("cannot sign in: %w", err))
}
w := HTTPResponseWriterFromContext(ctx)
securecookie.Set(
w,
r.sessionCookieConfig(time.Until(session.ExpiredAt)),
session.ID.String(),
)
return &types.SignInPayload{
Identity: &types.Identity{
ID: user.ID,
Email: user.EmailAddress,
EmailVerified: user.EmailAddressVerified,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
},
}, nil
}
// SignUp is the resolver for the signUp field.
func (r *mutationResolver) SignUp(ctx context.Context, input types.SignUpInput) (*types.SignUpPayload, error) {
identity, session, err := r.iam.AuthService.CreateIdentityWithPassword(
ctx,
&iam.CreateIdentityWithPasswordRequest{
Email: input.Email,
Password: input.Password,
FullName: input.FullName,
},
)
if err != nil {
// TODO handle error properly here
panic(fmt.Errorf("cannot create identity with password: %w", err))
}
w := HTTPResponseWriterFromContext(ctx)
securecookie.Set(
w,
r.sessionCookieConfig(time.Until(session.ExpiredAt)),
session.ID.String(),
)
return &types.SignUpPayload{
Identity: types.NewIdentity(identity),
}, nil
}
// SignOut is the resolver for the signOut field.
func (r *mutationResolver) SignOut(ctx context.Context) (*types.SignOutPayload, error) {
session := SessionFromContext(ctx)
err := r.iam.SessionService.CloseSession(ctx, session.ID)
if err != nil {
var ErrSessionNotFound *iam.ErrSessionNotFound
if errors.As(err, &ErrSessionNotFound) {
return &types.SignOutPayload{}, nil
}
panic(fmt.Errorf("cannot close session: %w", err))
}
return &types.SignOutPayload{Success: true}, nil
}
// SignUpFromInvitation is the resolver for the signUpFromInvitation field.
func (r *mutationResolver) SignUpFromInvitation(ctx context.Context, input types.SignUpFromInvitationInput) (*types.SignUpFromInvitationPayload, error) {
identity, session, err := r.iam.AuthService.CreateIdentityFromInvitation(
ctx,
&iam.CreateIdentityFromInvitationRequest{
InvitationToken: input.Token,
Password: input.Password,
},
)
if err != nil {
// TODO handle error properly here
panic(fmt.Errorf("cannot create identity from invitation: %w", err))
}
w := HTTPResponseWriterFromContext(ctx)
securecookie.Set(
w,
r.sessionCookieConfig(time.Until(session.ExpiredAt)),
session.ID.String(),
)
return &types.SignUpFromInvitationPayload{
Identity: &types.Identity{
ID: identity.ID,
Email: identity.EmailAddress,
EmailVerified: identity.EmailAddressVerified,
CreatedAt: identity.CreatedAt,
UpdatedAt: identity.UpdatedAt,
},
}, nil
}
// ForgotPassword is the resolver for the forgotPassword field.
func (r *mutationResolver) ForgotPassword(ctx context.Context, input types.ForgotPasswordInput) (*types.ForgotPasswordPayload, error) {
err := r.iam.AuthService.SendPasswordResetInstructionByEmail(
ctx,
input.Email,
)
if err != nil {
// TODO handle error properly here
panic(fmt.Errorf("cannot send password reset instruction by email: %w", err))
}
return &types.ForgotPasswordPayload{
Success: true,
}, nil
}
// ResetPassword is the resolver for the resetPassword field.
func (r *mutationResolver) ResetPassword(ctx context.Context, input types.ResetPasswordInput) (*types.ResetPasswordPayload, error) {
err := r.iam.AuthService.ResetPassword(
ctx,
&iam.ResetPasswordRequest{
Token: input.Token,
Password: input.Password,
},
)
if err != nil {
var errInvalidToken *iam.ErrInvalidToken
if errors.As(err, &errInvalidToken) {
return nil, gqlutils.Invalid(err, nil)
}
panic(fmt.Errorf("cannot reset password: %w", err))
}
return &types.ResetPasswordPayload{
Success: true,
}, nil
}
// VerifyEmail is the resolver for the verifyEmail field.
func (r *mutationResolver) VerifyEmail(ctx context.Context, input types.VerifyEmailInput) (*types.VerifyEmailPayload, error) {
err := r.iam.AccountService.VerifyEmail(ctx, input.Token)
if err != nil {
// TODO handle error properly here
panic(fmt.Errorf("cannot verify email: %w", err))
}
return &types.VerifyEmailPayload{
Success: true,
}, nil
}
// ChangePassword is the resolver for the changePassword field.
func (r *mutationResolver) ChangePassword(ctx context.Context, input types.ChangePasswordInput) (*types.ChangePasswordPayload, error) {
identity := UserFromContext(ctx)
err := r.iam.AccountService.ChangePassword(
ctx,
identity.ID,
&iam.ChangePasswordRequest{
CurrentPassword: input.CurrentPassword,
NewPassword: input.NewPassword,
},
)
if err != nil {
// TODO handle error properly here
panic(fmt.Errorf("cannot change password: %w", err))
}
return &types.ChangePasswordPayload{
Success: true,
}, nil
}
// ChangeEmail is the resolver for the changeEmail field.
func (r *mutationResolver) ChangeEmail(ctx context.Context, input types.ChangeEmailInput) (*types.ChangeEmailPayload, error) {
identity := UserFromContext(ctx)
err := r.iam.AccountService.ChangeEmail(
ctx,
identity.ID,
&iam.ChangeEmailRequest{
NewEmail: input.NewEmail,
Password: input.Password,
},
)
if err != nil {
// TODO handle error properly here
panic(fmt.Errorf("cannot change email: %w", err))
}
return &types.ChangeEmailPayload{
Success: true,
}, nil
}
// UpdateIdentityProfile is the resolver for the updateIdentityProfile field.
func (r *mutationResolver) UpdateIdentityProfile(ctx context.Context, input types.UpdateIdentityProfileInput) (*types.UpdateIdentityProfilePayload, error) {
panic(fmt.Errorf("not implemented: UpdateIdentityProfile - updateIdentityProfile"))
}
// RevokeSession is the resolver for the revokeSession field.
func (r *mutationResolver) RevokeSession(ctx context.Context, input types.RevokeSessionInput) (*types.RevokeSessionPayload, error) {
identity := UserFromContext(ctx)
err := r.iam.SessionService.RevokeSession(ctx, identity.ID, input.SessionID)
if err != nil {
var ErrSessionExpired *iam.ErrSessionExpired
if errors.As(err, &ErrSessionExpired) {
return &types.RevokeSessionPayload{Success: true}, nil
}
panic(fmt.Errorf("cannot revoke session: %w", err))
}
return &types.RevokeSessionPayload{Success: true}, nil
}
// RevokeAllSessions is the resolver for the revokeAllSessions field.
func (r *mutationResolver) RevokeAllSessions(ctx context.Context) (*types.RevokeAllSessionsPayload, error) {
session := SessionFromContext(ctx)
revokedCount, err := r.iam.SessionService.RevokeAllSessions(ctx, session.ID)
if err != nil {
panic(fmt.Errorf("cannot revoke all sessions: %w", err))
}
return &types.RevokeAllSessionsPayload{RevokedCount: int(revokedCount)}, nil
}
// CreatePersonalAPIKey is the resolver for the createPersonalAPIKey field.
func (r *mutationResolver) CreatePersonalAPIKey(ctx context.Context, input types.CreatePersonalAPIKeyInput) (*types.CreatePersonalAPIKeyPayload, error) {
identity := UserFromContext(ctx)
userAPIKey, token, err := r.iam.AccountService.CreatePersonalAPIKey(
ctx,
identity.ID,
input.Name,
input.ExpiresAt,
)
if err != nil {
panic(fmt.Errorf("cannot create personal api key: %w", err))
}
return &types.CreatePersonalAPIKeyPayload{
PersonalAPIKeyEdge: types.NewPersonalAPIKeyEdge(userAPIKey, coredata.UserAPIKeyOrderFieldCreatedAt),
Token: token,
}, nil
}
// UpdatePersonalAPIKey is the resolver for the updatePersonalAPIKey field.
func (r *mutationResolver) UpdatePersonalAPIKey(ctx context.Context, input types.UpdatePersonalAPIKeyInput) (*types.UpdatePersonalAPIKeyPayload, error) {
panic(fmt.Errorf("not implemented: UpdatePersonalAPIKey - updatePersonalAPIKey"))
}
// RevokePersonalAPIKey is the resolver for the revokePersonalAPIKey field.
func (r *mutationResolver) RevokePersonalAPIKey(ctx context.Context, input types.RevokePersonalAPIKeyInput) (*types.RevokePersonalAPIKeyPayload, error) {
identity := UserFromContext(ctx)
err := r.iam.AccountService.DeletePersonalAPIKey(ctx, identity.ID, input.TokenID)
if err != nil {
panic(fmt.Errorf("cannot delete personal api key: %w", err))
}
return &types.RevokePersonalAPIKeyPayload{Success: true}, nil
}
// CreateOrganization is the resolver for the createOrganization field.
func (r *mutationResolver) CreateOrganization(ctx context.Context, input types.CreateOrganizationInput) (*types.CreateOrganizationPayload, error) {
identity := UserFromContext(ctx)
var (
logoFile *iam.UploadedFile
horizontalLogoFile *iam.UploadedFile
)
if input.LogoFile != nil {
logoFile = &iam.UploadedFile{
Content: input.LogoFile.File,
Filename: input.LogoFile.Filename,
ContentType: input.LogoFile.ContentType,
Size: input.LogoFile.Size,
}
}
if input.HorizontalLogoFile != nil {
horizontalLogoFile = &iam.UploadedFile{
Content: input.HorizontalLogoFile.File,
Filename: input.HorizontalLogoFile.Filename,
ContentType: input.HorizontalLogoFile.ContentType,
Size: input.HorizontalLogoFile.Size,
}
}
organization, err := r.iam.OrganizationService.CreateOrganization(
ctx,
identity.ID,
&iam.CreateOrganizationRequest{
Name: input.Name,
LogoFile: logoFile,
HorizontalLogoFile: horizontalLogoFile,
},
)
if err != nil {
panic(fmt.Errorf("cannot create organization: %w", err))
}
return &types.CreateOrganizationPayload{
Organization: types.NewOrganization(organization),
}, nil
}
// UpdateOrganization is the resolver for the updateOrganization field.
func (r *mutationResolver) UpdateOrganization(ctx context.Context, input types.UpdateOrganizationInput) (*types.UpdateOrganizationPayload, error) {
organization, err := r.iam.OrganizationService.UpdateOrganization(
ctx,
input.OrganizationID,
&iam.UpdateOrganizationRequest{
Name: input.Name,
},
)
if err != nil {
panic(fmt.Errorf("cannot update organization: %w", err))
}
return &types.UpdateOrganizationPayload{
Organization: &types.Organization{
ID: organization.ID,
Name: organization.Name,
CreatedAt: organization.CreatedAt,
UpdatedAt: organization.UpdatedAt,
},
}, nil
}
// DeleteOrganization is the resolver for the deleteOrganization field.
func (r *mutationResolver) DeleteOrganization(ctx context.Context, input types.DeleteOrganizationInput) (*types.DeleteOrganizationPayload, error) {
err := r.iam.OrganizationService.DeleteOrganization(ctx, input.OrganizationID)
if err != nil {
panic(fmt.Errorf("cannot delete organization: %w", err))
}
return &types.DeleteOrganizationPayload{DeletedOrganizationID: input.OrganizationID}, nil
}
// InviteMember is the resolver for the inviteMember field.
func (r *mutationResolver) InviteMember(ctx context.Context, input types.InviteMemberInput) (*types.InviteMemberPayload, error) {
invitation, err := r.iam.OrganizationService.InviteMember(
ctx,
input.OrganizationID,
input.Email,
input.FullName,
coredata.MembershipRoleViewer,
)
if err != nil {
var errOrganizationNotFound *iam.ErrOrganizationNotFound
var errMembershipAlreadyExists *iam.ErrMembershipAlreadyExists
if errors.As(err, &errOrganizationNotFound) {
return nil, gqlutils.NotFound(err)
}
if errors.As(err, &errMembershipAlreadyExists) {
return nil, gqlutils.Conflict(err)
}
panic(fmt.Errorf("cannot add member to organization: %w", err))
}
return &types.InviteMemberPayload{
InvitationEdge: types.NewInvitationEdge(invitation, coredata.InvitationOrderFieldCreatedAt),
}, nil
}
// DeleteInvitation is the resolver for the deleteInvitation field.
func (r *mutationResolver) DeleteInvitation(ctx context.Context, input types.DeleteInvitationInput) (*types.DeleteInvitationPayload, error) {
err := r.iam.OrganizationService.DeleteInvitation(ctx, input.OrganizationID, input.InvitationID)
if err != nil {
var errInvitationNotFound *iam.ErrInvitationNotFound
var errInvitationNotPending *iam.ErrInvitationNotPending
if errors.As(err, &errInvitationNotFound) {
return nil, gqlutils.NotFound(err)
}
if errors.As(err, &errInvitationNotPending) {
return nil, gqlutils.Invalid(err, nil)
}
panic(fmt.Errorf("cannot delete invitation: %w", err))
}
return &types.DeleteInvitationPayload{DeletedInvitationID: input.InvitationID}, nil
}
// RemoveMember is the resolver for the removeMember field.
func (r *mutationResolver) RemoveMember(ctx context.Context, input types.RemoveMemberInput) (*types.RemoveMemberPayload, error) {
err := r.iam.OrganizationService.RemoveMember(ctx, input.OrganizationID, input.MembershipID)
if err != nil {
panic(fmt.Errorf("cannot remove member from organization: %w", err))
}
return &types.RemoveMemberPayload{DeletedMembershipID: input.MembershipID}, nil
}
// AcceptInvitation is the resolver for the acceptInvitation field.
func (r *mutationResolver) AcceptInvitation(ctx context.Context, input types.AcceptInvitationInput) (*types.AcceptInvitationPayload, error) {
identity := UserFromContext(ctx)
membership, err := r.iam.AccountService.AcceptInvitation(ctx, identity.ID, input.InvitationID)
if err != nil {
panic(fmt.Errorf("cannot accept invitation: %w", err))
}
return &types.AcceptInvitationPayload{
MembershipEdge: types.NewMembershipEdge(membership, coredata.MembershipOrderFieldCreatedAt),
}, nil
}
// CreateSAMLConfiguration is the resolver for the createSAMLConfiguration field.
func (r *mutationResolver) CreateSAMLConfiguration(ctx context.Context, input types.CreateSAMLConfigurationInput) (*types.CreateSAMLConfigurationPayload, error) {
req := &iam.CreateSAMLConfigurationRequest{
EmailDomain: input.EmailDomain,
IdPEntityID: input.IdpEntityID,
IdPSsoURL: input.IdpSsoURL,
IdPCertificate: input.IdpCertificate,
AutoSignupEnabled: input.AutoSignupEnabled,
}
if input.AttributeMappings != nil {
req.AttributeEmail = input.AttributeMappings.Email
req.AttributeFirstname = input.AttributeMappings.FirstName
req.AttributeLastname = input.AttributeMappings.LastName
req.AttributeRole = input.AttributeMappings.Role
}
samlConfiguration, err := r.iam.OrganizationService.CreateSAMLConfiguration(
ctx,
input.OrganizationID,
req,
)
if err != nil {
panic(fmt.Errorf("cannot create saml configuration: %w", err))
}
return &types.CreateSAMLConfigurationPayload{
SamlConfigurationEdge: types.NewSAMLConfigurationEdge(
samlConfiguration,
coredata.SAMLConfigurationOrderFieldCreatedAt,
),
}, nil
}
// UpdateSAMLConfiguration is the resolver for the updateSAMLConfiguration field.
func (r *mutationResolver) UpdateSAMLConfiguration(ctx context.Context, input types.UpdateSAMLConfigurationInput) (*types.UpdateSAMLConfigurationPayload, error) {
req := &iam.UpdateSAMLConfigurationRequest{
IdPEntityID: input.IdpEntityID,
IdPSsoURL: input.IdpSsoURL,
IdPCertificate: input.IdpCertificate,
AutoSignupEnabled: input.AutoSignupEnabled,
}
if input.AttributeMappings != nil {
req.AttributeEmail = input.AttributeMappings.Email
req.AttributeFirstname = input.AttributeMappings.FirstName
req.AttributeLastname = input.AttributeMappings.LastName
req.AttributeRole = input.AttributeMappings.Role
}
samlConfiguration, err := r.iam.OrganizationService.UpdateSAMLConfiguration(
ctx,
input.OrganizationID,
input.SamlConfigurationID,
req,
)
if err != nil {
panic(fmt.Errorf("cannot update saml configuration: %w", err))
}
return &types.UpdateSAMLConfigurationPayload{
SamlConfiguration: types.NewSAMLConfiguration(samlConfiguration),
}, nil
}
// DeleteSAMLConfiguration is the resolver for the deleteSAMLConfiguration field.
func (r *mutationResolver) DeleteSAMLConfiguration(ctx context.Context, input types.DeleteSAMLConfigurationInput) (*types.DeleteSAMLConfigurationPayload, error) {
err := r.iam.OrganizationService.DeleteSAMLConfiguration(ctx, input.OrganizationID, input.SamlConfigurationID)
if err != nil {
panic(fmt.Errorf("cannot delete saml configuration: %w", err))
}
return &types.DeleteSAMLConfigurationPayload{DeletedSamlConfigurationID: input.SamlConfigurationID}, nil
}
// LogoURL is the resolver for the logoUrl field.
func (r *organizationResolver) LogoURL(ctx context.Context, obj *types.Organization) (*string, error) {
presignedURL, err := r.iam.OrganizationService.GenerateLogoURL(ctx, obj.ID, 1*time.Hour)
if err != nil {
panic(fmt.Errorf("cannot generate logo URL: %w", err))
}
return presignedURL, nil
}
// HorizontalLogoURL is the resolver for the horizontalLogoUrl field.
func (r *organizationResolver) HorizontalLogoURL(ctx context.Context, obj *types.Organization) (*string, error) {
presignedURL, err := r.iam.OrganizationService.GenerateHorizontalLogoURL(ctx, obj.ID, 1*time.Hour)
if err != nil {
panic(fmt.Errorf("cannot generate horizontal logo URL: %w", err))
}
return presignedURL, nil
}
// Members is the resolver for the members field.
func (r *organizationResolver) Members(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.MembershipConnection, error) {
pageOrderBy := page.OrderBy[coredata.MembershipOrderField]{
Field: coredata.MembershipOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
cursor := cursor.NewCursor(first, after, last, before, pageOrderBy)
page, err := r.iam.OrganizationService.ListMembers(ctx, obj.ID, cursor)
if err != nil {
panic(fmt.Errorf("cannot list memberships: %w", err))
}
return types.NewMembershipConnection(page, r, obj.ID), nil
}
// Invitations is the resolver for the invitations field.
func (r *organizationResolver) Invitations(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, status *coredata.InvitationStatus) (*types.InvitationConnection, error) {
pageOrderBy := page.OrderBy[coredata.InvitationOrderField]{
Field: coredata.InvitationOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
filters := coredata.NewInvitationFilter(nil)
if status != nil {
filters = coredata.NewInvitationFilter([]coredata.InvitationStatus{*status})
}
cursor := cursor.NewCursor(first, after, last, before, pageOrderBy)
page, err := r.iam.OrganizationService.ListInvitations(ctx, obj.ID, cursor, filters)
if err != nil {
panic(fmt.Errorf("cannot list invitations: %w", err))
}
return types.NewInvitationConnection(page, r, obj.ID, filters), nil
}
// SamlConfigurations is the resolver for the samlConfigurations field.
func (r *organizationResolver) SamlConfigurations(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.SAMLConfigurationConnection, error) {
pageOrderBy := page.OrderBy[coredata.SAMLConfigurationOrderField]{
Field: coredata.SAMLConfigurationOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
cursor := cursor.NewCursor(first, after, last, before, pageOrderBy)
page, err := r.iam.OrganizationService.ListSAMLConfigurations(ctx, obj.ID, cursor)
if err != nil {
panic(fmt.Errorf("cannot list saml configurations: %w", err))
}
return types.NewSAMLConfigurationConnection(page, r, obj.ID), nil
}
// TotalCount is the resolver for the totalCount field.
func (r *personalAPIKeyConnectionResolver) TotalCount(ctx context.Context, obj *types.PersonalAPIKeyConnection) (int, error) {
switch obj.Resolver.(type) {
case *identityResolver:
count, err := r.iam.AccountService.CountPersonalAPIKeys(ctx, obj.ParentID)
if err != nil {
panic(fmt.Errorf("cannot count personal api keys: %w", err))
}
return count, nil
}
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
}
// Node is the resolver for the node field.
func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error) {
var loadNode func(ctx context.Context, id gid.GID) (types.Node, error)
user := UserFromContext(ctx)
r.iam.AccessManagementService.Authorize(ctx, user.ID, nil, id, iam.ActionGet)
switch id.EntityType() {
case coredata.OrganizationEntityType:
loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) {
organization, err := r.iam.OrganizationService.GetOrganization(ctx, id)
if err != nil {
return nil, err
}
return types.NewOrganization(organization), nil
}
case coredata.UserEntityType:
loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) {
identity, err := r.iam.AccountService.GetIdentity(ctx, id)
if err != nil {
return nil, err
}
return types.NewIdentity(identity), nil
}
case coredata.SessionEntityType:
loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) {
session, err := r.iam.GetSession(ctx, id)
if err != nil {
return nil, err
}
return types.NewSession(session), nil
}
case coredata.MembershipEntityType:
loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) {
membership, err := r.iam.GetMembership(ctx, id)
if err != nil {
return nil, err
}
return types.NewMembership(membership), nil
}
case coredata.InvitationEntityType:
loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) {
invitation, err := r.iam.GetInvitation(ctx, id)
if err != nil {
return nil, err
}
return types.NewInvitation(invitation), nil
}
default:
return nil, fmt.Errorf("unsupported entity type: %d", id.EntityType())
}
node, err := loadNode(ctx, id)
if err != nil {
var (
errOrganizationNotFound *iam.ErrOrganizationNotFound
errIdentityNotFound *iam.ErrUserNotFound
errSessionNotFound *iam.ErrSessionNotFound
errMembershipNotFound *iam.ErrMembershipNotFound
errInvitationNotFound *iam.ErrInvitationNotFound
)
if errors.As(err, &errOrganizationNotFound) ||
errors.As(err, &errIdentityNotFound) ||
errors.As(err, &errSessionNotFound) ||
errors.As(err, &errMembershipNotFound) ||
errors.As(err, &errInvitationNotFound) {
return nil, gqlutils.NotFound(err)
}
return nil, err
}
return node, nil
}
// Viewer is the resolver for the viewer field.
func (r *queryResolver) Viewer(ctx context.Context) (*types.Identity, error) {
user := UserFromContext(ctx)
return &types.Identity{
ID: user.ID,
Email: user.EmailAddress,
EmailVerified: user.EmailAddressVerified,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}, nil
}
// CheckSSOAvailability is the resolver for the checkSSOAvailability field.
func (r *queryResolver) CheckSSOAvailability(ctx context.Context, email string) (*types.SSOAvailability, error) {
panic(fmt.Errorf("not implemented: CheckSSOAvailability - checkSSOAvailability"))
}
// TotalCount is the resolver for the totalCount field.
func (r *sAMLConfigurationConnectionResolver) TotalCount(ctx context.Context, obj *types.SAMLConfigurationConnection) (int, error) {
switch obj.Resolver.(type) {
case *organizationResolver:
count, err := r.iam.OrganizationService.CountSAMLConfigurations(ctx, obj.ParentID)
if err != nil {
panic(fmt.Errorf("cannot count saml configurations: %w", err))
}
return count, nil
}
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
}
// TotalCount is the resolver for the totalCount field.
func (r *sessionConnectionResolver) TotalCount(ctx context.Context, obj *types.SessionConnection) (int, error) {
switch obj.Resolver.(type) {
case *identityResolver:
count, err := r.iam.AccountService.CountSessions(ctx, obj.ParentID)
if err != nil {
panic(fmt.Errorf("cannot count sessions: %w", err))
}
return count, nil
}
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
}
// Identity returns schema.IdentityResolver implementation.
func (r *Resolver) Identity() schema.IdentityResolver { return &identityResolver{r} }
// InvitationConnection returns schema.InvitationConnectionResolver implementation.
func (r *Resolver) InvitationConnection() schema.InvitationConnectionResolver {
return &invitationConnectionResolver{r}
}
// Membership returns schema.MembershipResolver implementation.
func (r *Resolver) Membership() schema.MembershipResolver { return &membershipResolver{r} }
// MembershipConnection returns schema.MembershipConnectionResolver implementation.
func (r *Resolver) MembershipConnection() schema.MembershipConnectionResolver {
return &membershipConnectionResolver{r}
}
// Mutation returns schema.MutationResolver implementation.
func (r *Resolver) Mutation() schema.MutationResolver { return &mutationResolver{r} }
// Organization returns schema.OrganizationResolver implementation.
func (r *Resolver) Organization() schema.OrganizationResolver { return &organizationResolver{r} }
// PersonalAPIKeyConnection returns schema.PersonalAPIKeyConnectionResolver implementation.
func (r *Resolver) PersonalAPIKeyConnection() schema.PersonalAPIKeyConnectionResolver {
return &personalAPIKeyConnectionResolver{r}
}
// Query returns schema.QueryResolver implementation.
func (r *Resolver) Query() schema.QueryResolver { return &queryResolver{r} }
// SAMLConfigurationConnection returns schema.SAMLConfigurationConnectionResolver implementation.
func (r *Resolver) SAMLConfigurationConnection() schema.SAMLConfigurationConnectionResolver {
return &sAMLConfigurationConnectionResolver{r}
}
// SessionConnection returns schema.SessionConnectionResolver implementation.
func (r *Resolver) SessionConnection() schema.SessionConnectionResolver {
return &sessionConnectionResolver{r}
}
type identityResolver struct{ *Resolver }
type invitationConnectionResolver struct{ *Resolver }
type membershipResolver struct{ *Resolver }
type membershipConnectionResolver struct{ *Resolver }
type mutationResolver struct{ *Resolver }
type organizationResolver struct{ *Resolver }
type personalAPIKeyConnectionResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
type sAMLConfigurationConnectionResolver struct{ *Resolver }
type sessionConnectionResolver struct{ *Resolver }