From a5f5a70e418d9fab580aab98c1bb524295816efc Mon Sep 17 00:00:00 2001 From: Nimish <85357445+nimish-ks@users.noreply.github.com> Date: Mon, 25 Sep 2023 11:27:35 +0530 Subject: [PATCH] Multi user (#57) * fix: clean up console.log * feat: grant user access to all envs when given admin role * feat: better colors for alert * feat: disallow changing env scope for admins * feat: misc improvements to alert style * fix: disable save button when user is admin * fix: dialog layout * fix: misc ui improvements * fix: ui fixes to tokens dialog * fix: misc ui fixes and cleanup * feat: user tokens page, misc styling improvements * fix: wire app new app dialog with keyring context * fix: graphql mutation operation name * fix: operation name * fix: veryify invite in useEffect hook --------- Co-authored-by: rohan-chaturvedi --- backend/backend/graphene/mutations/app.py | 12 +- frontend/apollo/gql.ts | 8 +- frontend/apollo/graphql.ts | 10 +- .../app/[team]/apps/[app]/members/page.tsx | 131 +++-- .../app/[team]/apps/[app]/tokens/page.tsx | 76 +-- frontend/app/[team]/layout.tsx | 1 - frontend/app/[team]/members/page.tsx | 205 +++++-- frontend/app/[team]/tokens/page.tsx | 508 ++++++++++++++++++ frontend/app/invite/[invite]/page.tsx | 19 +- frontend/components/apps/NewAppDialog.tsx | 89 +-- .../components/apps/tokens/SecretTokens.tsx | 6 +- .../components/auth/UnlockKeyringDialog.tsx | 8 +- frontend/components/common/Alert.tsx | 28 +- frontend/components/layout/Sidebar.tsx | 21 +- .../mutations/organisation/deleteInvite.gql | 2 +- frontend/utils/auth.ts | 3 +- 16 files changed, 890 insertions(+), 237 deletions(-) create mode 100644 frontend/app/[team]/tokens/page.tsx diff --git a/backend/backend/graphene/mutations/app.py b/backend/backend/graphene/mutations/app.py index 274aa902..0f1633d2 100644 --- a/backend/backend/graphene/mutations/app.py +++ b/backend/backend/graphene/mutations/app.py @@ -138,13 +138,11 @@ def mutate(cls, root, info, member_id, app_id, env_keys): org_member = OrganisationMember.objects.get( id=member_id, deleted_at=None) - if org_member in app.members.all(): - raise GraphQLError("This user already has access to this app") - else: - app.members.add(org_member) - for key in env_keys: - EnvironmentKey.objects.create( - environment_id=key.env_id, user_id=key.user_id, wrapped_seed=key.wrapped_seed, wrapped_salt=key.wrapped_salt, identity_key=key.identity_key) + + app.members.add(org_member) + for key in env_keys: + EnvironmentKey.objects.create( + environment_id=key.env_id, user_id=key.user_id, wrapped_seed=key.wrapped_seed, wrapped_salt=key.wrapped_salt, identity_key=key.identity_key) return AddAppMemberMutation(app=app) diff --git a/frontend/apollo/gql.ts b/frontend/apollo/gql.ts index b38fb386..7725e10c 100644 --- a/frontend/apollo/gql.ts +++ b/frontend/apollo/gql.ts @@ -30,7 +30,7 @@ const documents = { "mutation UpdateSecret($id: ID!, $secretData: SecretInput!) {\n editSecret(id: $id, secretData: $secretData) {\n secret {\n id\n updatedAt\n }\n }\n}": types.UpdateSecretDocument, "mutation InitAppEnvironments($devEnv: EnvironmentInput!, $stagingEnv: EnvironmentInput!, $prodEnv: EnvironmentInput!, $devAdminKeys: [EnvironmentKeyInput], $stagAdminKeys: [EnvironmentKeyInput], $prodAdminKeys: [EnvironmentKeyInput]) {\n devEnvironment: createEnvironment(\n environmentData: $devEnv\n adminKeys: $devAdminKeys\n ) {\n environment {\n id\n name\n createdAt\n identityKey\n }\n }\n stagingEnvironment: createEnvironment(\n environmentData: $stagingEnv\n adminKeys: $stagAdminKeys\n ) {\n environment {\n id\n name\n createdAt\n identityKey\n }\n }\n prodEnvironment: createEnvironment(\n environmentData: $prodEnv\n adminKeys: $prodAdminKeys\n ) {\n environment {\n id\n name\n createdAt\n identityKey\n }\n }\n}": types.InitAppEnvironmentsDocument, "mutation AcceptOrganisationInvite($orgId: ID!, $identityKey: String!, $wrappedKeyring: String!, $inviteId: ID!) {\n createOrganisationMember(\n orgId: $orgId\n identityKey: $identityKey\n wrappedKeyring: $wrappedKeyring\n inviteId: $inviteId\n ) {\n orgMember {\n id\n email\n createdAt\n role\n }\n }\n}": types.AcceptOrganisationInviteDocument, - "mutation DeleteInvite($inviteId: ID!) {\n deleteInvitation(inviteId: $inviteId) {\n ok\n }\n}": types.DeleteInviteDocument, + "mutation DeleteOrgInvite($inviteId: ID!) {\n deleteInvitation(inviteId: $inviteId) {\n ok\n }\n}": types.DeleteOrgInviteDocument, "mutation RemoveMember($memberId: ID!) {\n deleteOrganisationMember(memberId: $memberId) {\n ok\n }\n}": types.RemoveMemberDocument, "mutation InviteMember($orgId: ID!, $email: String!, $apps: [String], $role: String) {\n inviteOrganisationMember(orgId: $orgId, email: $email, apps: $apps, role: $role) {\n invite {\n id\n }\n }\n}": types.InviteMemberDocument, "mutation UpdateMemberRole($memberId: ID!, $role: String!) {\n updateOrganisationMemberRole(memberId: $memberId, role: $role) {\n orgMember {\n id\n role\n }\n }\n}": types.UpdateMemberRoleDocument, @@ -44,7 +44,7 @@ const documents = { "query GetAppLogs($appId: ID!, $start: BigInt, $end: BigInt) {\n logs(appId: $appId, start: $start, end: $end) {\n id\n timestamp\n phaseNode\n eventType\n ipAddress\n country\n city\n phSize\n }\n logsCount(appId: $appId)\n}": types.GetAppLogsDocument, "query GetApps($organisationId: ID!, $appId: ID!) {\n apps(organisationId: $organisationId, appId: $appId) {\n id\n name\n identityKey\n createdAt\n }\n}": types.GetAppsDocument, "query GetOrganisations {\n organisations {\n id\n name\n identityKey\n createdAt\n plan\n role\n memberId\n }\n}": types.GetOrganisationsDocument, - "query GetInvites($orgId: ID!) {\n organisationInvites(orgId: $orgId) {\n id\n createdAt\n expiresAt\n invitedBy {\n email\n }\n inviteeEmail\n }\n}": types.GetInvitesDocument, + "query GetInvites($orgId: ID!) {\n organisationInvites(orgId: $orgId) {\n id\n createdAt\n expiresAt\n invitedBy {\n email\n fullName\n }\n inviteeEmail\n }\n}": types.GetInvitesDocument, "query GetOrganisationAdminsAndSelf($organisationId: ID!) {\n organisationAdminsAndSelf(organisationId: $organisationId) {\n id\n role\n identityKey\n }\n}": types.GetOrganisationAdminsAndSelfDocument, "query GetOrganisationMembers($organisationId: ID!, $role: [String]) {\n organisationMembers(organisationId: $organisationId, role: $role) {\n id\n role\n identityKey\n email\n fullName\n avatarUrl\n createdAt\n }\n}": types.GetOrganisationMembersDocument, "query VerifyInvite($inviteId: ID!) {\n validateInvite(inviteId: $inviteId) {\n id\n organisation {\n id\n name\n }\n inviteeEmail\n invitedBy {\n email\n }\n apps {\n id\n name\n }\n }\n}": types.VerifyInviteDocument, @@ -143,7 +143,7 @@ export function graphql(source: "mutation AcceptOrganisationInvite($orgId: ID!, /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "mutation DeleteInvite($inviteId: ID!) {\n deleteInvitation(inviteId: $inviteId) {\n ok\n }\n}"): (typeof documents)["mutation DeleteInvite($inviteId: ID!) {\n deleteInvitation(inviteId: $inviteId) {\n ok\n }\n}"]; +export function graphql(source: "mutation DeleteOrgInvite($inviteId: ID!) {\n deleteInvitation(inviteId: $inviteId) {\n ok\n }\n}"): (typeof documents)["mutation DeleteOrgInvite($inviteId: ID!) {\n deleteInvitation(inviteId: $inviteId) {\n ok\n }\n}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -199,7 +199,7 @@ export function graphql(source: "query GetOrganisations {\n organisations {\n /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query GetInvites($orgId: ID!) {\n organisationInvites(orgId: $orgId) {\n id\n createdAt\n expiresAt\n invitedBy {\n email\n }\n inviteeEmail\n }\n}"): (typeof documents)["query GetInvites($orgId: ID!) {\n organisationInvites(orgId: $orgId) {\n id\n createdAt\n expiresAt\n invitedBy {\n email\n }\n inviteeEmail\n }\n}"]; +export function graphql(source: "query GetInvites($orgId: ID!) {\n organisationInvites(orgId: $orgId) {\n id\n createdAt\n expiresAt\n invitedBy {\n email\n fullName\n }\n inviteeEmail\n }\n}"): (typeof documents)["query GetInvites($orgId: ID!) {\n organisationInvites(orgId: $orgId) {\n id\n createdAt\n expiresAt\n invitedBy {\n email\n fullName\n }\n inviteeEmail\n }\n}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/frontend/apollo/graphql.ts b/frontend/apollo/graphql.ts index a6d311c2..485401f2 100644 --- a/frontend/apollo/graphql.ts +++ b/frontend/apollo/graphql.ts @@ -893,12 +893,12 @@ export type AcceptOrganisationInviteMutationVariables = Exact<{ export type AcceptOrganisationInviteMutation = { __typename?: 'Mutation', createOrganisationMember?: { __typename?: 'CreateOrganisationMemberMutation', orgMember?: { __typename?: 'OrganisationMemberType', id: string, email?: string | null, createdAt?: any | null, role: ApiOrganisationMemberRoleChoices } | null } | null }; -export type DeleteInviteMutationVariables = Exact<{ +export type DeleteOrgInviteMutationVariables = Exact<{ inviteId: Scalars['ID']; }>; -export type DeleteInviteMutation = { __typename?: 'Mutation', deleteInvitation?: { __typename?: 'DeleteInviteMutation', ok?: boolean | null } | null }; +export type DeleteOrgInviteMutation = { __typename?: 'Mutation', deleteInvitation?: { __typename?: 'DeleteInviteMutation', ok?: boolean | null } | null }; export type RemoveMemberMutationVariables = Exact<{ memberId: Scalars['ID']; @@ -1011,7 +1011,7 @@ export type GetInvitesQueryVariables = Exact<{ }>; -export type GetInvitesQuery = { __typename?: 'Query', organisationInvites?: Array<{ __typename?: 'OrganisationMemberInviteType', id: string, createdAt?: any | null, expiresAt: any, inviteeEmail: string, invitedBy: { __typename?: 'OrganisationMemberType', email?: string | null } } | null> | null }; +export type GetInvitesQuery = { __typename?: 'Query', organisationInvites?: Array<{ __typename?: 'OrganisationMemberInviteType', id: string, createdAt?: any | null, expiresAt: any, inviteeEmail: string, invitedBy: { __typename?: 'OrganisationMemberType', email?: string | null, fullName?: string | null } } | null> | null }; export type GetOrganisationAdminsAndSelfQueryVariables = Exact<{ organisationId: Scalars['ID']; @@ -1111,7 +1111,7 @@ export const RevokeServiceTokenDocument = {"kind":"Document","definitions":[{"ki export const UpdateSecretDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateSecret"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"secretData"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SecretInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"editSecret"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"secretData"},"value":{"kind":"Variable","name":{"kind":"Name","value":"secretData"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"secret"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]}}]} as unknown as DocumentNode; export const InitAppEnvironmentsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InitAppEnvironments"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"devEnv"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EnvironmentInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"stagingEnv"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EnvironmentInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"prodEnv"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EnvironmentInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"devAdminKeys"}},"type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EnvironmentKeyInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"stagAdminKeys"}},"type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EnvironmentKeyInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"prodAdminKeys"}},"type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EnvironmentKeyInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"devEnvironment"},"name":{"kind":"Name","value":"createEnvironment"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"environmentData"},"value":{"kind":"Variable","name":{"kind":"Name","value":"devEnv"}}},{"kind":"Argument","name":{"kind":"Name","value":"adminKeys"},"value":{"kind":"Variable","name":{"kind":"Name","value":"devAdminKeys"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"environment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}}]}}]}},{"kind":"Field","alias":{"kind":"Name","value":"stagingEnvironment"},"name":{"kind":"Name","value":"createEnvironment"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"environmentData"},"value":{"kind":"Variable","name":{"kind":"Name","value":"stagingEnv"}}},{"kind":"Argument","name":{"kind":"Name","value":"adminKeys"},"value":{"kind":"Variable","name":{"kind":"Name","value":"stagAdminKeys"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"environment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}}]}}]}},{"kind":"Field","alias":{"kind":"Name","value":"prodEnvironment"},"name":{"kind":"Name","value":"createEnvironment"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"environmentData"},"value":{"kind":"Variable","name":{"kind":"Name","value":"prodEnv"}}},{"kind":"Argument","name":{"kind":"Name","value":"adminKeys"},"value":{"kind":"Variable","name":{"kind":"Name","value":"prodAdminKeys"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"environment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}}]}}]}}]}}]} as unknown as DocumentNode; export const AcceptOrganisationInviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AcceptOrganisationInvite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"identityKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyring"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"inviteId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOrganisationMember"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orgId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}}},{"kind":"Argument","name":{"kind":"Name","value":"identityKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"identityKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"wrappedKeyring"},"value":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyring"}}},{"kind":"Argument","name":{"kind":"Name","value":"inviteId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"inviteId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"orgMember"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]}}]} as unknown as DocumentNode; -export const DeleteInviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteInvite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"inviteId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteInvitation"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"inviteId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"inviteId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]} as unknown as DocumentNode; +export const DeleteOrgInviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOrgInvite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"inviteId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteInvitation"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"inviteId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"inviteId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]} as unknown as DocumentNode; export const RemoveMemberDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RemoveMember"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOrganisationMember"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"memberId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]} as unknown as DocumentNode; export const InviteMemberDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InviteMember"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"email"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"apps"}},"type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"role"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"inviteOrganisationMember"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orgId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}}},{"kind":"Argument","name":{"kind":"Name","value":"email"},"value":{"kind":"Variable","name":{"kind":"Name","value":"email"}}},{"kind":"Argument","name":{"kind":"Name","value":"apps"},"value":{"kind":"Variable","name":{"kind":"Name","value":"apps"}}},{"kind":"Argument","name":{"kind":"Name","value":"role"},"value":{"kind":"Variable","name":{"kind":"Name","value":"role"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"invite"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; export const UpdateMemberRoleDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateMemberRole"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"role"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateOrganisationMemberRole"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"memberId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}}},{"kind":"Argument","name":{"kind":"Name","value":"role"},"value":{"kind":"Variable","name":{"kind":"Name","value":"role"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"orgMember"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]}}]} as unknown as DocumentNode; @@ -1125,7 +1125,7 @@ export const GetAppLogCountDocument = {"kind":"Document","definitions":[{"kind": export const GetAppLogsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAppLogs"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"start"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"BigInt"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"end"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"BigInt"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"logs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}},{"kind":"Argument","name":{"kind":"Name","value":"start"},"value":{"kind":"Variable","name":{"kind":"Name","value":"start"}}},{"kind":"Argument","name":{"kind":"Name","value":"end"},"value":{"kind":"Variable","name":{"kind":"Name","value":"end"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"phaseNode"}},{"kind":"Field","name":{"kind":"Name","value":"eventType"}},{"kind":"Field","name":{"kind":"Name","value":"ipAddress"}},{"kind":"Field","name":{"kind":"Name","value":"country"}},{"kind":"Field","name":{"kind":"Name","value":"city"}},{"kind":"Field","name":{"kind":"Name","value":"phSize"}}]}},{"kind":"Field","name":{"kind":"Name","value":"logsCount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}}]}]}}]} as unknown as DocumentNode; export const GetAppsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetApps"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apps"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"organisationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]} as unknown as DocumentNode; export const GetOrganisationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOrganisations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"organisations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"plan"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"memberId"}}]}}]}}]} as unknown as DocumentNode; -export const GetInvitesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetInvites"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"organisationInvites"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orgId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"email"}}]}},{"kind":"Field","name":{"kind":"Name","value":"inviteeEmail"}}]}}]}}]} as unknown as DocumentNode; +export const GetInvitesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetInvites"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"organisationInvites"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orgId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"fullName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"inviteeEmail"}}]}}]}}]} as unknown as DocumentNode; export const GetOrganisationAdminsAndSelfDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOrganisationAdminsAndSelf"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"organisationAdminsAndSelf"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"organisationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}}]}}]}}]} as unknown as DocumentNode; export const GetOrganisationMembersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOrganisationMembers"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"role"}},"type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"organisationMembers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"organisationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"role"},"value":{"kind":"Variable","name":{"kind":"Name","value":"role"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]} as unknown as DocumentNode; export const VerifyInviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"VerifyInvite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"inviteId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"validateInvite"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"inviteId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"inviteId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"organisation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"inviteeEmail"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"email"}}]}},{"kind":"Field","name":{"kind":"Name","value":"apps"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode; diff --git a/frontend/app/[team]/apps/[app]/members/page.tsx b/frontend/app/[team]/apps/[app]/members/page.tsx index 29dd883a..51469d67 100644 --- a/frontend/app/[team]/apps/[app]/members/page.tsx +++ b/frontend/app/[team]/apps/[app]/members/page.tsx @@ -24,10 +24,8 @@ import { FaTimes, FaUserCog, FaUserTimes, - FaUsersCog, } from 'react-icons/fa' import clsx from 'clsx' -import { copyToClipBoard } from '@/utils/clipboard' import { toast } from 'react-toastify' import { useSession } from 'next-auth/react' import { Avatar } from '@/components/common/Avatar' @@ -36,6 +34,8 @@ import { unwrapEnvSecretsForUser, wrapEnvSecretsForUser } from '@/utils/environm import { OrganisationKeyring, cryptoUtils } from '@/utils/auth' import { userIsAdmin } from '@/utils/permissions' import { RoleLabel } from '@/components/users/RoleLabel' +import { Alert } from '@/components/common/Alert' +import Link from 'next/link' export default function Members({ params }: { params: { team: string; app: string } }) { const { data } = useQuery(GetAppMembers, { variables: { appId: params.app } }) @@ -234,8 +234,20 @@ export default function Members({ params }: { params: { team: string; app: strin {memberOptions.length === 0 ? ( -
- All organisation members are added to this App. +
+ +

+ All organisation members are added to this App. You can invite more + users from the{' '} + + organisation members + {' '} + page. +

+
) : (
@@ -330,7 +342,7 @@ export default function Members({ params }: { params: { team: string; app: strin -
+
{envScope .map((env: Partial) => env.name) @@ -353,7 +365,7 @@ export default function Members({ params }: { params: { team: string; app: strin leaveTo="transform scale-95 opacity-0" > -
+
{envOptions.map((env: Partial) => ( {({ active, selected }) => ( @@ -387,7 +399,7 @@ export default function Members({ params }: { params: { team: string; app: strin > Sudo password -
+
- -
+ +
{envScope.length === 0 && showEnvHint && ( Select an environment scope @@ -712,6 +729,7 @@ export default function Members({ params }: { params: { team: string; app: strin onChange={setEnvScope} multiple name="environments" + disabled={memberIsAdmin} > {({ open }) => ( <> @@ -724,7 +742,12 @@ export default function Members({ params }: { params: { team: string; app: strin -
+
{envScope .map((env: Partial) => env.name) @@ -747,7 +770,7 @@ export default function Members({ params }: { params: { team: string; app: strin leaveTo="transform scale-95 opacity-0" > -
+
{envOptions.map((env: Partial) => ( {({ active, selected }) => ( @@ -772,47 +795,63 @@ export default function Members({ params }: { params: { team: string; app: strin
- - {!keyring && ( -
- -
- setPassword(e.target.value)} - type={showPw ? 'text' : 'password'} - minLength={16} - required - autoFocus - className="custom w-full text-zinc-800 font-mono dark:text-white bg-zinc-100 dark:bg-zinc-800" - /> - -
-
- )} )}
+ {!keyring && !memberIsAdmin && ( +
+ +
+ setPassword(e.target.value)} + type={showPw ? 'text' : 'password'} + minLength={16} + required + autoFocus + className="custom w-full text-zinc-800 font-mono dark:text-white bg-zinc-100 dark:bg-zinc-800 rounded-md" + /> + +
+
+ )} + + {memberIsAdmin && ( + +

+ This user is an , and has access to all + environments in this App. To restrict their access, change their role to{' '} + from the{' '} + + organisation members + {' '} + page. +

+
+ )}
-
@@ -829,7 +868,7 @@ export default function Members({ params }: { params: { team: string; app: strin return (
-
+
diff --git a/frontend/app/[team]/apps/[app]/tokens/page.tsx b/frontend/app/[team]/apps/[app]/tokens/page.tsx index 9c03cff7..aa5b02e1 100644 --- a/frontend/app/[team]/apps/[app]/tokens/page.tsx +++ b/frontend/app/[team]/apps/[app]/tokens/page.tsx @@ -19,6 +19,7 @@ import UnlockKeyringDialog from '@/components/auth/UnlockKeyringDialog' import { KeyringContext } from '@/contexts/keyringContext' import clsx from 'clsx' import { SecretTokens } from '@/components/apps/tokens/SecretTokens' +import { organisationContext } from '@/contexts/organisationContext' export default function Tokens({ params }: { params: { team: string; app: string } }) { const { data: orgsData } = useQuery(GetOrganisations) @@ -28,7 +29,7 @@ export default function Tokens({ params }: { params: { team: string; app: string const [activePanel, setActivePanel] = useState<'secrets' | 'kms'>('secrets') - const organisationId = orgsData?.organisations[0].id + const { activeOrganisation: organisation } = useContext(organisationContext) const { keyring } = useContext(KeyringContext) @@ -183,33 +184,6 @@ export default function Tokens({ params }: { params: { team: string; app: string
- {/*
- -
- setPw(e.target.value)} - type={showPw ? 'text' : 'password'} - minLength={16} - required - className="w-full " - /> - -
-
*/}
+
+ + + {}}> + +
+ + +
+
+ + + +

+ Create a new User token +

+ + +
+ + {userToken ? ( +
+
+
+ + user token + +
+ {userToken && ( +
+ +
+ {"Copy this value. You won't see it again!"} +
+
+ )} + {userToken && ( + + )} +
+
+ {userToken} +
+
+ ) : ( + +
+ + setName(e.target.value)} + /> +
+ +
+ + + + +
+ {tokenExpiryOptions.map((option) => ( + + {({ active, checked }) => ( +
+ {checked ? ( + + ) : ( + + )} + {option.name} +
+ )} +
+ ))} +
+
+ + {humanReadableExpiry(expiry)} + +
+ +
+ + +
+ + )} +
+
+
+
+
+
+ + ) +} + +export default function Tokens({ params }: { params: { team: string } }) { + const [getUserTokens, { data: userTokensData }] = useLazyQuery(GetUserTokens) + + const [deleteUserToken] = useMutation(RevokeUserToken) + + const { activeOrganisation: organisation } = useContext(organisationContext) + + const organisationId = organisation?.id + + const handleDeleteUserToken = async (tokenId: string) => { + await deleteUserToken({ + variables: { tokenId }, + refetchQueries: [ + { + query: GetUserTokens, + variables: { + organisationId, + }, + }, + ], + }) + } + + useEffect(() => { + if (organisationId) { + getUserTokens({ + variables: { + organisationId, + }, + }) + } + }, [getUserTokens, organisationId]) + + const userTokens = + userTokensData?.userTokens.sort((a: UserTokenType, b: UserTokenType) => { + return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + }) || [] + + const DeleteConfirmDialog = (props: { + token: UserTokenType | ServiceTokenType + onDelete: Function + }) => { + const { token, onDelete } = props + + const [isOpen, setIsOpen] = useState(false) + + const closeModal = () => { + setIsOpen(false) + } + + const openModal = () => { + setIsOpen(true) + } + + return ( + <> +
+ +
+ + + + +
+ + +
+
+ + + +

+ Delete{' '} + + {token.name} + +

+ + +
+ +
+

+ Are you sure you want to delete this token? +

+
+ + +
+
+
+
+
+
+
+
+ + ) + } + + const CreatedToken = (props: { + token: ServiceTokenType | UserTokenType + deleteHandler: Function + }) => { + const { token, deleteHandler } = props + + const isExpired = token.expiresAt === null ? false : new Date(token.expiresAt) < new Date() + + return ( + + + +
+
{token.name}
+
+
+
Created {relativeTimeFromDates(new Date(token.createdAt))}
+
+
+
+ + + + {isExpired ? 'Expired' : 'Expires'}{' '} + {token.expiresAt ? relativeTimeFromDates(new Date(token.expiresAt)) : 'never'} + + + + + + + ) + } + + return ( +
+ {organisation && } +
+
+

User tokens

+

+ Tokens used to authenticate with the CLI from personal devices. Used for development and + manual configuration. +

+
+
+
+
+ +
+ + + + + + + + + + + {userTokens.map((userToken: UserTokenType) => ( + + ))} + +
+ token + + expiry +
+
+
+
+
+ ) +} diff --git a/frontend/app/invite/[invite]/page.tsx b/frontend/app/invite/[invite]/page.tsx index 15e355e2..8d2b4c61 100644 --- a/frontend/app/invite/[invite]/page.tsx +++ b/frontend/app/invite/[invite]/page.tsx @@ -3,7 +3,7 @@ import { cryptoUtils } from '@/utils/auth' import VerifyInvite from '@/graphql/queries/organisation/validateOrganisationInvite.gql' import AcceptOrganisationInvite from '@/graphql/mutations/organisation/acceptInvite.gql' -import { useMutation, useQuery } from '@apollo/client' +import { useLazyQuery, useMutation, useQuery } from '@apollo/client' import { HeroPattern } from '@/components/common/HeroPattern' import { Button } from '@/components/common/Button' import { FaArrowRight } from 'react-icons/fa' @@ -39,9 +39,7 @@ const InvalidInvite = () => ( ) export default function Invite({ params }: { params: { invite: string } }) { - const { data, loading } = useQuery(VerifyInvite, { - variables: { inviteId: cryptoUtils.decodeInvite(params.invite) }, - }) + const [verifyInvite, { data, loading }] = useLazyQuery(VerifyInvite) const [acceptInvite] = useMutation(AcceptOrganisationInvite) @@ -59,6 +57,19 @@ export default function Invite({ params }: { params: { invite: string } }) { const [mnemonic, setMnemonic] = useState('') const [isloading, setIsLoading] = useState(false) + useEffect(() => { + const handleVerifyInvite = async () => { + const inviteId = await cryptoUtils.decodeInvite(params.invite) + + await verifyInvite({ + variables: { inviteId }, + }) + } + + if (params.invite) handleVerifyInvite() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [params.invite]) + const steps: Step[] = [ { index: 0, diff --git a/frontend/components/apps/NewAppDialog.tsx b/frontend/components/apps/NewAppDialog.tsx index aea84af3..97d86c9b 100644 --- a/frontend/components/apps/NewAppDialog.tsx +++ b/frontend/components/apps/NewAppDialog.tsx @@ -1,9 +1,9 @@ -import { cryptoUtils } from '@/utils/auth' +import { OrganisationKeyring, cryptoUtils } from '@/utils/auth' import { copyToClipBoard } from '@/utils/clipboard' import { getLocalKeyring } from '@/utils/localStorage' import { Dialog, Transition } from '@headlessui/react' import { useSession } from 'next-auth/react' -import { Fragment, ReactNode, useEffect, useState } from 'react' +import { Fragment, ReactNode, useContext, useEffect, useState } from 'react' import { FaCopy, FaCross, FaExclamationTriangle, FaEye, FaEyeSlash, FaTimes } from 'react-icons/fa' import { toast } from 'react-toastify' import { Button } from '../common/Button' @@ -17,6 +17,7 @@ import { } from '@/apollo/graphql' import { splitSecret } from '@/utils/keyshares' import { UpgradeRequestForm } from '../forms/UpgradeRequestForm' +import { KeyringContext } from '@/contexts/keyringContext' const FREE_APP_LIMIT = 5 const PRO_APP_LIMIT = 10 @@ -44,6 +45,8 @@ export default function NewAppDialog(props: { variant: 'primary', } + const { keyring, setKeyring } = useContext(KeyringContext) + const complete = () => appId && appSecret const reset = () => { @@ -67,6 +70,21 @@ export default function NewAppDialog(props: { toast.info('Copied') } + const validateKeyring = async (password: string) => { + return new Promise(async (resolve) => { + if (keyring) resolve(keyring) + else { + const decryptedKeyring = await cryptoUtils.getKeyring( + session?.user?.email!, + organisation!.id, + password + ) + setKeyring(decryptedKeyring) + resolve(decryptedKeyring) + } + }) + } + const handleCreateApp = async () => { const APP_VERSION = 1 @@ -78,18 +96,8 @@ export default function NewAppDialog(props: { const id = crypto.randomUUID() try { - const deviceKey = await cryptoUtils.deviceVaultKey(pw, session?.user?.email!) - const encryptedKeyring = getLocalKeyring(session?.user?.email!, organisation.id) - if (!encryptedKeyring) throw 'Error fetching local encrypted keys from browser' - const decryptedKeyring = await cryptoUtils.decryptAccountKeyring( - encryptedKeyring!, - deviceKey - ) - if (!decryptedKeyring) throw 'Failed to decrypt keys' - const encryptedAppSeed = await cryptoUtils.encryptedAppSeed( - appSeed, - decryptedKeyring.symmetricKey - ) + const keyring = await validateKeyring(pw) + const encryptedAppSeed = await cryptoUtils.encryptedAppSeed(appSeed, keyring.symmetricKey) const appKeys = await cryptoUtils.appKeyring(appSeed) const appKeyShares = await splitSecret(appKeys.privateKey) @@ -121,6 +129,7 @@ export default function NewAppDialog(props: { setAppId(`phApp:v${APP_VERSION}:${appKeys.publicKey}`) resolve(true) + closeModal() } catch (error) { reject(error) } @@ -235,33 +244,35 @@ export default function NewAppDialog(props: { />
-
- -
- setPw(e.target.value)} - type={showPw ? 'text' : 'password'} - minLength={16} - required - className="w-full " - /> - + Sudo password + +
+ setPw(e.target.value)} + type={showPw ? 'text' : 'password'} + minLength={16} + required + className="w-full " + /> + +
-
+ )}
diff --git a/frontend/components/apps/tokens/SecretTokens.tsx b/frontend/components/apps/tokens/SecretTokens.tsx index 719e2239..a3890dad 100644 --- a/frontend/components/apps/tokens/SecretTokens.tsx +++ b/frontend/components/apps/tokens/SecretTokens.tsx @@ -538,7 +538,7 @@ const CreateServiceTokenDialog = (props: { organisationId: string; appId: string -
+
{envScope .map((env: Partial) => env.name) @@ -561,7 +561,7 @@ const CreateServiceTokenDialog = (props: { organisationId: string; appId: string leaveTo="transform scale-95 opacity-0" > -
+
{envOptions.map((env: Partial) => ( {({ active, selected }) => ( @@ -574,7 +574,7 @@ const CreateServiceTokenDialog = (props: { organisationId: string; appId: string {selected ? ( ) : ( - + )} {env.name} diff --git a/frontend/components/auth/UnlockKeyringDialog.tsx b/frontend/components/auth/UnlockKeyringDialog.tsx index 5b064d92..59f8bb43 100644 --- a/frontend/components/auth/UnlockKeyringDialog.tsx +++ b/frontend/components/auth/UnlockKeyringDialog.tsx @@ -94,14 +94,14 @@ export default function UnlockKeyringDialog(props: { organisationId: string }) {

-
+
-
+