add new door onboarding
This commit is contained in:
parent
e99d820abf
commit
c56b90714f
220
packages/doorman-api/src/functions/api/door/onboard.ts
Normal file
220
packages/doorman-api/src/functions/api/door/onboard.ts
Normal file
@ -0,0 +1,220 @@
|
||||
import { ServerlessEventObject, ServerlessFunctionSignature } from "@twilio-labs/serverless-runtime-types/types";
|
||||
import { UserAgentHeader } from "../../../utils/blockUserAgent";
|
||||
import { getMetricFromRegistry, withMetrics } from "../../../common/DoormanHandler";
|
||||
import { TwilioContext } from "../../../types/TwilioContext";
|
||||
import { OnboardMetrics, registerMetrics } from "../../../metrics/OnboardMetrics";
|
||||
import { createOnboardDoorConfig, DOOR_CONFIG_SK, DoorConfigSchema, getDoorConfigID, getOnboardDoorId, OnboardDoorReq, OnboardDoorReqSchema } from "../../../schema/DoorConfig";
|
||||
import { createDynaBridgeClient } from "../../../utils/ddb";
|
||||
import DiscordOauth2 from "discord-oauth2";
|
||||
import { DoorAliasSchema, getDoorAliasID } from "../../../schema/DoorAlias";
|
||||
import { Counter } from "prom-client";
|
||||
|
||||
export interface OnboardRequest extends ServerlessEventObject<{}, UserAgentHeader> {
|
||||
newConfig?: string;
|
||||
|
||||
// for oauth redirect
|
||||
code?: string;
|
||||
state?: string;
|
||||
}
|
||||
|
||||
export interface DiscordOnboardingState {
|
||||
name: string;
|
||||
id: string;
|
||||
apiRedirect: string;
|
||||
}
|
||||
|
||||
const ONBOARDING_SCOPE = ['identify', 'email', 'guilds.join'];
|
||||
|
||||
export const handler: ServerlessFunctionSignature<TwilioContext, OnboardRequest> = withMetrics('onboard', async (context, event, callback, metricsRegistry) => {
|
||||
const response = new Twilio.Response();
|
||||
response.appendHeader('Content-Type', 'application/json');
|
||||
|
||||
registerMetrics(metricsRegistry);
|
||||
|
||||
const db = createDynaBridgeClient(context);
|
||||
|
||||
// return oauth link
|
||||
const oauth = new DiscordOauth2({
|
||||
clientId: context.DISCORD_CLIENT_ID,
|
||||
clientSecret: context.DISCORD_CLIENT_SECRET,
|
||||
redirectUri: context.DOORMAN_URL,
|
||||
});
|
||||
|
||||
// create door config route
|
||||
if (event.code && event.state) {
|
||||
const code = event.code;
|
||||
let discordState: DiscordOnboardingState;
|
||||
|
||||
try {
|
||||
discordState = JSON.parse(event.state);
|
||||
} catch (e) {
|
||||
response.setStatusCode(400);
|
||||
response.setBody({ err: "invalid state" });
|
||||
return callback(null, response);
|
||||
}
|
||||
|
||||
const config = await db.entities.onboardDoorConfig.findById(getOnboardDoorId(discordState.name));
|
||||
if (!config || config.nonce !== discordState.id) {
|
||||
response.setStatusCode(404)
|
||||
.setBody({ err: "approval not found" });
|
||||
|
||||
return callback(null, response);
|
||||
}
|
||||
|
||||
console.log("getting discord token");
|
||||
|
||||
let token: DiscordOauth2.TokenRequestResult;
|
||||
|
||||
try {
|
||||
token = await oauth.tokenRequest({
|
||||
code,
|
||||
scope: ONBOARDING_SCOPE,
|
||||
grantType: "authorization_code",
|
||||
});
|
||||
} catch (err) {
|
||||
console.log("something went wrong with discord authorization");
|
||||
response.setStatusCode(401);
|
||||
response.setBody({ err });
|
||||
return callback(null, response);
|
||||
}
|
||||
|
||||
const user = await oauth.getUser(token.access_token);
|
||||
const profileId = user.id;
|
||||
|
||||
console.log(profileId);
|
||||
|
||||
if (!profileId) {
|
||||
response.setStatusCode(404);
|
||||
response.setBody({ err: "profile not found" });
|
||||
|
||||
return callback(null, response);
|
||||
}
|
||||
|
||||
console.log('trying to join Doorman server');
|
||||
|
||||
// join doorman discord server
|
||||
await fetch(
|
||||
`https://discord.com/api/v8/guilds/${context.DISCORD_GUILD_ID}/members/${profileId}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': 'Bot ' + context.DISCORD_BOT_TOKEN,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
'access_token': token.access_token,
|
||||
})
|
||||
}).catch(err => console.error(err));
|
||||
|
||||
type ActionType = Parameters<typeof db.transaction>[0][0];
|
||||
|
||||
// door alias
|
||||
const doorAlias = DoorAliasSchema.parse({
|
||||
name: config.name,
|
||||
PK: config.buzzer,
|
||||
});
|
||||
|
||||
const createDoorAlias: ActionType = {
|
||||
action: "Put",
|
||||
type: "doorAlias",
|
||||
entity: doorAlias,
|
||||
};
|
||||
|
||||
// door config
|
||||
const doorConfig = DoorConfigSchema.parse({
|
||||
...config,
|
||||
discordUsers: [profileId],
|
||||
PK: `door-${config.name}`,
|
||||
SK: DOOR_CONFIG_SK,
|
||||
});
|
||||
|
||||
const createDoorConfig: ActionType = {
|
||||
action: "Put",
|
||||
type: "doorConfig",
|
||||
entity: doorConfig,
|
||||
}
|
||||
|
||||
// delete onboarding config
|
||||
const deleteOnboardingConfig: ActionType = {
|
||||
action: "Delete",
|
||||
type: "onboardDoorConfig",
|
||||
entity: config,
|
||||
};
|
||||
|
||||
// and all must succeed or none succeed
|
||||
try {
|
||||
await db.transaction([createDoorAlias, createDoorConfig, deleteOnboardingConfig]);
|
||||
response.setStatusCode(200);
|
||||
response.setBody({ redirect: context.DOORMAN_URL + `?door=${config.name}` });
|
||||
} catch (e) {
|
||||
getMetricFromRegistry<Counter>(metricsRegistry, OnboardMetrics.TRANSACTION_CONFLICT).inc(1);
|
||||
console.error(e);
|
||||
|
||||
response.setStatusCode(409);
|
||||
response.setBody({ err: "something went wrong during onboarding" });
|
||||
}
|
||||
|
||||
return callback(null, response);
|
||||
}
|
||||
|
||||
let newConfig = event.newConfig;
|
||||
|
||||
if (!newConfig) {
|
||||
response.setStatusCode(400);
|
||||
return callback(null, response);
|
||||
}
|
||||
|
||||
let newConfigParsed: OnboardDoorReq;
|
||||
|
||||
console.log("parsing config");
|
||||
try {
|
||||
newConfigParsed = OnboardDoorReqSchema.parse(JSON.parse(newConfig));
|
||||
} catch (err) {
|
||||
response.setStatusCode(400);
|
||||
response.setBody({ err });
|
||||
return callback(null, response);
|
||||
}
|
||||
|
||||
const newConfigObj = createOnboardDoorConfig(newConfigParsed);
|
||||
|
||||
// check if this name or buzzer is already registered?
|
||||
const existingAlias = await db.entities.doorAlias.findById(getDoorAliasID(newConfigObj.buzzer));
|
||||
|
||||
if (existingAlias) {
|
||||
getMetricFromRegistry<Counter>(metricsRegistry, OnboardMetrics.BUZZER_CONFLICT).inc({ buzzer: newConfigObj.buzzer }, 1);
|
||||
response.setStatusCode(409);
|
||||
response.setBody({ err: `A buzzer is already registered with the number ${newConfigObj.buzzer}` });
|
||||
return callback(null, response);
|
||||
}
|
||||
|
||||
const existingConfig = await db.entities.doorConfig.findById(getDoorConfigID(newConfigObj.name));
|
||||
|
||||
if (existingConfig) {
|
||||
getMetricFromRegistry<Counter>(metricsRegistry, OnboardMetrics.NAME_CONFLICT).inc({ name: newConfigObj.name }, 1);
|
||||
response.setStatusCode(409);
|
||||
response.setBody({ err: `A buzzer is already registered with the name ${newConfigObj.name}` });
|
||||
return callback(null, response);
|
||||
}
|
||||
|
||||
// TODO: check here for any race condition on onboarding. Techncially 2 ppl could conflict on name or number
|
||||
// and would fail after the discord auth step resulting in TRANSACTION_CONFLICT
|
||||
|
||||
// save to DB, this is a middle step until discord auth comes in
|
||||
await db.entities.onboardDoorConfig.save(newConfigObj);
|
||||
|
||||
const discordState: DiscordOnboardingState = {
|
||||
name: newConfigObj.name,
|
||||
id: newConfigObj.nonce,
|
||||
apiRedirect: '/api/door/onboard',
|
||||
}
|
||||
|
||||
const redirect = oauth.generateAuthUrl({
|
||||
scope: ONBOARDING_SCOPE,
|
||||
responseType: 'code',
|
||||
state: JSON.stringify(discordState),
|
||||
});
|
||||
|
||||
response.setStatusCode(200);
|
||||
response.setBody({ redirect });
|
||||
return callback(null, response);
|
||||
});
|
||||
27
packages/doorman-api/src/metrics/OnboardMetrics.ts
Normal file
27
packages/doorman-api/src/metrics/OnboardMetrics.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { Counter, Registry, Summary } from "prom-client";
|
||||
|
||||
export enum OnboardMetrics {
|
||||
BUZZER_CONFLICT = "BuzzerConflict",
|
||||
NAME_CONFLICT = "NameConflict",
|
||||
TRANSACTION_CONFLICT = "TransactionConflict",
|
||||
}
|
||||
|
||||
export const registerMetrics = (metricsRegistry: Registry) => {
|
||||
metricsRegistry.registerMetric(new Counter({
|
||||
name: OnboardMetrics.BUZZER_CONFLICT,
|
||||
help: "Onboarding was attepted with a buzzer number that is already registered",
|
||||
labelNames: ['buzzer'],
|
||||
}));
|
||||
|
||||
metricsRegistry.registerMetric(new Counter({
|
||||
name: OnboardMetrics.NAME_CONFLICT,
|
||||
help: "Onboarding was attepted with a name that is already registered",
|
||||
labelNames: ['name'],
|
||||
}));
|
||||
|
||||
metricsRegistry.registerMetric(new Counter({
|
||||
name: OnboardMetrics.TRANSACTION_CONFLICT,
|
||||
help: "There was a conflict in the transactions after discord authorization. This likely means two onboardings conflicted",
|
||||
}));
|
||||
}
|
||||
|
||||
@ -68,3 +68,39 @@ export const createEditDoorConfig = (door: string, newConfig: EditDoorConfigReq)
|
||||
approvalId: randomUUID().toString(),
|
||||
};
|
||||
}
|
||||
|
||||
// onboard a new door
|
||||
export const ONBOARD_DOOR_CONFIG_SK = "onboard-config";
|
||||
|
||||
export const OnboardDoorConfigSchema = DoorConfigSchema.omit({ discordUsers: true }).extend({
|
||||
PK: z.string(), // this is the door name
|
||||
SK: z.literal(ONBOARD_DOOR_CONFIG_SK).default(ONBOARD_DOOR_CONFIG_SK),
|
||||
name: z.string(),
|
||||
nonce: z.string(), // random UUID, used to check the oauth is actually from this user who initiated it
|
||||
});
|
||||
|
||||
export type OnboardDoorConfig = z.infer<typeof OnboardDoorConfigSchema>;
|
||||
|
||||
export const OnboardDoorConfigEntity: DynaBridgeEntity<OnboardDoorConfig> = {
|
||||
tableName: "doorman",
|
||||
id: ["PK", "SK"],
|
||||
};
|
||||
|
||||
export const getOnboardDoorId = (code: string): string[] => {
|
||||
return [
|
||||
code,
|
||||
ONBOARD_DOOR_CONFIG_SK,
|
||||
];
|
||||
}
|
||||
|
||||
export const OnboardDoorReqSchema = OnboardDoorConfigSchema.omit({ PK: true, SK: true, nonce: true });
|
||||
export type OnboardDoorReq = z.infer<typeof OnboardDoorReqSchema>;
|
||||
|
||||
export const createOnboardDoorConfig = (req: OnboardDoorReq): OnboardDoorConfig => {
|
||||
return {
|
||||
...req,
|
||||
PK: req.name,
|
||||
SK: ONBOARD_DOOR_CONFIG_SK,
|
||||
nonce: randomUUID(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { TwilioContext } from "../types/TwilioContext";
|
||||
import { DynaBridge } from "dynabridge";
|
||||
import { DoorConfigEntity, EditDoorConfigEntity } from "../schema/DoorConfig";
|
||||
import { DoorConfigEntity, EditDoorConfigEntity, OnboardDoorConfigEntity } from "../schema/DoorConfig";
|
||||
import { DoorAliasEntity } from "../schema/DoorAlias";
|
||||
import { LockStatusEntity } from "../schema/LockStatus";
|
||||
|
||||
@ -11,6 +11,7 @@ export const createDynaBridgeClient = (context: TwilioContext) => {
|
||||
editDoorConfig: EditDoorConfigEntity,
|
||||
doorAlias: DoorAliasEntity,
|
||||
lockStatus: LockStatusEntity,
|
||||
onboardDoorConfig: OnboardDoorConfigEntity,
|
||||
}, {
|
||||
serialize: (entity) => entity,
|
||||
deserialize: (entity) => {
|
||||
|
||||
@ -6,6 +6,7 @@ import { DoorPage, loader as doorpageloader } from './pages/DoorPage';
|
||||
import { ErrorPage } from './pages/ErrorPage';
|
||||
import { QueryRouter } from './routers/QueryRouter';
|
||||
import { EditPage } from './pages/EditPage';
|
||||
import { RedirectPage } from './pages/RedirectPage';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
@ -24,7 +25,9 @@ const router = createBrowserRouter([
|
||||
<QueryRouter
|
||||
mapping={{
|
||||
edit: <EditPage />,
|
||||
door: <DoorPage />
|
||||
door: <DoorPage />,
|
||||
onboard: <EditPage isOnboarding={true} />,
|
||||
state: <RedirectPage />
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { AppLayout, BreadcrumbGroup, Button, Container, Flashbar, FlashbarProps, Form, FormField, Header, Input, Link, SpaceBetween } from "@cloudscape-design/components";
|
||||
import { Alert, AppLayout, BreadcrumbGroup, Button, Container, Flashbar, FlashbarProps, Form, FormField, Header, Input, Link, SpaceBetween } from "@cloudscape-design/components";
|
||||
import { useLoaderData, useSearchParams } from "react-router-dom";
|
||||
import { DoorResponse } from "../types/DoorResponse";
|
||||
import { useForm } from "react-hook-form";
|
||||
@ -7,13 +7,23 @@ import { ReactNode, useState } from "react";
|
||||
import CInput from "react-hook-form-cloudscape/components/input";
|
||||
import CTextArea from "react-hook-form-cloudscape/components/textarea";
|
||||
|
||||
export type DoorEditForm = DoorResponse & { pin: string, fallbackNumber: string, discordUser: string };
|
||||
export type DoorEditForm = DoorResponse & { pin: string, fallbackNumber: string, discordUser: string, isConfirmed: boolean };
|
||||
|
||||
export const EditPage = () => {
|
||||
const EDIT_ROUTE = '/api/door/edit';
|
||||
const ONBOARD_ROUTE = '/api/door/onboard';
|
||||
|
||||
export interface EditPageProps {
|
||||
isOnboarding?: boolean;
|
||||
}
|
||||
|
||||
export const EditPage = ({ isOnboarding }: EditPageProps) => {
|
||||
const doorResponse = useLoaderData() as DoorResponse;
|
||||
const door = doorResponse.id;
|
||||
const { register, setValue, setError, getValues, watch, formState, clearErrors, control } = useForm<DoorEditForm>({
|
||||
defaultValues: doorResponse,
|
||||
const { setValue, setError, getValues, watch, formState, clearErrors, control, register } = useForm<DoorEditForm>({
|
||||
defaultValues: {
|
||||
...doorResponse,
|
||||
isConfirmed: !isOnboarding,
|
||||
},
|
||||
mode: "all",
|
||||
});
|
||||
|
||||
@ -45,13 +55,15 @@ export const EditPage = () => {
|
||||
return newAlert.id as string;
|
||||
}
|
||||
|
||||
|
||||
const fallbackNumbers = watch("fallbackNumbers");
|
||||
const discordUsers = watch("discordUsers");
|
||||
const fallbackNumbers = watch("fallbackNumbers") || [];
|
||||
const discordUsers = watch("discordUsers") || [];
|
||||
|
||||
const fallbackNumbersError = formState.errors.fallbackNumbers?.message;
|
||||
const discordUsersError = formState.errors.discordUsers?.message;
|
||||
|
||||
const backUrl = isOnboarding? '/': `?door=${door}`;
|
||||
const apiRoute = isOnboarding ? ONBOARD_ROUTE: EDIT_ROUTE;
|
||||
|
||||
return (
|
||||
<AppLayout
|
||||
contentType="form"
|
||||
@ -60,8 +72,8 @@ export const EditPage = () => {
|
||||
breadcrumbs={
|
||||
<BreadcrumbGroup
|
||||
items={[
|
||||
{ text: 'Door', href: '#' },
|
||||
{ text: door, href: `?edit&door=${door}` },
|
||||
{ text: 'Door', href: backUrl },
|
||||
isOnboarding ? { text: "Onboard", href: "?onboard" }: { text: door, href: `?edit&door=${door}` },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
@ -75,21 +87,44 @@ export const EditPage = () => {
|
||||
<Form
|
||||
actions={
|
||||
<SpaceBetween direction="horizontal" size="xs">
|
||||
<Button formAction="none" variant="link" href={`?door=${door}`}>
|
||||
<Button formAction="none" variant="link" href={backUrl}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
if (isOnboarding && !getValues("isConfirmed")) {
|
||||
setError("isConfirmed", {
|
||||
type: "manual",
|
||||
message: "Must confirm that Doorman number is configured",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const form = {
|
||||
...getValues(),
|
||||
id: undefined,
|
||||
name: getValues("id"),
|
||||
timeout: parseInt("" + getValues("timeout"))
|
||||
};
|
||||
|
||||
fetch(`/api/door/edit?door=${door}&newConfig=${encodeURIComponent(JSON.stringify(form))}`)
|
||||
fetch(apiRoute + `?door=${door}&newConfig=${encodeURIComponent(JSON.stringify(form))}`)
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
addAlert("success", `Created approval, check Discord notifcation from Doorman to confirm and approve the changes`);
|
||||
if (res.err) {
|
||||
addAlert("error", res.err);
|
||||
return;
|
||||
}
|
||||
if (!isOnboarding) {
|
||||
addAlert("success", `Created approval, check Discord notifcation from Doorman to confirm and approve the changes`);
|
||||
} else if (res.redirect) {
|
||||
// redirect for discord login
|
||||
addAlert("in-progress", `Created Door, you will now be redirected to Discord login to complete the onboarding`);
|
||||
|
||||
// redirect in 2 seconds
|
||||
setTimeout(() => {
|
||||
window.location = res.redirect;
|
||||
}, 2_000);
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
@ -97,35 +132,36 @@ export const EditPage = () => {
|
||||
</Button>
|
||||
</SpaceBetween>
|
||||
}
|
||||
header={<Header variant="h1">Edit Door</Header>}
|
||||
header={
|
||||
<Header variant="h1">
|
||||
{isOnboarding? "Onboard Door": "Edit Door"}
|
||||
</Header>
|
||||
}
|
||||
>
|
||||
<Container
|
||||
header={
|
||||
<Header variant="h2">
|
||||
Door - {door}
|
||||
</Header>
|
||||
}
|
||||
>
|
||||
<Container>
|
||||
<SpaceBetween direction="vertical" size="l">
|
||||
<FormField label="Buzzer Number (read-only)" constraintText="The phone number of your buzzer system">
|
||||
<CInput readOnly disabled name="buzzer" control={control} />
|
||||
<FormField label="Door Name" constraintText="Unique name for this Door, will be used in your Doorman URL">
|
||||
<CInput readOnly={!isOnboarding} disabled={!isOnboarding} name="id" control={control} />
|
||||
</FormField>
|
||||
<FormField label="PIN" constraintText={"The code to unlock the buzzer"}>
|
||||
<FormField label="Buzzer Number" constraintText="Phone number that calls you when your buzzer is called">
|
||||
<CInput readOnly={!isOnboarding} disabled={!isOnboarding} name="buzzer" control={control} />
|
||||
</FormField>
|
||||
<FormField label="PIN" constraintText={"Code to unlock this Door in Doorman"}>
|
||||
<CInput type="password" name="pin" control={control} />
|
||||
</FormField>
|
||||
<FormField label="Buzzer Code" constraintText={"The number that you dial on your buzzer"}>
|
||||
<FormField label="Buzzer Code" constraintText={"Extension that is used to reach your unit on your buzzer"}>
|
||||
<CInput name="buzzerCode" control={control} />
|
||||
</FormField>
|
||||
<FormField label="Timeout" constraintText="Time in seconds for the door to remain unlocked">
|
||||
<FormField label="Timeout" constraintText="Time in seconds for the door to remain unlocked from Doorman">
|
||||
<CInput type="number" name="timeout" control={control} />
|
||||
</FormField>
|
||||
<FormField label="Unlock key" constraintText="Key to press to buzz up on Intercom">
|
||||
<FormField label="Unlock Key" constraintText="Phone digit pressed to grant access">
|
||||
<CInput name="pressKey" control={control} />
|
||||
</FormField>
|
||||
<FormField
|
||||
errorText={fallbackNumbersError}
|
||||
label="Fallback numbers"
|
||||
constraintText="Phone numbers to dial through in case door is not unlocked"
|
||||
constraintText="Phone numbers to dial through in case Door is not unlocked by Doorman"
|
||||
>
|
||||
<InputTokenGroup
|
||||
name={"fallbackNumber"}
|
||||
@ -152,43 +188,53 @@ export const EditPage = () => {
|
||||
}}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="Discord Users"
|
||||
errorText={discordUsersError}
|
||||
constraintText={
|
||||
<>
|
||||
<Link fontSize={"body-s"} href="https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID#h_01HRSTXPS5H5D7JBY2QKKPVKNA">Discord users</Link> to receive notifications
|
||||
</>
|
||||
}
|
||||
>
|
||||
<InputTokenGroup
|
||||
name={"discordUser"}
|
||||
control={control}
|
||||
tokenGroupProps={{ items: discordUsers.map(n => ({ label: n }))}}
|
||||
onAdd={() => {
|
||||
const newValue = getValues().discordUser;
|
||||
if (newValue.length === 0) {
|
||||
return;
|
||||
}
|
||||
clearErrors("discordUsers");
|
||||
setValue("discordUsers", [...discordUsers, newValue]);
|
||||
setValue("discordUser", "");
|
||||
}}
|
||||
onDismiss={(i) => {
|
||||
clearErrors("discordUsers");
|
||||
{!isOnboarding &&
|
||||
<FormField
|
||||
label="Discord Users"
|
||||
errorText={discordUsersError}
|
||||
constraintText={
|
||||
<>
|
||||
<Link fontSize={"body-s"} href="https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID#h_01HRSTXPS5H5D7JBY2QKKPVKNA">Discord users</Link> to receive notifications
|
||||
</>
|
||||
}
|
||||
>
|
||||
<InputTokenGroup
|
||||
name={"discordUser"}
|
||||
control={control}
|
||||
tokenGroupProps={{ items: discordUsers.map(n => ({ label: n }))}}
|
||||
onAdd={() => {
|
||||
const newValue = getValues().discordUser;
|
||||
if (newValue.length === 0) {
|
||||
return;
|
||||
}
|
||||
clearErrors("discordUsers");
|
||||
setValue("discordUsers", [...discordUsers, newValue]);
|
||||
setValue("discordUser", "");
|
||||
}}
|
||||
onDismiss={(i) => {
|
||||
clearErrors("discordUsers");
|
||||
|
||||
if (discordUsers.length === 1) {
|
||||
setError("discordUsers", { message: "Can't remove last entry" })
|
||||
return;
|
||||
}
|
||||
discordUsers.splice(i, 1);
|
||||
setValue("discordUsers", [...discordUsers]);
|
||||
}}
|
||||
/>
|
||||
</FormField>
|
||||
if (discordUsers.length === 1) {
|
||||
setError("discordUsers", { message: "Can't remove last entry" })
|
||||
return;
|
||||
}
|
||||
discordUsers.splice(i, 1);
|
||||
setValue("discordUsers", [...discordUsers]);
|
||||
}}
|
||||
/>
|
||||
</FormField>
|
||||
}
|
||||
<FormField label="Welcome Message" constraintText="Message to display after a successful unlock">
|
||||
<CTextArea name="greeting" control={control} />
|
||||
</FormField>
|
||||
{isOnboarding &&
|
||||
<FormField label="Register Doorman in Building" constraintText="Confirm that you added our number to your buzzer system: 604-757-1824" errorText={formState.errors.isConfirmed?.message}>
|
||||
<label>
|
||||
I have added Doorman's number to my buzzer
|
||||
<input type="checkbox" {...register("isConfirmed")} />
|
||||
</label>
|
||||
</FormField>
|
||||
}
|
||||
</SpaceBetween>
|
||||
</Container>
|
||||
</Form>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { AppLayout, Container, Header, TextContent } from "@cloudscape-design/components"
|
||||
import { AppLayout, Container, Header, Link, TextContent } from "@cloudscape-design/components"
|
||||
|
||||
export const ErrorPage = () => {
|
||||
return (
|
||||
@ -11,7 +11,7 @@ export const ErrorPage = () => {
|
||||
<Header>Doorman</Header>
|
||||
<img src="doorman.png" />
|
||||
<TextContent>
|
||||
This isn't a valid door, please double check your URL
|
||||
This isn't a valid door, please double check your URL, or <Link href="/?onboard">Onboard</Link> to Doorman
|
||||
</TextContent>
|
||||
</Container>
|
||||
}
|
||||
|
||||
32
packages/doorman-ui/src/pages/RedirectPage.tsx
Normal file
32
packages/doorman-ui/src/pages/RedirectPage.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import type { DiscordOnboardingState } from "../../../doorman-api/src/functions/api/door/onboard";
|
||||
import { Spinner } from "@cloudscape-design/components";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const RedirectPage = () => {
|
||||
const [params, setParams] = useSearchParams();
|
||||
|
||||
const state: DiscordOnboardingState = JSON.parse(params.get("state") as string);
|
||||
const discordToken: string = params.get("code") as string;
|
||||
|
||||
// if this is not a redirect, then go home
|
||||
if (!state || !discordToken) {
|
||||
window.location.href = '/';
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetch(state.apiRedirect + '?' + params.toString())
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
if (res.err) {
|
||||
console.error(res);
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = res.redirect;
|
||||
});
|
||||
}, []);
|
||||
|
||||
return <Spinner />;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user