Skip to content

Commit

Permalink
Merge pull request #115 from supertokens/feat/account-linking
Browse files Browse the repository at this point in the history
feat: Add account linking support
  • Loading branch information
rishabhpoddar authored Sep 13, 2023
2 parents 672340b + 9189007 commit 1d0f7c4
Show file tree
Hide file tree
Showing 55 changed files with 1,486 additions and 681 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [unreleased]

## [0.8.0] - 2023-09-14

- Adds support for account linking

## [0.7.2] - 2023-09-11

- Enforces read, write permissions for allowed user on the user management dashboard.
Expand Down
51 changes: 51 additions & 0 deletions api_spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,57 @@ paths:
type: string
enum:
- Not Found
/user/unlink:
get:
tags:
- User Details
summary: Unlink a user account
operationId: userUnlink
parameters:
- name: authorization
in: header
required: true
schema:
type: string
example: "Bearer API_KEY"
- name: recipeUserId
in: query
required: true
schema:
type: string
responses:
200:
description: Success
content:
application/json:
schema:
type: object
properties:
status:
type: string
default: "OK"
400:
description: error code 400
content:
text/plain:
schema:
type: string
401:
description: Unauthorised access
content:
text/plain:
schema:
type: string
enum:
- Unauthorised access
404:
description: error code 404
content:
text/plain:
schema:
type: string
enum:
- Not Found
/user/email/verify:
get:
tags:
Expand Down
5 changes: 5 additions & 0 deletions build/asset-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@
"static/media/phone-no.svg": "/static/media/phone-no.svg",
"static/media/provider-google.svg": "/static/media/provider-google.svg",
"static/media/no-users-graphic.svg": "/static/media/no-users-graphic.svg",
"static/media/unlink-login-method.png": "/static/media/unlink-login-method.png",
"static/media/eye-stroke.svg": "/static/media/eye-stroke.svg",
"static/media/people-restricted.svg": "/static/media/people-restricted.svg",
"static/media/mail-opened.svg": "/static/media/mail-opened.svg",
"static/media/form-field-error-icon.svg": "/static/media/form-field-error-icon.svg",
"static/media/provider-github.svg": "/static/media/provider-github.svg",
"static/media/edit-login-method.png": "/static/media/edit-login-method.png",
"static/media/mail.svg": "/static/media/mail.svg",
"static/media/lock-opened.svg": "/static/media/lock-opened.svg",
"static/media/eye.svg": "/static/media/eye.svg",
Expand All @@ -38,6 +40,7 @@
"static/media/star_sparkle.svg": "/static/media/star_sparkle.svg",
"static/media/close.svg": "/static/media/close.svg",
"static/media/copy.svg": "/static/media/copy.svg",
"static/media/delete-login-method.png": "/static/media/delete-login-method.png",
"static/media/close-icon.svg": "/static/media/close-icon.svg",
"static/media/edit.svg": "/static/media/edit.svg",
"static/media/right_arrow_icon.svg": "/static/media/right_arrow_icon.svg",
Expand All @@ -51,6 +54,8 @@
"static/media/chevron-right.svg": "/static/media/chevron-right.svg",
"static/media/people.svg": "/static/media/people.svg",
"static/media/provider-facebook.svg": "/static/media/provider-facebook.svg",
"static/media/Union-yellow.png": "/static/media/Union-yellow.png",
"static/media/Union.png": "/static/media/Union.png",
"static/media/checkmark-yellow.svg": "/static/media/checkmark-yellow.svg",
"static/media/triangle-down.svg": "/static/media/triangle-down.svg",
"main.css.map": "/static/css/main.css.map",
Expand Down
4 changes: 2 additions & 2 deletions build/static/css/main.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/static/css/main.css.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/static/js/bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/static/js/bundle.js.map

Large diffs are not rendered by default.

