Skip to content

Commit

Permalink
feat: platform oAuth clients frontend (#12867)
Browse files Browse the repository at this point in the history
* add oauth client to settings

* fix imports

* add react and axios

* oauth client form and card components

* hooks for oauth clients data

* index page for oauth clients

* oauth client list component

* oauth client form page

* shift atoms into platform

* init platform folder

* refactor handleSubmit functioin

* platform

* platform

* platform parts

* revert tsconfig constant platform

* fix: useOauthClients

* feat: create oauth client with api

* fix: add prettier to platform type package

* fixup! fix: add prettier to platform type package

* chore: class-validator types in platform package

* add types for delete oauth client iput

* add onSuccess and onError methods

* update oauth client card view with client id and secret

* cleanup comments

* split oauth persisit hook into create and delete hooks

* fix: oauth client creation / deletion / listing

* fixup! fix: oauth client creation / deletion / listing

* fix: comment logo for now

* fix: layout setting org keys

* cleanup comments

* minor style fixes, add logic for client permissions

* show toast after deleting client

* not passing clint logo at the moment

---------

Co-authored-by: Morgan Vernay <[email protected]>
  • Loading branch information
Ryukemeister and ThyMinimalDev authored Dec 19, 2023
1 parent 64e1658 commit b987f6e
Show file tree
Hide file tree
Showing 22 changed files with 693 additions and 17 deletions.
2 changes: 1 addition & 1 deletion apps/api/v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"dev": "yarn workspace @calcom/platform-constants build:watch & yarn workspace @calcom/platform-utils build:watch & nest start --watch",
"dev": "yarn workspace @calcom/platform-constants build:watch & yarn workspace @calcom/platform-utils build:watch & yarn workspace @calcom/platform-types build:watch & nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"test": "jest",
Expand Down
6 changes: 3 additions & 3 deletions apps/api/v2/src/modules/oauth/oauth-client.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { GetUser } from "@/modules/auth/decorator";
import { Roles } from "@/modules/auth/decorator/roles/roles.decorator";
import { NextAuthGuard } from "@/modules/auth/guard";
import { OrganizationRolesGuard } from "@/modules/auth/guard/organization-roles/organization-roles.guard";
import { CreateOAuthClientInput } from "@/modules/oauth/input/create-oauth-client";
import { UpdateOAuthClientInput } from "@/modules/oauth/input/update-oauth-client";
import { OAuthClientRepository } from "@/modules/oauth/oauth-client.repository";
import {
Expand All @@ -22,7 +21,8 @@ import {
import { MembershipRole, PlatformOAuthClient } from "@prisma/client";

import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { ApiResponse } from "@calcom/platform-types";
import { CreateOAuthClientInput } from "@calcom/platform-types";
import type { ApiResponse } from "@calcom/platform-types";

@Controller({
path: "oauth-clients",
Expand Down Expand Up @@ -88,7 +88,7 @@ export class OAuthClientController {
}

@Delete("/:clientId")
@HttpCode(HttpStatus.NO_CONTENT)
@HttpCode(HttpStatus.OK)
@Roles([MembershipRole.ADMIN, MembershipRole.OWNER])
async deleteOAuthClient(@Param("clientId") clientId: string): Promise<ApiResponse<PlatformOAuthClient>> {
this.logger.log(`Deleting OAuth Client with ID: ${clientId}`);
Expand Down
3 changes: 2 additions & 1 deletion apps/api/v2/src/modules/oauth/oauth-client.repository.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { CreateOAuthClientInput } from "@/modules/oauth/input/create-oauth-client";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import type { PlatformOAuthClient } from "@prisma/client";

import type { CreateOAuthClientInput } from "@calcom/platform-types";

@Injectable()
export class OAuthClientRepository {
constructor(
Expand Down
2 changes: 1 addition & 1 deletion apps/api/v2/test/oauth.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { AuthModule } from "@/modules/auth/auth.module";
import { NextAuthStrategy } from "@/modules/auth/strategy";
import { CreateOAuthClientInput } from "@/modules/oauth/input/create-oauth-client";
import { UpdateOAuthClientInput } from "@/modules/oauth/input/update-oauth-client";
import { OAuthClientModule } from "@/modules/oauth/oauth-client.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
Expand All @@ -15,6 +14,7 @@ import { Membership, PlatformOAuthClient, Team, User } from "@prisma/client";
import * as request from "supertest";

import { SUCCESS_STATUS } from "@calcom/platform-constants";
import type { CreateOAuthClientInput } from "@calcom/platform-types";
import { ApiSuccessResponse } from "@calcom/platform-types";

import { bootstrap } from "../src/app";
Expand Down
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
"react-multi-email": "^0.5.3",
"react-phone-input-2": "^2.15.1",
"react-phone-number-input": "^3.2.7",
"react-query": "^3.39.3",
"react-schemaorg": "^2.0.0",
"react-select": "^5.7.0",
"react-timezone-select": "^1.4.0",
Expand Down
131 changes: 131 additions & 0 deletions apps/web/pages/auth/platform/authorize.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// import { useOAuthClient } from "@pages/settings/platform/oauth-clients/hooks/useOAuthClients";
import { useRouter } from "next/navigation";

import { APP_NAME } from "@calcom/lib/constants";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Avatar, Button } from "@calcom/ui";
import { Info } from "@calcom/ui/components/icon";
import { Plus } from "@calcom/ui/components/icon";

import PageWrapper from "@components/PageWrapper";

import { PERMISSIONS_GROUPED_MAP } from "../../../../../packages/platform/constants/permissions";
import { hasPermission } from "../../../../../packages/platform/utils/permissions";

export default function Authorize() {
const { t } = useLocale();
const router = useRouter();

const searchParams = useCompatSearchParams();
const queryString = searchParams?.toString();

// const { isLoading, error, data: client } = useOAuthClient(queryString);

const client: {
name: string;
logo?: string;
redirect_uris: string[];
permissions: number;
} = {
name: "Acme.com",
redirect_uris: ["", ""],
permissions: 7,
};

console.log("These are the search params:", queryString);

const permissions = Object.values(PERMISSIONS_GROUPED_MAP).map((value) => {
let permissionsMessage = "";
const hasReadPermission = hasPermission(client.permissions, value.read);
const hasWritePermission = hasPermission(client.permissions, value.write);

if (hasReadPermission || hasWritePermission) {
permissionsMessage = hasReadPermission ? "Read" : "Write";
}

if (hasReadPermission && hasWritePermission) {
permissionsMessage = "Read, write";
}

return (
!!permissionsMessage && (
<li key={value.read} className="relative pl-5 text-sm">
<span className="absolute left-0">&#10003;</span>
{permissionsMessage} your {`${value.label}s`.toLocaleLowerCase()}
</li>
)
);
});

return (
<div className="flex min-h-screen items-center justify-center">
<div className="mt-2 max-w-xl rounded-md bg-white px-9 pb-3 pt-2">
<div className="flex items-center justify-center">
{/*
below is where the client logo will be displayed
first we check if the client has a logo property and display logo if present
else we take logo from user profile pic
*/}
{client.logo ? (
<Avatar
alt=""
fallback={<Plus className="text-subtle h-6 w-6" />}
className="items-center"
imageSrc={client.logo}
size="lg"
/>
) : (
<Avatar
alt=""
fallback={<Plus className="text-subtle h-6 w-6" />}
className="items-center"
imageSrc="/cal-com-icon.svg"
size="lg"
/>
)}
<div className="relative -ml-6 h-24 w-24">
<div className="absolute inset-0 flex items-center justify-center">
<div className="flex h-[70px] w-[70px] items-center justify-center rounded-full bg-white">
<img src="/cal-com-icon.svg" alt="Logo" className="h-16 w-16 rounded-full" />
</div>
</div>
</div>
</div>
<h1 className="px-5 pb-5 pt-3 text-center text-2xl font-bold tracking-tight text-black">
{t("access_cal_account", { clientName: client.name, appName: APP_NAME })}
</h1>
<div className="mb-4 mt-5 font-medium text-black">
{t("allow_client_to", { clientName: client.name })}
</div>
<ul className="space-y-4 text-sm text-black">{permissions}</ul>
<div className="bg-subtle mb-8 mt-8 flex rounded-md p-3">
<div>
<Info className="mr-1 mt-0.5 h-4 w-4" />
</div>
<div className="ml-1 ">
<div className="mb-1 text-sm font-medium">
{t("allow_client_to_do", { clientName: client.name })}
</div>
<div className="text-sm">{t("oauth_access_information", { appName: APP_NAME })}</div>{" "}
</div>
</div>
<div className="border-subtle border- -mx-9 mb-4 border-b" />
<div className="flex justify-end">
<Button
className="bg-primary mr-2 text-black"
onClick={() => {
router.back();
}}>
{t("go_back")}
</Button>
<Button data-testid="allow-button" className="bg-black text-white">
{t("allow")}
</Button>
</div>
</div>
</div>
);
}

Authorize.PageWrapper = PageWrapper;
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { Asterisk, Clipboard } from "lucide-react";
import React from "react";

import { classNames } from "@calcom/lib";
import { PERMISSIONS_GROUPED_MAP } from "@calcom/platform-constants";
import type { Avatar } from "@calcom/prisma/client";
import { Button, showToast } from "@calcom/ui";

import { hasPermission } from "../../../../../../../../packages/platform/utils/permissions";

type OAuthClientCardProps = {
name: string;
logo?: Avatar;
redirect_uris: string[];
permissions: number;
lastItem: boolean;
id: string;
secret: string;
onDelete: (id: string) => Promise<void>;
isLoading: boolean;
};

export const OAuthClientCard = ({
name,
logo,
redirect_uris,
permissions,
id,
secret,
lastItem,
onDelete,
isLoading,
}: OAuthClientCardProps) => {
const clientPermissions = Object.values(PERMISSIONS_GROUPED_MAP).map((value, index) => {
let permissionsMessage = "";
const hasReadPermission = hasPermission(permissions, value.read);
const hasWritePermission = hasPermission(permissions, value.write);

if (hasReadPermission || hasWritePermission) {
permissionsMessage = hasReadPermission ? "read" : "write";
}

if (hasReadPermission && hasWritePermission) {
permissionsMessage = "read/write";
}

return (
!!permissionsMessage && (
<div key={value.read} className="relative text-sm">
&nbsp;{permissionsMessage} {`${value.label}s`.toLocaleLowerCase()}
{Object.values(PERMISSIONS_GROUPED_MAP).length === index + 1 ? " " : ", "}
</div>
)
);
});

return (
<div
className={classNames(
"flex w-full justify-between px-4 py-4 sm:px-6",
lastItem ? "" : "border-subtle border-b"
)}>
<div className="flex flex-col gap-2">
<div className="flex gap-1">
<p className="font-semibold">
Client name: <span className="font-normal">{name}</span>
</p>
</div>
{!!logo && (
<div>
<>{logo}</>
</div>
)}
<div className="flex flex-col gap-2">
<div className="flex flex-row items-center gap-2">
<div className="font-semibold">Client Id:</div>
<div>{id}</div>
<Clipboard
type="button"
className="h-4 w-4 cursor-pointer"
onClick={() => {
navigator.clipboard.writeText(id);
showToast("Client id copied to clipboard.", "success");
}}
/>
</div>
</div>
<div className="flex items-center gap-2">
<div className="font-semibold">Client Secret:</div>
<div className="flex items-center justify-center rounded-md">
{[...new Array(20)].map((_, index) => (
<Asterisk key={`${index}asterisk`} className="h-2 w-2" />
))}
<Clipboard
type="button"
className="ml-2 h-4 w-4 cursor-pointer"
onClick={() => {
navigator.clipboard.writeText(secret);
showToast("Client secret copied to clipboard.", "success");
}}
/>
</div>
</div>
<div className="border-subtle flex text-sm">
<span className="font-semibold">Permissions: </span>
<div className="flex">{clientPermissions}</div>
</div>
<div className="flex gap-1 text-sm">
<span className="font-semibold">Redirect uris: </span>
{redirect_uris.map((item, index) => (redirect_uris.length === index + 1 ? `${item}` : `${item}, `))}
</div>
</div>
<div className="flex items-center">
<Button
className="bg-red-500 text-white hover:bg-red-600"
loading={isLoading}
disabled={isLoading}
onClick={() => onDelete(id)}>
Delete
</Button>
</div>
</div>
);
};
Loading

0 comments on commit b987f6e

Please sign in to comment.