oauth2 view start, simple modal

This commit is contained in:
Evert Prants 2022-08-30 21:08:54 +03:00
parent 4588e1c1bf
commit 5c9094390f
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
23 changed files with 447 additions and 16 deletions

View File

@ -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;
}
}
}

View File

@ -1,10 +1,15 @@
import Link from 'next/link'; import Link from 'next/link';
import styles from './LoginPage.module.scss';
export const LoginPage = () => { export const LoginPage = () => {
return ( return (
<> <main className={styles.wrapper}>
<h1>Log in</h1> <div className={styles.loginBox}>
<Link href={'/api/login'}>Log in with Icy Network</Link> <h1>Icy Network Administration</h1>
</> <Link href={'/api/login'}>
<a className={styles.loginButton}>Log in with Icy Network</a>
</Link>
</div>
</main>
); );
}; };

View File

@ -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;
}

View File

@ -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 (
<Modal>
<ModalHeader>
<h3>Test!</h3>
</ModalHeader>
<ModalBody>
<p>This is only a test</p>
</ModalBody>
<ModalFooter>
<button onClick={() => close(true)}>Close</button>
</ModalFooter>
</Modal>
);
};
const OAuth2ClientCard = ({ client }: { client: OAuth2ClientListItem }) => (
<div className={styles.clientCard}>
<div className={styles.pictureWrapper}>
{client.picture ? (
<Image
src={`${UPLOADS_URL}/${client.picture.file}`}
width={128}
height={128}
alt=""
/>
) : (
<Image src={application} alt="" width={128} height={128} />
)}
</div>
<div className={styles.clientInfo}>
<h2>{client.title}</h2>
<span className={styles.clientDescription}>{client.description}</span>
<dl>
<dt>Client ID</dt>
<dd>{client.client_id}</dd>
<dt>Allowed scopes</dt>
<dd>{client.scope?.split(' ').join(', ')}</dd>
<dt>Allowed grant types</dt>
<dd>{client.grants?.split(' ').join(', ')}</dd>
<dt>Activated</dt>
<dd>{client.activated ? 'Yes' : <b>NOT ACTIVATED</b>}</dd>
<dt>Verified</dt>
<dd>{client.verified ? 'Yes' : 'No'}</dd>
<dt>Created</dt>
<dd>{new Date(client.created_at).toDateString()}</dd>
</dl>
</div>
</div>
);
const OAuth2ClientList = ({
pageIndex,
searchTerm,
setPage,
}: {
pageIndex: number;
searchTerm: string;
setPage: (page: number) => void;
}) => {
const { data } = useSWR<PaginatedResponse<OAuth2ClientListItem>>(
`/api/admin/oauth2/clients?page=${pageIndex}${
searchTerm ? `&q=${searchTerm}` : ''
}`
);
return data ? (
<>
<button onClick={() => ModalService.open(TestModal)}>test</button>
<div className={styles.clientList}>
{data.list.map((client) => (
<OAuth2ClientCard client={client} key={client.client_id} />
))}
{data?.pagination && (
<Paginator setPage={setPage} pagination={data.pagination}></Paginator>
)}
</div>
</>
) : (
<span>Nothing found</span>
);
};
export const OAuth2Page = () => {
const { user } = useUser({ redirectTo: '/login' });
const [pageIndex, setPageIndex] = useState(1);
const [searchTerm, setSearchTerm] = useState('');
return (
<>
<Header user={user}></Header>
<Container>
<h1>OAuth2 clients</h1>
<OAuth2ClientList
pageIndex={pageIndex}
searchTerm={searchTerm}
setPage={setPageIndex}
/>
</Container>
</>
);
};

View File

@ -16,16 +16,16 @@ const navItems = [
title: 'Users', title: 'Users',
privileges: ['admin', 'admin:user'], privileges: ['admin', 'admin:user'],
}, },
{
path: '/privileges',
title: 'Privileges',
privileges: ['admin', 'admin:user:privilege'],
},
{ {
path: '/oauth2', path: '/oauth2',
title: 'OAuth2', title: 'OAuth2',
privileges: [['admin', 'admin:oauth2'], 'self:oauth2'], privileges: [['admin', 'admin:oauth2'], 'self:oauth2'],
}, },
{
path: '/privileges',
title: 'Privileges',
privileges: ['admin', 'admin:user:privilege'],
},
{ {
path: '/documents', path: '/documents',
title: 'Documents', title: 'Documents',

View File

@ -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;
}
}

View File

@ -0,0 +1,5 @@
import styles from './Modal.module.scss';
export default function Modal({ children }: { children: JSX.Element[] }) {
return <div className={styles.modal}>{children}</div>;
}

View File

