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
- >
+
+
+
);
};
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 0000000..6ad5ce6
Binary files /dev/null and b/public/application.png differ
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;