oauth2 view start, simple modal
This commit is contained in:
parent
4588e1c1bf
commit
5c9094390f
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,15 @@
|
||||
import Link from 'next/link';
|
||||
import styles from './LoginPage.module.scss';
|
||||
|
||||
export const LoginPage = () => {
|
||||
return (
|
||||
<>
|
||||
<h1>Log in</h1>
|
||||
<Link href={'/api/login'}>Log in with Icy Network</Link>
|
||||
</>
|
||||
<main className={styles.wrapper}>
|
||||
<div className={styles.loginBox}>
|
||||
<h1>Icy Network Administration</h1>
|
||||
<Link href={'/api/login'}>
|
||||
<a className={styles.loginButton}>Log in with Icy Network</a>
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
29
components/OAuth2Page/OAuth2Page.module.scss
Normal file
29
components/OAuth2Page/OAuth2Page.module.scss
Normal 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;
|
||||
}
|
120
components/OAuth2Page/OAuth2Page.tsx
Normal file
120
components/OAuth2Page/OAuth2Page.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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',
|
||||
|
32
components/common/Modal/Modal/Modal.module.scss
Normal file
32
components/common/Modal/Modal/Modal.module.scss
Normal 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;
|
||||
}
|
||||
}
|
5
components/common/Modal/Modal/Modal.tsx
Normal file
5
components/common/Modal/Modal/Modal.tsx
Normal 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>;
|
||||
}
|
9
components/common/Modal/ModalBody/ModalBody.tsx
Normal file
9
components/common/Modal/ModalBody/ModalBody.tsx
Normal 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>;
|
||||
}
|
9
components/common/Modal/ModalFooter/ModalFooter.tsx
Normal file
9
components/common/Modal/ModalFooter/ModalFooter.tsx
Normal 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>;
|
||||
}
|
9
components/common/Modal/ModalHeader/ModalHeader.tsx
Normal file
9
components/common/Modal/ModalHeader/ModalHeader.tsx
Normal 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>;
|
||||
}
|
8
components/common/Modal/ModalRoot/ModalRoot.module.scss
Normal file
8
components/common/Modal/ModalRoot/ModalRoot.module.scss
Normal 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);
|
||||
}
|
53
components/common/Modal/ModalRoot/ModalRoot.tsx
Normal file
53
components/common/Modal/ModalRoot/ModalRoot.tsx
Normal 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>
|
||||
);
|
||||
}
|
25
components/common/Modal/services/ModalService.ts
Normal file
25
components/common/Modal/services/ModalService.ts
Normal 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;
|
13
lib/types/modal.interface.ts
Normal file
13
lib/types/modal.interface.ts
Normal 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;
|
||||
}
|
37
lib/types/oauth2-client.interface.ts
Normal file
37
lib/types/oauth2-client.interface.ts
Normal 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;
|
||||
};
|
||||
}
|
5
lib/types/picture.interface.ts
Normal file
5
lib/types/picture.interface.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface Picture {
|
||||
id: number;
|
||||
file: number;
|
||||
mimetype: string;
|
||||
}
|
@ -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[];
|
||||
}
|
||||
|
@ -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) {
|
||||
}}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
<ModalRoot />
|
||||
</SWRConfig>
|
||||
);
|
||||
}
|
||||
|
@ -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',
|
||||
|
3
pages/oauth2.tsx
Normal file
3
pages/oauth2.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
import { OAuth2Page } from '../components/OAuth2Page/OAuth2Page';
|
||||
|
||||
export default OAuth2Page;
|
BIN
public/application.png
Normal file
BIN
public/application.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
28
styles/_breakpoint.scss
Normal file
28
styles/_breakpoint.scss
Normal 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";
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
Reference in New Issue
Block a user