@ -0,0 +1,9 @@
import styles from '../Modal/Modal.module.scss';
export default function ModalBody({
children,
}: {
children: JSX.Element | [];
}) {
return <div className={styles.body}>{children}</div>;
}

View File

@ -0,0 +1,9 @@
import styles from '../Modal/Modal.module.scss';
export default function ModalFooter({
children,
}: {
children: JSX.Element | [];
}) {
return <div className={styles.footer}>{children}</div>;
}

View File

@ -0,0 +1,9 @@
import styles from '../Modal/Modal.module.scss';
export default function ModalHeader({
children,
}: {
children: JSX.Element | [];
}) {
return <div className={styles.header}>{children}</div>;
}

View File

@ -0,0 +1,8 @@
.modalRoot {
position: fixed;
top: 0;
z-index: 100;
width: 100vw;
height: 100vh;
background-color: rgba(1, 1, 1, 0.5);
}

View File

@ -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<ModalDetail<unknown> | 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 (
<section className={modal?.component ? styles.modalRoot : ''}>
{ModalComponent && (
<ModalComponent
{...modal?.props}
close={modal?.close}
className={ModalComponent ? 'd-block' : ''}
/>
)}
</section>
);
}

View File

@ -0,0 +1,25 @@
import { ModalType, ModalDetail } from '../../../../lib/types/modal.interface';
const ModalService = {
on(event: string, callback: (props: ModalDetail<unknown>) => void) {
document.addEventListener(event, (e: Event) =>
callback((e as CustomEvent<ModalDetail<unknown>>).detail)
);
},
open<T>(component: ModalType<T>, props: any = {}) {
return new Promise((resolve, _) => {
document.dispatchEvent(
new CustomEvent<ModalDetail<T>>('open', {
detail: {
component,
props,
resolve,
target: (document.activeElement as HTMLElement) || undefined,
},
})
);
});
},
};
export default ModalService;

View File

@ -0,0 +1,13 @@
import React from 'react';
export type ModalType<T> = React.ElementType<
T & { close: (...args: any[]) => void }
>;
export interface ModalDetail<T> {
component: ModalType<T>;
props: any;
target?: HTMLElement;
resolve?: (...args: any[]) => void;
close?: (...args: any[]) => void;
}

View File

@ -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;
};
}

View File

@ -0,0 +1,5 @@
export interface Picture {
id: number;
file: number;
mimetype: string;
}

View File

@ -1,11 +1,6 @@
import { Picture } from './picture.interface';
import { Privilege } from './privilege.interface'; import { Privilege } from './privilege.interface';
export interface UserPicture {
id: number;
file: number;
mimetype: string;
}
export interface UserListItem { export interface UserListItem {
id: number; id: number;
uuid: string; uuid: string;
@ -16,6 +11,6 @@ export interface UserListItem {
updated_at: string; updated_at: string;
activity_at: string; activity_at: string;
activated: boolean; activated: boolean;
picture?: UserPicture; picture?: Picture;
privileges?: Privilege[]; privileges?: Privilege[];
} }

View File

@ -2,6 +2,7 @@ import '../styles/globals.scss';
import type { AppProps } from 'next/app'; import type { AppProps } from 'next/app';
import { SWRConfig } from 'swr'; import { SWRConfig } from 'swr';
import fetchJson from '../lib/utils/swr-fetcher'; import fetchJson from '../lib/utils/swr-fetcher';
import ModalRoot from '../components/common/Modal/ModalRoot/ModalRoot';
function MyApp({ Component, pageProps }: AppProps) { function MyApp({ Component, pageProps }: AppProps) {
return ( return (
@ -14,6 +15,7 @@ function MyApp({ Component, pageProps }: AppProps) {
}} }}
> >
<Component {...pageProps} /> <Component {...pageProps} />
<ModalRoot />
</SWRConfig> </SWRConfig>
); );
} }

View File

@ -18,6 +18,7 @@ export default function handler(req: NextApiRequest, res: NextApiResponse) {
state: stateToken, state: stateToken,
}) })
); );
const params = new URLSearchParams({ const params = new URLSearchParams({
client_id: CLIENT_ID, client_id: CLIENT_ID,
response_type: 'code', response_type: 'code',

3
pages/oauth2.tsx Normal file
View File

@ -0,0 +1,3 @@
import { OAuth2Page } from '../components/OAuth2Page/OAuth2Page';
export default OAuth2Page;

BIN
public/application.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

28
styles/_breakpoint.scss Normal file
View File

@ -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";
}
}

View File

@ -1,3 +1,5 @@
@import 'breakpoint';
html, html,
body { body {
padding: 0; padding: 0;
@ -16,6 +18,14 @@ a {
box-sizing: border-box; box-sizing: border-box;
} }
h1,
h2,
h3,
h4,
h5 {
margin-top: 0;
}
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
html { html {
color-scheme: dark; color-scheme: dark;