From 5c9094390fb79867b8d1586bcf236f5ab2c7d222 Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Tue, 30 Aug 2022 21:08:54 +0300 Subject: [PATCH] oauth2 view start, simple modal --- components/LoginPage/LoginPage.module.scss | 33 +++++ components/LoginPage/LoginPage.tsx | 13 +- components/OAuth2Page/OAuth2Page.module.scss | 29 +++++ components/OAuth2Page/OAuth2Page.tsx | 120 ++++++++++++++++++ components/common/Header/Header.tsx | 10 +- .../common/Modal/Modal/Modal.module.scss | 32 +++++ components/common/Modal/Modal/Modal.tsx | 5 + .../common/Modal/ModalBody/ModalBody.tsx | 9 ++ .../common/Modal/ModalFooter/ModalFooter.tsx | 9 ++ .../common/Modal/ModalHeader/ModalHeader.tsx | 9 ++ .../Modal/ModalRoot/ModalRoot.module.scss | 8 ++ .../common/Modal/ModalRoot/ModalRoot.tsx | 53 ++++++++ .../common/Modal/services/ModalService.ts | 25 ++++ lib/types/modal.interface.ts | 13 ++ lib/types/oauth2-client.interface.ts | 37 ++++++ lib/types/picture.interface.ts | 5 + lib/types/users.interfaces.ts | 9 +- pages/_app.tsx | 2 + pages/api/login.ts | 1 + pages/oauth2.tsx | 3 + public/application.png | Bin 0 -> 14817 bytes styles/_breakpoint.scss | 28 ++++ styles/globals.scss | 10 ++ 23 files changed, 447 insertions(+), 16 deletions(-) create mode 100644 components/OAuth2Page/OAuth2Page.module.scss create mode 100644 components/OAuth2Page/OAuth2Page.tsx create mode 100644 components/common/Modal/Modal/Modal.module.scss create mode 100644 components/common/Modal/Modal/Modal.tsx create mode 100644 components/common/Modal/ModalBody/ModalBody.tsx create mode 100644 components/common/Modal/ModalFooter/ModalFooter.tsx create mode 100644 components/common/Modal/ModalHeader/ModalHeader.tsx create mode 100644 components/common/Modal/ModalRoot/ModalRoot.module.scss create mode 100644 components/common/Modal/ModalRoot/ModalRoot.tsx create mode 100644 components/common/Modal/services/ModalService.ts create mode 100644 lib/types/modal.interface.ts create mode 100644 lib/types/oauth2-client.interface.ts create mode 100644 lib/types/picture.interface.ts create mode 100644 pages/oauth2.tsx create mode 100644 public/application.png create mode 100644 styles/_breakpoint.scss diff --git a/components/LoginPage/LoginPage.module.scss b/components/LoginPage/LoginPage.module.scss index e69de29..e81e9e2 100644 --- a/components/LoginPage/LoginPage.module.scss +++ b/components/LoginPage/LoginPage.module.scss @@ -0,0 +1,33 @@ +.wrapper { + width: 100vw; + height: 100vh; + overflow: hidden; + + .loginBox { + max-width: 480px; + min-height: 240px; + gap: 2rem; + background-color: #fff; + margin: 0 auto; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin-top: 4%; + + h1 { + margin-top: 0; + text-align: center; + } + + .loginButton { + display: flex; + padding: 1rem; + background: #00aaff; + color: #000; + font-weight: bold; + border-radius: 8px; + } + } +} diff --git a/components/LoginPage/LoginPage.tsx b/components/LoginPage/LoginPage.tsx index faad930..808062b 100644 --- a/components/LoginPage/LoginPage.tsx +++ b/components/LoginPage/LoginPage.tsx @@ -1,10 +1,15 @@ import Link from 'next/link'; +import styles from './LoginPage.module.scss'; export const LoginPage = () => { return ( - <> -

Log in

- Log in with Icy Network - +
+
+

Icy Network Administration

+ + Log in with Icy Network + +
+
); }; diff --git a/components/OAuth2Page/OAuth2Page.module.scss b/components/OAuth2Page/OAuth2Page.module.scss new file mode 100644 index 0000000..f531465 --- /dev/null +++ b/components/OAuth2Page/OAuth2Page.module.scss @@ -0,0 +1,29 @@ +.clientCard { + display: flex; + flex-direction: row; + gap: 1rem; + padding: 0.5rem; + box-shadow: 0 0 8px rgba(0, 0, 0, 0.25); + .pictureWrapper { + overflow: hidden; + } + + .clientInfo { + display: flex; + flex-direction: column; + + h2 { + padding: 0; + margin: 0; + .clientname { + font-size: 0.875rem; + } + } + } +} + +.clientList { + display: flex; + flex-direction: column; + gap: 1rem; +} diff --git a/components/OAuth2Page/OAuth2Page.tsx b/components/OAuth2Page/OAuth2Page.tsx new file mode 100644 index 0000000..b8d16a9 --- /dev/null +++ b/components/OAuth2Page/OAuth2Page.tsx @@ -0,0 +1,120 @@ +import useSWR from 'swr'; +import Image from 'next/image'; +import { OAuth2ClientListItem } from '../../lib/types/oauth2-client.interface'; +import { PaginatedResponse } from '../../lib/types/paginated-response.interface'; +import { Paginator } from '../common/Paginator/Paginator'; +import styles from './OAuth2Page.module.scss'; +import { UPLOADS_URL } from '../../lib/constants'; +import application from '../../public/application.png'; +import { useState } from 'react'; +import useUser from '../../lib/hooks/useUser'; +import { Container } from '../common/Container/Container'; +import { Header } from '../common/Header/Header'; +import Modal from '../common/Modal/Modal/Modal'; +import ModalHeader from '../common/Modal/ModalHeader/ModalHeader'; +import ModalBody from '../common/Modal/ModalBody/ModalBody'; +import ModalFooter from '../common/Modal/ModalFooter/ModalFooter'; +import ModalService from '../common/Modal/services/ModalService'; + +const TestModal = ({ close }: { close: (...args: any[]) => void }) => { + return ( + + +

Test!

+
+ +

This is only a test

+
+ + + +
+ ); +}; + +const OAuth2ClientCard = ({ client }: { client: OAuth2ClientListItem }) => ( +
+
+ {client.picture ? ( + + ) : ( + + )} +
+
+

{client.title}

+ {client.description} +
+
Client ID
+
{client.client_id}
+
Allowed scopes
+
{client.scope?.split(' ').join(', ')}
+
Allowed grant types
+
{client.grants?.split(' ').join(', ')}
+
Activated
+
{client.activated ? 'Yes' : NOT ACTIVATED}
+
Verified
+
{client.verified ? 'Yes' : 'No'}
+
Created
+
{new Date(client.created_at).toDateString()}
+
+
+
+); + +const OAuth2ClientList = ({ + pageIndex, + searchTerm, + setPage, +}: { + pageIndex: number; + searchTerm: string; + setPage: (page: number) => void; +}) => { + const { data } = useSWR>( + `/api/admin/oauth2/clients?page=${pageIndex}${ + searchTerm ? `&q=${searchTerm}` : '' + }` + ); + + return data ? ( + <> + +
+ {data.list.map((client) => ( + + ))} + {data?.pagination && ( + + )} +
+ + ) : ( + Nothing found + ); +}; + +export const OAuth2Page = () => { + const { user } = useUser({ redirectTo: '/login' }); + const [pageIndex, setPageIndex] = useState(1); + const [searchTerm, setSearchTerm] = useState(''); + + return ( + <> +
+ +

OAuth2 clients

+ +
+ + ); +}; diff --git a/components/common/Header/Header.tsx b/components/common/Header/Header.tsx index d6d2e2f..fbfcf41 100644 --- a/components/common/Header/Header.tsx +++ b/components/common/Header/Header.tsx @@ -16,16 +16,16 @@ const navItems = [ title: 'Users', privileges: ['admin', 'admin:user'], }, - { - path: '/privileges', - title: 'Privileges', - privileges: ['admin', 'admin:user:privilege'], - }, { path: '/oauth2', title: 'OAuth2', privileges: [['admin', 'admin:oauth2'], 'self:oauth2'], }, + { + path: '/privileges', + title: 'Privileges', + privileges: ['admin', 'admin:user:privilege'], + }, { path: '/documents', title: 'Documents', diff --git a/components/common/Modal/Modal/Modal.module.scss b/components/common/Modal/Modal/Modal.module.scss new file mode 100644 index 0000000..fe28cb0 --- /dev/null +++ b/components/common/Modal/Modal/Modal.module.scss @@ -0,0 +1,32 @@ +.modal { + display: flex; + flex-direction: column; + max-width: 480px; + margin: 0 auto; + background-color: #fff; + margin-top: 8%; + + .header { + padding: 1rem; + border-bottom: 1px solid #ddd; + display: flex; + justify-content: space-between; + + h1, + h2, + h3 { + margin: 0; + } + } + + .body { + padding: 1rem; + } + + .footer { + padding: 1rem; + border-top: 1px solid #ddd; + display: flex; + justify-content: flex-end; + } +} diff --git a/components/common/Modal/Modal/Modal.tsx b/components/common/Modal/Modal/Modal.tsx new file mode 100644 index 0000000..6a11321 --- /dev/null +++ b/components/common/Modal/Modal/Modal.tsx @@ -0,0 +1,5 @@ +import styles from './Modal.module.scss'; + +export default function Modal({ children }: { children: JSX.Element[] }) { + return
{children}
; +} diff --git a/components/common/Modal/ModalBody/ModalBody.tsx b/components/common/Modal/ModalBody/ModalBody.tsx new file mode 100644 index 0000000..a60f008 --- /dev/null +++ b/components/common/Modal/ModalBody/ModalBody.tsx @@ -0,0 +1,9 @@ +import styles from '../Modal/Modal.module.scss'; + +export default function ModalBody({ + children, +}: { + children: JSX.Element | []; +}) { + return
{children}
; +} diff --git a/components/common/Modal/ModalFooter/ModalFooter.tsx b/components/common/Modal/ModalFooter/ModalFooter.tsx new file mode 100644 index 0000000..a3804d2 --- /dev/null +++ b/components/common/Modal/ModalFooter/ModalFooter.tsx @@ -0,0 +1,9 @@ +import styles from '../Modal/Modal.module.scss'; + +export default function ModalFooter({ + children, +}: { + children: JSX.Element | []; +}) { + return
{children}
; +} diff --git a/components/common/Modal/ModalHeader/ModalHeader.tsx b/components/common/Modal/ModalHeader/ModalHeader.tsx new file mode 100644 index 0000000..873f447 --- /dev/null +++ b/components/common/Modal/ModalHeader/ModalHeader.tsx @@ -0,0 +1,9 @@ +import styles from '../Modal/Modal.module.scss'; + +export default function ModalHeader({ + children, +}: { + children: JSX.Element | []; +}) { + return
{children}
; +} diff --git a/components/common/Modal/ModalRoot/ModalRoot.module.scss b/components/common/Modal/ModalRoot/ModalRoot.module.scss new file mode 100644 index 0000000..3a2982e --- /dev/null +++ b/components/common/Modal/ModalRoot/ModalRoot.module.scss @@ -0,0 +1,8 @@ +.modalRoot { + position: fixed; + top: 0; + z-index: 100; + width: 100vw; + height: 100vh; + background-color: rgba(1, 1, 1, 0.5); +} diff --git a/components/common/Modal/ModalRoot/ModalRoot.tsx b/components/common/Modal/ModalRoot/ModalRoot.tsx new file mode 100644 index 0000000..4e53720 --- /dev/null +++ b/components/common/Modal/ModalRoot/ModalRoot.tsx @@ -0,0 +1,53 @@ +import { useState, useEffect } from 'react'; +import { ModalDetail } from '../../../../lib/types/modal.interface'; +import ModalService from '../services/ModalService'; +import styles from './ModalRoot.module.scss'; + +export default function ModalRoot() { + const [modal, setModal] = useState | null>(null); + + useEffect(() => { + ModalService.on('open', ({ component, props, target, resolve }) => { + setModal({ + component, + props, + close: (...args) => { + setModal(null); + resolve?.call(null, ...args); + target?.focus(); + }, + }); + }); + }, []); + + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + modal?.close?.call(null, false); + } + }; + + if (!modal) { + return; + } + + window.addEventListener('keyup', handler); + return () => { + window.removeEventListener('keyup', handler); + }; + }, [modal]); + + const ModalComponent = modal?.component ? modal.component : null; + + return ( +
+ {ModalComponent && ( + + )} +
+ ); +} diff --git a/components/common/Modal/services/ModalService.ts b/components/common/Modal/services/ModalService.ts new file mode 100644 index 0000000..751d972 --- /dev/null +++ b/components/common/Modal/services/ModalService.ts @@ -0,0 +1,25 @@ +import { ModalType, ModalDetail } from '../../../../lib/types/modal.interface'; + +const ModalService = { + on(event: string, callback: (props: ModalDetail) => void) { + document.addEventListener(event, (e: Event) => + callback((e as CustomEvent>).detail) + ); + }, + open(component: ModalType, props: any = {}) { + return new Promise((resolve, _) => { + document.dispatchEvent( + new CustomEvent>('open', { + detail: { + component, + props, + resolve, + target: (document.activeElement as HTMLElement) || undefined, + }, + }) + ); + }); + }, +}; + +export default ModalService; diff --git a/lib/types/modal.interface.ts b/lib/types/modal.interface.ts new file mode 100644 index 0000000..2780c30 --- /dev/null +++ b/lib/types/modal.interface.ts @@ -0,0 +1,13 @@ +import React from 'react'; + +export type ModalType = React.ElementType< + T & { close: (...args: any[]) => void } +>; + +export interface ModalDetail { + component: ModalType; + props: any; + target?: HTMLElement; + resolve?: (...args: any[]) => void; + close?: (...args: any[]) => void; +} diff --git a/lib/types/oauth2-client.interface.ts b/lib/types/oauth2-client.interface.ts new file mode 100644 index 0000000..65701eb --- /dev/null +++ b/lib/types/oauth2-client.interface.ts @@ -0,0 +1,37 @@ +import { Picture } from './picture.interface'; + +export enum OAuth2ClientURLType { + REDIRECT_URI = 'redirect_uri', + TERMS = 'terms', + PRIVACY = 'privacy', + WEBSITE = 'website', +} + +export interface OAuth2ClientURL { + id: number; + url: string; + type: OAuth2ClientURLType; + created_at: Date; + updated_at: Date; +} + +export interface OAuth2ClientListItem { + id: number; + client_id: string; + client_secret: string; + title: string; + description: string; + scope: string; + grants: string; + activated: boolean; + verified: boolean; + created_at: string; + updated_at: string; + urls?: OAuth2ClientURL[]; + picture?: Picture; + owner?: { + id: number; + uuid: string; + username: string; + }; +} diff --git a/lib/types/picture.interface.ts b/lib/types/picture.interface.ts new file mode 100644 index 0000000..7c3250c --- /dev/null +++ b/lib/types/picture.interface.ts @@ -0,0 +1,5 @@ +export interface Picture { + id: number; + file: number; + mimetype: string; +} diff --git a/lib/types/users.interfaces.ts b/lib/types/users.interfaces.ts index 191c1c6..b90c6d7 100644 --- a/lib/types/users.interfaces.ts +++ b/lib/types/users.interfaces.ts @@ -1,11 +1,6 @@ +import { Picture } from './picture.interface'; import { Privilege } from './privilege.interface'; -export interface UserPicture { - id: number; - file: number; - mimetype: string; -} - export interface UserListItem { id: number; uuid: string; @@ -16,6 +11,6 @@ export interface UserListItem { updated_at: string; activity_at: string; activated: boolean; - picture?: UserPicture; + picture?: Picture; privileges?: Privilege[]; } diff --git a/pages/_app.tsx b/pages/_app.tsx index fa9bf14..7f7c4c2 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -2,6 +2,7 @@ import '../styles/globals.scss'; import type { AppProps } from 'next/app'; import { SWRConfig } from 'swr'; import fetchJson from '../lib/utils/swr-fetcher'; +import ModalRoot from '../components/common/Modal/ModalRoot/ModalRoot'; function MyApp({ Component, pageProps }: AppProps) { return ( @@ -14,6 +15,7 @@ function MyApp({ Component, pageProps }: AppProps) { }} > + ); } diff --git a/pages/api/login.ts b/pages/api/login.ts index 1b0e59d..c424908 100644 --- a/pages/api/login.ts +++ b/pages/api/login.ts @@ -18,6 +18,7 @@ export default function handler(req: NextApiRequest, res: NextApiResponse) { state: stateToken, }) ); + const params = new URLSearchParams({ client_id: CLIENT_ID, response_type: 'code', diff --git a/pages/oauth2.tsx b/pages/oauth2.tsx new file mode 100644 index 0000000..3c358fb --- /dev/null +++ b/pages/oauth2.tsx @@ -0,0 +1,3 @@ +import { OAuth2Page } from '../components/OAuth2Page/OAuth2Page'; + +export default OAuth2Page; diff --git a/public/application.png b/public/application.png new file mode 100644 index 0000000000000000000000000000000000000000..6ad5ce633b6045241648eb142ded0a295a7fde5a GIT binary patch literal 14817 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 literal 0 HcmV?d00001 diff --git a/styles/_breakpoint.scss b/styles/_breakpoint.scss new file mode 100644 index 0000000..2a43723 --- /dev/null +++ b/styles/_breakpoint.scss @@ -0,0 +1,28 @@ +$breakpoints: ( + xxs: 480px, + xs: 720px, + sm: 1024px, + md: 1280px, + lg: 1440px, + xl: 1920px, + xxl: 2580px, +); + +@mixin break-on($breakpoint, $type) { + @if map-has-key($breakpoints, $breakpoint) { + $mediaValue: map-get($breakpoints, $breakpoint); + @if $type == up { + @media (min-width: $mediaValue) { + @content; + } + } @else if ($type == down) { + @media (max-width: $mediaValue) { + @content; + } + } @else { + @warn "Unknown `#{$type}` in $media query type"; + } + } @else { + @warn "Unknown `#{$breakpoint}` in $breakpoints"; + } +} diff --git a/styles/globals.scss b/styles/globals.scss index 2ceac67..0e7092e 100644 --- a/styles/globals.scss +++ b/styles/globals.scss @@ -1,3 +1,5 @@ +@import 'breakpoint'; + html, body { padding: 0; @@ -16,6 +18,14 @@ a { box-sizing: border-box; } +h1, +h2, +h3, +h4, +h5 { + margin-top: 0; +} + @media (prefers-color-scheme: dark) { html { color-scheme: dark;