Binary file added build/static/media/Union-yellow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added build/static/media/Union.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added build/static/media/delete-login-method.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added build/static/media/edit-login-method.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added build/static/media/unlink-login-method.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dashboard",
"version": "0.7.2",
"version": "0.8.0",
"private": true,
"dependencies": {
"@babel/core": "^7.16.0",
Expand Down
23 changes: 23 additions & 0 deletions server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ import morgan from "morgan";
import SuperTokens from "supertokens-node";
import { errorHandler, middleware } from "supertokens-node/framework/express";
import Dashboard from "supertokens-node/lib/build/recipe/dashboard/recipe";
import AccountLinking from "supertokens-node/recipe/accountlinking";
import EmailPassword from "supertokens-node/recipe/emailpassword";
import EmailVerification from "supertokens-node/recipe/emailverification";
import Passwordless from "supertokens-node/recipe/passwordless";
import Session from "supertokens-node/recipe/session";
import ThirdParty from "supertokens-node/recipe/thirdparty";
import UserMetaData from "supertokens-node/recipe/usermetadata";
import RecipeUserId from "../../supertokens-node/lib/build/recipeUserId";

const websiteDomain = "http://localhost:3000";

Expand All @@ -44,6 +46,7 @@ SuperTokens.init({
},
recipeList: [
Dashboard.init({
apiKey: "test",
// Keep this so that the dev server uses api key based login
override: {
functions: (original) => {
Expand Down Expand Up @@ -85,6 +88,7 @@ SuperTokens.init({
mode: "REQUIRED",
}),
Session.init(),
AccountLinking.init(),
],
});

Expand All @@ -104,6 +108,25 @@ app.get("/status", (req, res) => {
res.status(200).send("Started");
});

app.get("/link", async (req, res) => {
await AccountLinking.linkAccounts(
"public",
new RecipeUserId("6b763048-486f-4965-b2e0-2f7650efbdf5"),
"6f922cbf-99de-4078-a9d0-e67dff5df09d"
);
await AccountLinking.linkAccounts(
"public",
new RecipeUserId("9a8837c0-ee02-457b-93bd-61bf16a6c2f9"),
"6f922cbf-99de-4078-a9d0-e67dff5df09d"
);
await AccountLinking.linkAccounts(
"public",
new RecipeUserId("a31e669f-553a-40dc-9192-1b06c9d75d31"),
"6f922cbf-99de-4078-a9d0-e67dff5df09d"
);
return res.status(200).send("OK");
});

app.use((err: any, req: Request, res: Response, next: NextFunction) => {
// Leaving this in because it helps with debugging
console.log("Internal error", err);
Expand Down
8 changes: 6 additions & 2 deletions src/api/user/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,22 @@ import { getApiUrl, useFetchData } from "../../utils";
type TDeleteUserResponse = Promise<{ status: "OK" } | undefined>;

interface IUseDeleteUserService {
deleteUser: (userId: string) => TDeleteUserResponse;
deleteUser: (userId: string, removeAllLinkedAccounts: boolean) => TDeleteUserResponse;
}

const useDeleteUserService = (): IUseDeleteUserService => {
const fetchData = useFetchData();

const deleteUser = async (userId: string): Promise<{ status: "OK" } | undefined> => {
const deleteUser = async (
userId: string,
removeAllLinkedAccounts: boolean
): Promise<{ status: "OK" } | undefined> => {
const response = await fetchData({
url: getApiUrl("/api/user"),
method: "DELETE",
query: {
userId,
removeAllLinkedAccounts: String(removeAllLinkedAccounts),
},
});

Expand Down
2 changes: 1 addition & 1 deletion src/api/user/email/verify/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const useVerifyUserEmail = (): IUseVerifyUserEmailService => {
url: getApiUrl("/api/user/email/verify", tenantId),
method: "PUT",
config: {
body: JSON.stringify({ verified: isEmailVerified, userId }),
body: JSON.stringify({ verified: isEmailVerified, recipeUserId: userId }),
},
});
return response?.ok;
Expand Down
2 changes: 1 addition & 1 deletion src/api/user/email/verify/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const useVerifyUserTokenService = (): IUseVerifyUserTokenService => {
method: "POST",
config: {
body: JSON.stringify({
userId,
recipeUserId: userId,
}),
},
});
Expand Down
19 changes: 9 additions & 10 deletions src/api/user/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,18 @@
* under the License.
*/

import { UserWithRecipeId } from "../../ui/pages/usersList/types";
import { User } from "../../ui/pages/usersList/types";
import { getApiUrl, useFetchData } from "../../utils";

interface IUseUserService {
updateUserInformation: (args: IUpdateUserInformationArgs) => Promise<UpdateUserInformationResponse>;
getUser: (userId: string, recipeId: string) => Promise<GetUserInfoResult>;
getUser: (userId: string) => Promise<GetUserInfoResult>;
}

interface IUpdateUserInformationArgs {
export interface IUpdateUserInformationArgs {
userId: string;
recipeId: string;
recipeUserId: string;
tenantId: string | undefined;
email?: string;
phone?: string;
Expand All @@ -40,7 +41,7 @@ export type GetUserInfoResult =
}
| {
status: "OK";
user: UserWithRecipeId;
user: User;
};

export type UpdateUserInformationResponse =
Expand All @@ -55,13 +56,12 @@ export type UpdateUserInformationResponse =
export const useUserService = (): IUseUserService => {
const fetchData = useFetchData();

const getUser = async (userId: string, recipeId: string): Promise<GetUserInfoResult> => {
const getUser = async (userId: string): Promise<GetUserInfoResult> => {
const response = await fetchData({
url: getApiUrl("/api/user"),
method: "GET",
query: {
userId,
recipeId,
},
});

Expand All @@ -80,10 +80,7 @@ export const useUserService = (): IUseUserService => {
};
}

return {
status: "OK",
user: body,
};
return body;
}

return {
Expand All @@ -94,6 +91,7 @@ export const useUserService = (): IUseUserService => {
const updateUserInformation = async ({
userId,
recipeId,
recipeUserId,
email,
phone,
firstName,
Expand All @@ -116,6 +114,7 @@ export const useUserService = (): IUseUserService => {
body: JSON.stringify({
recipeId,
userId,
recipeUserId,
phone: phoneToSend,
email: emailToSend,
firstName: firstNameToSend,
Expand Down
2 changes: 1 addition & 1 deletion src/api/user/password/reset/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const usePasswordResetService = (): IUsePasswordResetService => {
query: { userId },
config: {
body: JSON.stringify({
userId,
recipeUserId: userId,
newPassword,
}),
},
Expand Down
39 changes: 39 additions & 0 deletions src/api/user/unlink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { getApiUrl, useFetchData } from "../../utils";

type TUnlinkUserResponse = Promise<{ status: "OK" } | undefined>;

interface IUseUnlinkService {
unlinkUser: (userId: string) => TUnlinkUserResponse;
}

const useUnlinkService = (): IUseUnlinkService => {
const fetchData = useFetchData();

const unlinkUser = async (recipeUserId: string): Promise<{ status: "OK" } | undefined> => {
const response = await fetchData({
url: getApiUrl("/api/user/unlink"),
method: "GET",
query: {
recipeUserId: recipeUserId,
},
});

if (response.ok) {
const body = await response.json();

if (body.status !== "OK") {
return undefined;
}

return body;
}

return undefined;
};

return {
unlinkUser,
};
};

export default useUnlinkService;
Binary file added src/assets/Union-yellow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/Union.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/delete-login-method.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/edit-login-method.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/unlink-login-method.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,8 @@ import "./assets/trash-opened.svg";
import "./assets/trash.svg";
import "./assets/triangle-down.svg";
import "./logo.svg";
import "./assets/Union.png";
import "./assets/Union-yellow.png";
import "./assets/delete-login-method.png";
import "./assets/edit-login-method.png";
import "./assets/unlink-login-method.png";
1 change: 1 addition & 0 deletions src/ui/components/auth/SignInWithApiKeyContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const SignInWithApiKeyContent = (props: SignInWithApiKeyContentProps) => {
<button
className="button"
type="submit"
style={{ marginTop: "1em" }}
disabled={loading}>
<span>Continue</span>{" "}
<img
Expand Down
5 changes: 3 additions & 2 deletions src/ui/components/copyText/CopyText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,17 @@ import "./CopyText.scss";
type CopyTextProps = {
showChild?: boolean;
children: string;
copyVal?: string;
};

export const CopyText: React.FC<CopyTextProps> = ({ showChild = true, children }: CopyTextProps) => {
export const CopyText: React.FC<CopyTextProps> = ({ showChild = true, children, copyVal }: CopyTextProps) => {
const alertWidth = 80;
const copyBoxRef = useRef<HTMLDivElement>(null);
const [isCopied, setIsCopied] = useState<boolean>(false);
const copyClick = useCallback(() => {
if (!isCopied) {
setIsCopied(true);
void navigator.clipboard.writeText(children);
void navigator.clipboard.writeText(copyVal ?? children);
}
}, [isCopied, children]);

Expand Down
Loading

0 comments on commit 1d0f7c4

Please sign in to comment.