Add possibility to invite members to workspace (#579)
* Add possibility to invite members to workspace * Update endpoints * Wrap up front end * Fix according to review * Fix lint
This commit is contained in:
@ -39,6 +39,7 @@ function AuthRoutes() {
|
||||
<Route path={AuthPath.Index} element={<Index />} />
|
||||
<Route path={AuthPath.Callback} element={<Verify />} />
|
||||
<Route path={AuthPath.PasswordLogin} element={<PasswordLogin />} />
|
||||
<Route path={AuthPath.InviteLink} element={<PasswordLogin />} />
|
||||
<Route
|
||||
path={AuthPath.CreateWorkspace}
|
||||
element={<CreateWorkspace />}
|
||||
|
||||
@ -1163,6 +1163,7 @@ export type Mutation = {
|
||||
deleteManyPipelineProgress: AffectedRows;
|
||||
deleteWorkspaceMember: WorkspaceMember;
|
||||
renewToken: AuthTokens;
|
||||
signUp: LoginToken;
|
||||
updateOneCommentThread: CommentThread;
|
||||
updateOneCompany?: Maybe<Company>;
|
||||
updateOnePerson?: Maybe<Person>;
|
||||
@ -1244,6 +1245,13 @@ export type MutationRenewTokenArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationSignUpArgs = {
|
||||
email: Scalars['String'];
|
||||
password: Scalars['String'];
|
||||
workspaceInviteHash?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateOneCommentThreadArgs = {
|
||||
data: CommentThreadUpdateInput;
|
||||
where: CommentThreadWhereUniqueInput;
|
||||
@ -2439,6 +2447,7 @@ export type PipelineWhereUniqueInput = {
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
checkUserExists: UserExists;
|
||||
checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid;
|
||||
clientConfig: ClientConfig;
|
||||
currentUser: User;
|
||||
currentWorkspace: Workspace;
|
||||
@ -2460,6 +2469,11 @@ export type QueryCheckUserExistsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryCheckWorkspaceInviteHashIsValidArgs = {
|
||||
inviteHash: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryFindManyCommentThreadsArgs = {
|
||||
cursor?: InputMaybe<CommentThreadWhereUniqueInput>;
|
||||
distinct?: InputMaybe<Array<CommentThreadScalarFieldEnum>>;
|
||||
@ -2917,6 +2931,7 @@ export type Workspace = {
|
||||
displayName?: Maybe<Scalars['String']>;
|
||||
domainName?: Maybe<Scalars['String']>;
|
||||
id: Scalars['ID'];
|
||||
inviteHash?: Maybe<Scalars['String']>;
|
||||
logo?: Maybe<Scalars['String']>;
|
||||
people?: Maybe<Array<Person>>;
|
||||
pipelineProgresses?: Maybe<Array<PipelineProgress>>;
|
||||
@ -2926,6 +2941,11 @@ export type Workspace = {
|
||||
workspaceMember?: Maybe<Array<WorkspaceMember>>;
|
||||
};
|
||||
|
||||
export type WorkspaceInviteHashValid = {
|
||||
__typename?: 'WorkspaceInviteHashValid';
|
||||
isValid: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type WorkspaceMember = {
|
||||
__typename?: 'WorkspaceMember';
|
||||
createdAt: Scalars['DateTime'];
|
||||
@ -3054,6 +3074,7 @@ export type WorkspaceUpdateInput = {
|
||||
displayName?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
|
||||
domainName?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
|
||||
id?: InputMaybe<StringFieldUpdateOperationsInput>;
|
||||
inviteHash?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
|
||||
logo?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
|
||||
people?: InputMaybe<PersonUpdateManyWithoutWorkspaceNestedInput>;
|
||||
pipelineProgresses?: InputMaybe<PipelineProgressUpdateManyWithoutWorkspaceNestedInput>;
|
||||
@ -3086,6 +3107,23 @@ export type ChallengeMutationVariables = Exact<{
|
||||
|
||||
export type ChallengeMutation = { __typename?: 'Mutation', challenge: { __typename?: 'LoginToken', loginToken: { __typename?: 'AuthToken', expiresAt: string, token: string } } };
|
||||
|
||||
export type SignUpMutationVariables = Exact<{
|
||||
email: Scalars['String'];
|
||||
password: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type SignUpMutation = { __typename?: 'Mutation', signUp: { __typename?: 'LoginToken', loginToken: { __typename?: 'AuthToken', expiresAt: string, token: string } } };
|
||||
|
||||
export type SignUpToWorkspaceMutationVariables = Exact<{
|
||||
email: Scalars['String'];
|
||||
password: Scalars['String'];
|
||||
workspaceInviteHash: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type SignUpToWorkspaceMutation = { __typename?: 'Mutation', signUp: { __typename?: 'LoginToken', loginToken: { __typename?: 'AuthToken', expiresAt: string, token: string } } };
|
||||
|
||||
export type VerifyMutationVariables = Exact<{
|
||||
loginToken: Scalars['String'];
|
||||
}>;
|
||||
@ -3526,6 +3564,85 @@ export function useChallengeMutation(baseOptions?: Apollo.MutationHookOptions<Ch
|
||||
export type ChallengeMutationHookResult = ReturnType<typeof useChallengeMutation>;
|
||||
export type ChallengeMutationResult = Apollo.MutationResult<ChallengeMutation>;
|
||||
export type ChallengeMutationOptions = Apollo.BaseMutationOptions<ChallengeMutation, ChallengeMutationVariables>;
|
||||
export const SignUpDocument = gql`
|
||||
mutation SignUp($email: String!, $password: String!) {
|
||||
signUp(email: $email, password: $password) {
|
||||
loginToken {
|
||||
expiresAt
|
||||
token
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type SignUpMutationFn = Apollo.MutationFunction<SignUpMutation, SignUpMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useSignUpMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useSignUpMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useSignUpMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [signUpMutation, { data, loading, error }] = useSignUpMutation({
|
||||
* variables: {
|
||||
* email: // value for 'email'
|
||||
* password: // value for 'password'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useSignUpMutation(baseOptions?: Apollo.MutationHookOptions<SignUpMutation, SignUpMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<SignUpMutation, SignUpMutationVariables>(SignUpDocument, options);
|
||||
}
|
||||
export type SignUpMutationHookResult = ReturnType<typeof useSignUpMutation>;
|
||||
export type SignUpMutationResult = Apollo.MutationResult<SignUpMutation>;
|
||||
export type SignUpMutationOptions = Apollo.BaseMutationOptions<SignUpMutation, SignUpMutationVariables>;
|
||||
export const SignUpToWorkspaceDocument = gql`
|
||||
mutation SignUpToWorkspace($email: String!, $password: String!, $workspaceInviteHash: String!) {
|
||||
signUp(
|
||||
email: $email
|
||||
password: $password
|
||||
workspaceInviteHash: $workspaceInviteHash
|
||||
) {
|
||||
loginToken {
|
||||
expiresAt
|
||||
token
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type SignUpToWorkspaceMutationFn = Apollo.MutationFunction<SignUpToWorkspaceMutation, SignUpToWorkspaceMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useSignUpToWorkspaceMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useSignUpToWorkspaceMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useSignUpToWorkspaceMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [signUpToWorkspaceMutation, { data, loading, error }] = useSignUpToWorkspaceMutation({
|
||||
* variables: {
|
||||
* email: // value for 'email'
|
||||
* password: // value for 'password'
|
||||
* workspaceInviteHash: // value for 'workspaceInviteHash'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useSignUpToWorkspaceMutation(baseOptions?: Apollo.MutationHookOptions<SignUpToWorkspaceMutation, SignUpToWorkspaceMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<SignUpToWorkspaceMutation, SignUpToWorkspaceMutationVariables>(SignUpToWorkspaceDocument, options);
|
||||
}
|
||||
export type SignUpToWorkspaceMutationHookResult = ReturnType<typeof useSignUpToWorkspaceMutation>;
|
||||
export type SignUpToWorkspaceMutationResult = Apollo.MutationResult<SignUpToWorkspaceMutation>;
|
||||
export type SignUpToWorkspaceMutationOptions = Apollo.BaseMutationOptions<SignUpToWorkspaceMutation, SignUpToWorkspaceMutationVariables>;
|
||||
export const VerifyDocument = gql`
|
||||
mutation Verify($loginToken: String!) {
|
||||
verify(loginToken: $loginToken) {
|
||||
|
||||
@ -27,15 +27,15 @@ root.render(
|
||||
<ApolloProvider>
|
||||
<AppThemeProvider>
|
||||
<StrictMode>
|
||||
<UserProvider>
|
||||
<ClientConfigProvider>
|
||||
<HotkeysProvider initiallyActiveScopes={INITIAL_HOTKEYS_SCOPES}>
|
||||
<BrowserRouter>
|
||||
<HotkeysProvider initiallyActiveScopes={INITIAL_HOTKEYS_SCOPES}>
|
||||
<BrowserRouter>
|
||||
<UserProvider>
|
||||
<ClientConfigProvider>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</HotkeysProvider>
|
||||
</ClientConfigProvider>
|
||||
</UserProvider>
|
||||
</ClientConfigProvider>
|
||||
</UserProvider>
|
||||
</BrowserRouter>
|
||||
</HotkeysProvider>
|
||||
</StrictMode>
|
||||
</AppThemeProvider>
|
||||
</ApolloProvider>
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { useChallengeMutation, useVerifyMutation } from '~/generated/graphql';
|
||||
import {
|
||||
useChallengeMutation,
|
||||
useSignUpMutation,
|
||||
useSignUpToWorkspaceMutation,
|
||||
useVerifyMutation,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
import { currentUserState } from '../states/currentUserState';
|
||||
import { isAuthenticatingState } from '../states/isAuthenticatingState';
|
||||
@ -13,6 +18,8 @@ export function useAuth() {
|
||||
const [, setIsAuthenticating] = useRecoilState(isAuthenticatingState);
|
||||
|
||||
const [challenge] = useChallengeMutation();
|
||||
const [signUp] = useSignUpMutation();
|
||||
const [SignUpToWorkspace] = useSignUpToWorkspaceMutation();
|
||||
const [verify] = useVerifyMutation();
|
||||
|
||||
const handleChallenge = useCallback(
|
||||
@ -74,10 +81,57 @@ export function useAuth() {
|
||||
setTokenPair(null);
|
||||
}, [setTokenPair]);
|
||||
|
||||
const handleSignUp = useCallback(
|
||||
async (email: string, password: string) => {
|
||||
const signUpResult = await signUp({
|
||||
variables: {
|
||||
email,
|
||||
password,
|
||||
},
|
||||
});
|
||||
|
||||
if (signUpResult.errors) {
|
||||
throw signUpResult.errors;
|
||||
}
|
||||
|
||||
if (!signUpResult.data?.signUp) {
|
||||
throw new Error('No login token');
|
||||
}
|
||||
|
||||
await handleVerify(signUpResult.data?.signUp.loginToken.token);
|
||||
},
|
||||
[signUp, handleVerify],
|
||||
);
|
||||
|
||||
const handleSignUpToWorkspace = useCallback(
|
||||
async (email: string, password: string, workspaceInviteHash: string) => {
|
||||
const signUpResult = await SignUpToWorkspace({
|
||||
variables: {
|
||||
email,
|
||||
password,
|
||||
workspaceInviteHash,
|
||||
},
|
||||
});
|
||||
|
||||
if (signUpResult.errors) {
|
||||
throw signUpResult.errors;
|
||||
}
|
||||
|
||||
if (!signUpResult.data?.signUp) {
|
||||
throw new Error('No login token');
|
||||
}
|
||||
|
||||
await handleVerify(signUpResult.data?.signUp.loginToken.token);
|
||||
},
|
||||
[SignUpToWorkspace, handleVerify],
|
||||
);
|
||||
|
||||
return {
|
||||
challenge: handleChallenge,
|
||||
verify: handleVerify,
|
||||
login: handleLogin,
|
||||
signUp: handleSignUp,
|
||||
signUpToWorkspace: handleSignUpToWorkspace,
|
||||
logout: handleLogout,
|
||||
};
|
||||
}
|
||||
|
||||
@ -11,6 +11,36 @@ export const CHALLENGE = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
export const SIGN_UP = gql`
|
||||
mutation SignUp($email: String!, $password: String!) {
|
||||
signUp(email: $email, password: $password) {
|
||||
loginToken {
|
||||
expiresAt
|
||||
token
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const SIGN_UP_TO_WORKSPACE = gql`
|
||||
mutation SignUpToWorkspace(
|
||||
$email: String!
|
||||
$password: String!
|
||||
$workspaceInviteHash: String!
|
||||
) {
|
||||
signUp(
|
||||
email: $email
|
||||
password: $password
|
||||
workspaceInviteHash: $workspaceInviteHash
|
||||
) {
|
||||
loginToken {
|
||||
expiresAt
|
||||
token
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const VERIFY = gql`
|
||||
mutation Verify($loginToken: String!) {
|
||||
verify(loginToken: $loginToken) {
|
||||
|
||||
@ -130,10 +130,10 @@ export function CommentThread({
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (commentThread) {
|
||||
if (commentThread && !title) {
|
||||
setTitle(commentThread?.title ?? '');
|
||||
}
|
||||
}, [commentThread]);
|
||||
}, [commentThread, title]);
|
||||
|
||||
if (!commentThread) {
|
||||
return <></>;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useRecoilState } from 'recoil';
|
||||
@ -62,35 +62,61 @@ export function PasswordLogin() {
|
||||
);
|
||||
const [formError, setFormError] = useState('');
|
||||
|
||||
const { login } = useAuth();
|
||||
|
||||
const handleLogin = useCallback(async () => {
|
||||
try {
|
||||
setMockMode(false);
|
||||
|
||||
await login(authFlowUserEmail, internalPassword);
|
||||
|
||||
navigate('/auth/create/workspace');
|
||||
} catch (err: any) {
|
||||
setFormError(err.message);
|
||||
}
|
||||
}, [login, authFlowUserEmail, internalPassword, setMockMode, navigate]);
|
||||
|
||||
useScopedHotkeys(
|
||||
'enter',
|
||||
() => {
|
||||
handleLogin();
|
||||
},
|
||||
InternalHotkeysScope.PasswordLogin,
|
||||
[handleLogin],
|
||||
);
|
||||
|
||||
const { login, signUp, signUpToWorkspace } = useAuth();
|
||||
const { loading, data } = useCheckUserExistsQuery({
|
||||
variables: {
|
||||
email: authFlowUserEmail,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setMockMode(true);
|
||||
}, [setMockMode]);
|
||||
|
||||
const workspaceInviteHash = useParams().workspaceInviteHash;
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
try {
|
||||
setMockMode(false);
|
||||
if (data?.checkUserExists.exists) {
|
||||
await login(authFlowUserEmail, internalPassword);
|
||||
} else {
|
||||
if (workspaceInviteHash) {
|
||||
await signUpToWorkspace(
|
||||
authFlowUserEmail,
|
||||
internalPassword,
|
||||
workspaceInviteHash,
|
||||
);
|
||||
} else {
|
||||
await signUp(authFlowUserEmail, internalPassword);
|
||||
}
|
||||
}
|
||||
navigate('/auth/create/workspace');
|
||||
} catch (err: any) {
|
||||
setFormError(err.message);
|
||||
}
|
||||
}, [
|
||||
login,
|
||||
signUp,
|
||||
signUpToWorkspace,
|
||||
authFlowUserEmail,
|
||||
internalPassword,
|
||||
setMockMode,
|
||||
navigate,
|
||||
data?.checkUserExists.exists,
|
||||
|
||||
workspaceInviteHash,
|
||||
]);
|
||||
|
||||
useScopedHotkeys(
|
||||
'enter',
|
||||
() => {
|
||||
handleSubmit();
|
||||
},
|
||||
InternalHotkeysScope.PasswordLogin,
|
||||
[handleSubmit],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Logo />
|
||||
@ -124,7 +150,7 @@ export function PasswordLogin() {
|
||||
<StyledButtonContainer>
|
||||
<MainButton
|
||||
title="Continue"
|
||||
onClick={handleLogin}
|
||||
onClick={handleSubmit}
|
||||
disabled={!authFlowUserEmail || !internalPassword || loading}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
@ -4,4 +4,5 @@ export enum AuthPath {
|
||||
PasswordLogin = 'password-login',
|
||||
CreateWorkspace = 'create/workspace',
|
||||
CreateProfile = 'create/profile',
|
||||
InviteLink = 'invite/:workspaceInviteHash',
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ type MockedUser = Pick<
|
||||
workspaceMember: Pick<WorkspaceMember, 'id' | '__typename'> & {
|
||||
workspace: Pick<
|
||||
Workspace,
|
||||
'id' | 'displayName' | 'domainName' | 'logo' | '__typename'
|
||||
'id' | 'displayName' | 'domainName' | 'logo' | 'inviteHash' | '__typename'
|
||||
>;
|
||||
};
|
||||
};
|
||||
@ -35,6 +35,7 @@ export const mockedUsersData: Array<MockedUser> = [
|
||||
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
|
||||
displayName: 'Twenty',
|
||||
domainName: 'twenty.com',
|
||||
inviteHash: 'twenty.com-invite-hash',
|
||||
logo: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAACb0lEQVR4nO2VO4taQRTHr3AblbjxEVlwCwVhg7BoqqCIjy/gAyyFWNlYBOxsfH0KuxgQGwXRUkGuL2S7i1barGAgiwbdW93SnGOc4BonPiKahf3DwXFmuP/fPM4ZlvmlTxAhCBdzHnEQWYiv7Mr4C3NeuVYhQYDPzOUUQgDLBQGcLHNhvQK8DACPx8PTxiqVyvISG43GbyaT6Qfpn06n0m63e/tPAPF4vJ1MJu8kEsnWTCkWi1yr1RKGw+GDRqPBOTfr44vFQvD7/Q/lcpmaaVQAr9fLp1IpO22c47hGOBz+MB6PH+Vy+VYDAL8qlUoGtVotzOfzq4MAgsHgE/6KojiQyWR/bKVSqbSszHFM8Pl8z1YK48JsNltCOBwOnrYLO+8AAIjb+nHbycoTiUQfDJ7tFq4YAHiVSmXBxcD41u8flQU8z7fhzO0r83atVns3Go3u9Xr9x0O/RQXo9/tsIBBg6vX606a52Wz+bZ7P5/WwG29gxSJzhKgA6XTaDoFNF+krFAocmC//4yWEcSf2wTm7mCO19xFgSsKOLI16vV7b7XY7mRNoLwA0JymJ5uQIzgIAuX5PzDElT2m+E8BqtQ4ymcx7Yq7T6a6ZE4sKgOadTucaCwkxp1UzlEKh0GDxIXOwDWHAdi6Xe3swQDQa/Q7mywoolUpvsaptymazDWKxmBHTlWXZm405BFZoNpuGgwEmk4mE2SGtVivii4f1AO7J3ZopkQCQj7Ar1FeRChCJRJzVapX6DKNIfSc1Ax+wtQWQ55h6bH8FWDfYV4fO3wlwDr0C/BcADYiTPCxHqIEA2QsCZAkAKnRGkMbKN/sTX5YHPQ1e7SkAAAAASUVORK5CYII=',
|
||||
},
|
||||
},
|
||||
@ -54,6 +55,7 @@ export const mockedUsersData: Array<MockedUser> = [
|
||||
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
|
||||
displayName: 'Twenty',
|
||||
domainName: 'twenty.com',
|
||||
inviteHash: 'twenty.com-invite-hash',
|
||||
logo: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAACb0lEQVR4nO2VO4taQRTHr3AblbjxEVlwCwVhg7BoqqCIjy/gAyyFWNlYBOxsfH0KuxgQGwXRUkGuL2S7i1barGAgiwbdW93SnGOc4BonPiKahf3DwXFmuP/fPM4ZlvmlTxAhCBdzHnEQWYiv7Mr4C3NeuVYhQYDPzOUUQgDLBQGcLHNhvQK8DACPx8PTxiqVyvISG43GbyaT6Qfpn06n0m63e/tPAPF4vJ1MJu8kEsnWTCkWi1yr1RKGw+GDRqPBOTfr44vFQvD7/Q/lcpmaaVQAr9fLp1IpO22c47hGOBz+MB6PH+Vy+VYDAL8qlUoGtVotzOfzq4MAgsHgE/6KojiQyWR/bKVSqbSszHFM8Pl8z1YK48JsNltCOBwOnrYLO+8AAIjb+nHbycoTiUQfDJ7tFq4YAHiVSmXBxcD41u8flQU8z7fhzO0r83atVns3Go3u9Xr9x0O/RQXo9/tsIBBg6vX606a52Wz+bZ7P5/WwG29gxSJzhKgA6XTaDoFNF+krFAocmC//4yWEcSf2wTm7mCO19xFgSsKOLI16vV7b7XY7mRNoLwA0JymJ5uQIzgIAuX5PzDElT2m+E8BqtQ4ymcx7Yq7T6a6ZE4sKgOadTucaCwkxp1UzlEKh0GDxIXOwDWHAdi6Xe3swQDQa/Q7mywoolUpvsaptymazDWKxmBHTlWXZm405BFZoNpuGgwEmk4mE2SGtVivii4f1AO7J3ZopkQCQj7Ar1FeRChCJRJzVapX6DKNIfSc1Ax+wtQWQ55h6bH8FWDfYV4fO3wlwDr0C/BcADYiTPCxHqIEA2QsCZAkAKnRGkMbKN/sTX5YHPQ1e7SkAAAAASUVORK5CYII=',
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user