Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: platform oAuth clients frontend #12867

Merged
merged 35 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b7c0bad
add oauth client to settings
Ryukemeister Dec 5, 2023
7e3a9bf
fix imports
Ryukemeister Dec 5, 2023
5dc5039
add react and axios
Ryukemeister Dec 5, 2023
a8fa989
oauth client form and card components
Ryukemeister Dec 5, 2023
add8f5f
hooks for oauth clients data
Ryukemeister Dec 5, 2023
614a4b4
index page for oauth clients
Ryukemeister Dec 5, 2023
7c78261
oauth client list component
Ryukemeister Dec 5, 2023
b5c9ae3
oauth client form page
Ryukemeister Dec 5, 2023
db73d9d
shift atoms into platform
Ryukemeister Dec 6, 2023
e3a25a2
init platform folder
Ryukemeister Dec 6, 2023
c221e6e
refactor handleSubmit functioin
Ryukemeister Dec 6, 2023
58d7736
platform
Ryukemeister Dec 14, 2023
12b91be
platform
Ryukemeister Dec 14, 2023
41b8fd6
platform parts
Ryukemeister Dec 14, 2023
51014c0
Merge branch 'platform' into platform-oauth-clients
ThyMinimalDev Dec 14, 2023
8accc90
revert tsconfig constant platform
ThyMinimalDev Dec 14, 2023
e8700b2
fix: useOauthClients
ThyMinimalDev Dec 14, 2023
ea37476
Merge branch 'platform' into platform-oauth-clients
ThyMinimalDev Dec 14, 2023
26b5f79
feat: create oauth client with api
ThyMinimalDev Dec 14, 2023
14d2353
fix: add prettier to platform type package
ThyMinimalDev Dec 14, 2023
96a6d65
fixup! fix: add prettier to platform type package
ThyMinimalDev Dec 14, 2023
a76a733
chore: class-validator types in platform package
ThyMinimalDev Dec 15, 2023
4739f22
add types for delete oauth client iput
Ryukemeister Dec 19, 2023
8f73c30
add onSuccess and onError methods
Ryukemeister Dec 19, 2023
587d374
update oauth client card view with client id and secret
Ryukemeister Dec 19, 2023
52fefa4
cleanup comments
Ryukemeister Dec 19, 2023
26d7fa3
split oauth persisit hook into create and delete hooks
Ryukemeister Dec 19, 2023
e3dbef9
fix: oauth client creation / deletion / listing
ThyMinimalDev Dec 19, 2023
8455c95
fixup! fix: oauth client creation / deletion / listing
ThyMinimalDev Dec 19, 2023
7de592a
fix: comment logo for now
ThyMinimalDev Dec 19, 2023
8120996
fix: layout setting org keys
ThyMinimalDev Dec 19, 2023
922a911
cleanup comments
Ryukemeister Dec 19, 2023
256f4b3
minor style fixes, add logic for client permissions
Ryukemeister Dec 19, 2023
cb4467c
show toast after deleting client
Ryukemeister Dec 19, 2023
4194480
not passing clint logo at the moment
Ryukemeister Dec 19, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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