[next] Admin Authentication (#2155)

* feat: Support sign in via E-Mail, and social logins + permission check

* feat: no permission info

* test: unit test

* test: fix remaining tests

* test: add integration tests

* feat: add admin account completion

* test: auth completion feature tests

* fix: translation

* fix: comment marker design

* fix: linting issues

* chore: address pr review comments

* chore: add comment
This commit is contained in:
Kiwi
2019-02-04 21:26:41 +00:00
committed by Wyatt Johnson
parent 939152ee81
commit 7e8ef2189d
143 changed files with 5991 additions and 2048 deletions
@@ -0,0 +1,18 @@
import { commitLocalUpdate, Environment } from "relay-runtime";
import { createMutationContainer } from "talk-framework/lib/relay";
import { LOCAL_ID } from "talk-framework/lib/relay/withLocalStateContainer";
export type ClearAuthErrorMutation = () => Promise<void>;
export async function commit(environment: Environment, input: undefined) {
return commitLocalUpdate(environment, store => {
const record = store.get(LOCAL_ID)!;
record.setValue(null, "authError");
});
}
export const withClearAuthErrorMutation = createMutationContainer(
"clearAuthError",
commit
);
@@ -0,0 +1,25 @@
import { Environment } from "relay-runtime";
import { TalkContext } from "talk-framework/lib/bootstrap";
import { createMutationContainer } from "talk-framework/lib/relay";
import { commit as setAuthToken } from "talk-framework/mutations/SetAuthTokenMutation";
interface CompleteAccountInput {
authToken: string;
}
export type CompleteAccountMutation = (
input: CompleteAccountInput
) => Promise<void>;
export async function commit(
environment: Environment,
input: CompleteAccountInput,
context: TalkContext
) {
await setAuthToken(environment, { authToken: input.authToken }, context);
}
export const withCompleteAccountMutation = createMutationContainer(
"completeAccount",
commit
);
@@ -0,0 +1,21 @@
import { Environment, RecordSource } from "relay-runtime";
import { LOCAL_ID } from "talk-framework/lib/relay";
import { createRelayEnvironment } from "talk-framework/testHelpers";
import { commit } from "./SetAuthViewMutation";
let environment: Environment;
const source: RecordSource = new RecordSource();
beforeAll(() => {
environment = createRelayEnvironment({
source,
});
});
it("Sets view", () => {
const view = "SIGN_IN";
commit(environment, { view }, {} as any);
expect(source.get(LOCAL_ID)!.authView).toEqual(view);
});
@@ -0,0 +1,28 @@
import { commitLocalUpdate, Environment } from "relay-runtime";
import { TalkContext } from "talk-framework/lib/bootstrap";
import { createMutationContainer } from "talk-framework/lib/relay";
import { LOCAL_ID } from "talk-framework/lib/relay/withLocalStateContainer";
export interface SetAuthViewInput {
// TODO: replace with generated typescript types.
view: "SIGN_IN" | "ADD_EMAIL_ADDRESS" | "CREATE_USERNAME" | "CREATE_PASSWORD";
}
export type SetAuthViewMutation = (input: SetAuthViewInput) => Promise<void>;
export async function commit(
environment: Environment,
input: SetAuthViewInput,
{ pym }: TalkContext
) {
return commitLocalUpdate(environment, store => {
const record = store.get(LOCAL_ID)!;
record.setValue(input.view, "authView");
});
}
export const withSetAuthViewMutation = createMutationContainer(
"setAuthView",
commit
);
@@ -0,0 +1,46 @@
import { graphql } from "react-relay";
import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
} from "talk-framework/lib/relay";
import { Omit } from "talk-framework/types";
import { SetEmailMutation as MutationTypes } from "talk-admin/__generated__/SetEmailMutation.graphql";
export type SetEmailInput = Omit<
MutationTypes["variables"]["input"],
"clientMutationId"
>;
const mutation = graphql`
mutation SetEmailMutation($input: SetEmailInput!) {
setEmail(input: $input) {
user {
email
}
clientMutationId
}
}
`;
let clientMutationId = 0;
function commit(environment: Environment, input: SetEmailInput) {
return commitMutationPromiseNormalized<MutationTypes>(environment, {
mutation,
variables: {
input: {
...input,
clientMutationId: (clientMutationId++).toString(),
},
},
});
}
export const withSetEmailMutation = createMutationContainer("setEmail", commit);
export type SetEmailMutation = (
input: SetEmailInput
) => Promise<MutationTypes["response"]["setEmail"]>;
@@ -0,0 +1,51 @@
import { graphql } from "react-relay";
import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
} from "talk-framework/lib/relay";
import { Omit } from "talk-framework/types";
import { SetPasswordMutation as MutationTypes } from "talk-admin/__generated__/SetPasswordMutation.graphql";
export type SetPasswordInput = Omit<
MutationTypes["variables"]["input"],
"clientMutationId"
>;
const mutation = graphql`
mutation SetPasswordMutation($input: SetPasswordInput!) {
setPassword(input: $input) {
user {
profiles {
__typename
}
}
clientMutationId
}
}
`;
let clientMutationId = 0;
function commit(environment: Environment, input: SetPasswordInput) {
return commitMutationPromiseNormalized<MutationTypes>(environment, {
mutation,
variables: {
input: {
...input,
clientMutationId: (clientMutationId++).toString(),
},
},
});
}
export const withSetPasswordMutation = createMutationContainer(
"setPassword",
commit
);
export type SetPasswordMutation = (
input: SetPasswordInput
) => Promise<MutationTypes["response"]["setPassword"]>;
@@ -1,8 +1,13 @@
import { Environment, RecordSource } from "relay-runtime";
import { REDIRECT_PATH_KEY } from "talk-admin/constants";
import { LOCAL_ID } from "talk-framework/lib/relay";
import { createRelayEnvironment } from "talk-framework/testHelpers";
import {
createInMemoryStorage,
createPromisifiedStorage,
} from "talk-framework/lib/storage";
import { commit } from "./SetRedirectPathMutation";
let environment: Environment;
@@ -14,7 +19,20 @@ beforeAll(() => {
});
});
it("Sets redirectPath", () => {
commit(environment, { path: "/path" });
it("Sets redirectPath", async () => {
const storage = createInMemoryStorage();
await commit(environment, { path: "/path" }, {
localStorage: createPromisifiedStorage(storage),
} as any);
expect(source.get(LOCAL_ID)!.redirectPath).toEqual("/path");
expect(storage.getItem(REDIRECT_PATH_KEY)).toEqual("/path");
});
it("Removes redirectPath", async () => {
const storage = createInMemoryStorage();
await commit(environment, { path: null }, {
localStorage: createPromisifiedStorage(storage),
} as any);
expect(source.get(LOCAL_ID)!.redirectPath).toEqual(null);
expect(storage.getItem(REDIRECT_PATH_KEY)).toEqual(null);
});
@@ -1,5 +1,7 @@
import { commitLocalUpdate, Environment } from "relay-runtime";
import { REDIRECT_PATH_KEY } from "talk-admin/constants";
import { TalkContext } from "talk-framework/lib/bootstrap";
import { createMutationContainer, LOCAL_ID } from "talk-framework/lib/relay";
export interface SetRedirectPathInput {
@@ -12,8 +14,15 @@ export type SetRedirectPathMutation = (
export async function commit(
environment: Environment,
input: SetRedirectPathInput
input: SetRedirectPathInput,
{ localStorage }: TalkContext
) {
if (!input.path) {
await localStorage.removeItem(REDIRECT_PATH_KEY);
} else {
await localStorage.setItem(REDIRECT_PATH_KEY, input.path);
}
return commitLocalUpdate(environment, store => {
const record = store.get(LOCAL_ID)!;
record.setValue(input.path, "redirectPath");
@@ -0,0 +1,49 @@
import { graphql } from "react-relay";
import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
} from "talk-framework/lib/relay";
import { Omit } from "talk-framework/types";
import { SetUsernameMutation as MutationTypes } from "talk-admin/__generated__/SetUsernameMutation.graphql";
export type SetUsernameInput = Omit<
MutationTypes["variables"]["input"],
"clientMutationId"
>;
const mutation = graphql`
mutation SetUsernameMutation($input: SetUsernameInput!) {
setUsername(input: $input) {
user {
username
}
clientMutationId
}
}
`;
let clientMutationId = 0;
function commit(environment: Environment, input: SetUsernameInput) {
return commitMutationPromiseNormalized<MutationTypes>(environment, {
mutation,
variables: {
input: {
...input,
clientMutationId: (clientMutationId++).toString(),
},
},
});
}
export const withSetUsernameMutation = createMutationContainer(
"setUsername",
commit
);
export type SetUsernameMutation = (
input: SetUsernameInput
) => Promise<MutationTypes["response"]["setUsername"]>;
@@ -2,7 +2,6 @@ import { Environment } from "relay-runtime";
import { TalkContext } from "talk-framework/lib/bootstrap";
import { createMutationContainer } from "talk-framework/lib/relay";
import { commit as setAuthToken } from "talk-framework/mutations/SetAuthTokenMutation";
import { signIn, SignInInput } from "talk-framework/rest";
export type SignInMutation = (input: SignInInput) => Promise<void>;
@@ -13,7 +12,13 @@ export async function commit(
context: TalkContext
) {
const result = await signIn(context.rest, input);
setAuthToken(environment, { authToken: result.token }, context);
// Put the token on the hash and clean the session.
// It'll be picked up by initLocalState.
location.hash = `accessToken=${result.token}`;
await context.clearSession();
// TODO: (cvle) A better way would be if `context.clearSession` would return the new session and
// we set the accessToken directly in there.
}
export const withSignInMutation = createMutationContainer("signIn", commit);
+21
View File
@@ -20,3 +20,24 @@ export {
withRegenerateSSOKeyMutation,
RegenerateSSOKeyMutation,
} from "./RegenerateSSOKeyMutation";
export {
withSetAuthViewMutation,
SetAuthViewMutation,
} from "./SetAuthViewMutation";
export {
withClearAuthErrorMutation,
ClearAuthErrorMutation,
} from "./ClearAuthErrorMutation";
export {
withCompleteAccountMutation,
CompleteAccountMutation,
} from "./CompleteAccountMutation";
export { withSetEmailMutation, SetEmailMutation } from "./SetEmailMutation";
export {
withSetUsernameMutation,
SetUsernameMutation,
} from "./SetUsernameMutation";
export {
withSetPasswordMutation,
SetPasswordMutation,
} from "./SetPasswordMutation";