initial stash

This commit is contained in:
Evert Prants 2022-08-29 21:09:28 +03:00
parent 2f021a0082
commit 0ed1a3072d
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
46 changed files with 8373 additions and 1414 deletions

View File

@ -1,3 +1,6 @@
{
"extends": "next/core-web-vitals"
"extends": [
"next/core-web-vitals",
"prettier"
]
}

4
.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"semi": true,
"singleQuote": true
}

9
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,9 @@
{
"editor.formatOnSave": true,
"files.insertFinalNewline": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"eslint.validate": ["typescript", "typescriptreact", "html"],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}

View File

@ -0,0 +1,10 @@
import Link from 'next/link';
export const LoginPage = () => {
return (
<>
<h1>Log in</h1>
<Link href={'/api/login'}>Log in with Icy Network</Link>
</>
);
};

View File

@ -0,0 +1,29 @@
.userCard {
display: flex;
flex-direction: row;
gap: 1rem;
padding: 0.5rem;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.25);
.pictureWrapper {
overflow: hidden;
}
.userInfo {
display: flex;
flex-direction: column;
h2 {
padding: 0;
margin: 0;
.username {
font-size: 0.875rem;
}
}
}
}
.userList {
display: flex;
flex-direction: column;
gap: 1rem;
}

View File

@ -0,0 +1,92 @@
import styles from './UsersPage.module.scss';
import { useState } from 'react';
import useSWR from 'swr';
import useUser from '../../lib/hooks/useUser';
import { PaginatedResponse } from '../../lib/types/paginated-response.interface';
import { UserListItem } from '../../lib/types/users.interfaces';
import { Container } from '../common/Container/Container';
import { Header } from '../common/Header/Header';
import avatar from '../../public/avatar.png';
import Image from 'next/image';
import { UPLOADS_URL } from '../../lib/constants';
import { Paginator } from '../common/Paginator/Paginator';
const UserCard = ({ user }: { user: UserListItem }) => (
<div className={styles.userCard}>
<div className={styles.pictureWrapper}>
{user.picture ? (
<Image
src={`${UPLOADS_URL}/${user.picture.file}`}
width={128}
height={128}
alt=""
/>
) : (
<Image src={avatar} alt="" width={128} height={128} />
)}
</div>
<div className={styles.userInfo}>
<h2>
{user.display_name}{' '}
<span className={styles.username}>@{user.username}</span>
</h2>
<dl>
<dt>UUID</dt>
<dd>{user.uuid}</dd>
<dt>Email</dt>
<dd>{user.email}</dd>
<dt>Activated</dt>
<dd>{user.activated ? 'Yes' : <b>NOT ACTIVATED</b>}</dd>
<dt>Registered</dt>
<dd>{new Date(user.created_at).toDateString()}</dd>
</dl>
</div>
</div>
);
const UserList = ({
pageIndex,
searchTerm,
setPage,
}: {
pageIndex: number;
searchTerm: string;
setPage: (page: number) => void;
}) => {
const { data } = useSWR<PaginatedResponse<UserListItem>>(
`/api/admin/users?page=${pageIndex}${searchTerm ? `&q=${searchTerm}` : ''}`
);
return data ? (
<>
<div className={styles.userList}>
{data.list.map((user) => (
<UserCard user={user} key={user.uuid} />
))}
<Paginator setPage={setPage} pagination={data.pagination}></Paginator>
</div>
</>
) : (
<span>Nothing found</span>
);
};
export const UsersPage = () => {
const { user } = useUser({ redirectTo: '/login' });
const [pageIndex, setPageIndex] = useState(1);
const [searchTerm, setSearchTerm] = useState('');
return (
<>
<Header user={user}></Header>
<Container>
<h1>Users</h1>
<UserList
pageIndex={pageIndex}
searchTerm={searchTerm}
setPage={setPageIndex}
/>
</Container>
</>
);
};

View File

@ -0,0 +1,5 @@
.container {
max-width: 1080px;
margin: 0 auto;
padding: 1rem;
}

View File

@ -0,0 +1,5 @@
import styles from './Container.module.scss';
export const Container = ({ children }: { children: React.ReactNode }) => {
return <main className={styles.container}>{children}</main>;
};

View File

@ -0,0 +1,35 @@
.nav {
display: flex;
background-color: #2eb9ff;
border-bottom: 4px solid #00aaff;
.inner {
display: flex;
max-width: 1080px;
width: 100%;
margin: 0 auto;
ul {
list-style: none;
display: flex;
flex-direction: row;
padding: 0;
margin: 0;
&.alignRight {
margin-left: auto;
}
li {
display: flex;
&.active {
font-weight: bold;
}
a {
padding: 1rem;
}
}
}
}
}

View File

@ -0,0 +1,91 @@
import styles from './Header.module.scss';
import { CurrentUserDto } from '../../../lib/types/user-response.interface';
import { useEffect, useState } from 'react';
import userHasPrivileges from '../../../lib/utils/has-privileges';
import Link from 'next/link';
import { useRouter } from 'next/router';
import conditionalClass from '../../../lib/utils/conditional-class';
const navItems = [
{
title: 'Home',
path: '/',
},
{
path: '/users',
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: '/documents',
title: 'Documents',
privileges: ['admin:document'],
},
];
const useAvailableNavigation = (user?: CurrentUserDto) => {
const [stateList, setStateList] = useState(
Array.from({ length: navItems.length }, (_, i) => i === 0)
);
useEffect(() => {
if (!user) {
return;
}
let newList: boolean[] = [];
navItems.forEach((item, index) => {
newList[index] = item.privileges
? userHasPrivileges(user, ...item.privileges)
: true;
});
setStateList(newList);
}, [user]);
return stateList;
};
export const Header = ({ user }: { user?: CurrentUserDto }) => {
const router = useRouter();
const privileges = useAvailableNavigation(user);
return (
<nav className={styles.nav}>
<div className={styles.inner}>
<ul>
{navItems
.filter((_, index) => privileges[index])
.map((item) => (
<li
key={item.title}
className={conditionalClass(
router.pathname === item.path,
styles.active
)}
>
<Link href={item.path}>{item.title}</Link>
</li>
))}
</ul>
<ul className={styles.alignRight}>
<li>
{user ? (
<Link href={'/api/logout'}>Log out</Link>
) : (
<Link href={'/api/login'}>Log in</Link>
)}
</li>
</ul>
</div>
</nav>
);
};

View File

@ -0,0 +1,22 @@
.paginator {
display: flex;
margin: 0 auto;
.pageButton {
appearance: none;
padding: 0.5rem 0.75rem;
border: 1px solid #ddd;
&.previous {
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
border-right: 0;
}
&.next {
border-top-right-radius: 8px;
border-bottom-right-radius: 8px;
border-left: 0;
}
}
}

View File

@ -0,0 +1,46 @@
import styles from './Paginator.module.scss';
import { Pagination } from '../../../lib/types/pagination.interface';
import conditionalClass from '../../../lib/utils/conditional-class';
export const Paginator = ({
pagination,
setPage,
}: {
pagination: Pagination;
setPage: (page: number) => void;
}) => {
// TODO: smarter page numbers
return (
<div className={styles.paginator}>
<button
className={[styles.pageButton, styles.previous].join(' ')}
onClick={() => setPage(pagination.page - 1)}
disabled={pagination.page <= 1}
>
Prev
</button>
{Array.from({ length: pagination.pageCount }, (_, i) => i + 1).map(
(pageNum) => (
<button
key={pageNum}
onClick={() => setPage(pageNum)}
disabled={pageNum === pagination.page}
className={[
styles.pageButton,
conditionalClass(pageNum === pagination.page, styles.current),
].join(' ')}
>
{pageNum}
</button>
)
)}
<button
className={[styles.pageButton, styles.next].join(' ')}
onClick={() => setPage(pagination.page + 1)}
disabled={pagination.page + 1 >= pagination.pageCount}
>
Next
</button>
</div>
);
};

8
lib/api/remote.ts Normal file
View File

@ -0,0 +1,8 @@
import { API_URL } from '../constants';
import { CurrentUserDto } from '../types/user-response.interface';
import { fetchBearer, fetchToken } from '../utils/fetch';
export const getUserInfo = async (accessToken: string) =>
await fetchBearer<CurrentUserDto>(accessToken, `${API_URL}/user`);
export const getAccessToken = async (code: string) => await fetchToken(code);

8
lib/constants.ts Normal file
View File

@ -0,0 +1,8 @@
export const PUBLIC_URL = process.env.NEXT_PUBLIC_URL as string;
export const SERVER_URL = process.env.SERVER_URL as string;
export const API_URL = `${SERVER_URL}/api`;
export const OAUTH_URL = `${SERVER_URL}/oauth2`;
export const CLIENT_ID = process.env.CLIENT_ID as string;
export const CLIENT_SECRET = process.env.CLIENT_SECRET as string;
export const COOKIE_KEYS = [process.env.COOKIE_KEY] as string[];
export const UPLOADS_URL = process.env.NEXT_PUBLIC_UPLOADS_URL as string;

View File

@ -0,0 +1,16 @@
import { useEffect, useState } from 'react';
import { CurrentUserDto } from '../types/user-response.interface';
import userHasPrivileges from '../utils/has-privileges';
export default function useHasPrivileges(
user: CurrentUserDto | undefined,
...privileges: (string | string[])[]
) {
const [hasPrivileges, setHasPrivileges] = useState(false);
useEffect(() => {
setHasPrivileges(userHasPrivileges(user, ...privileges));
}, [user, privileges]);
return hasPrivileges;
}

31
lib/hooks/useUser.ts Normal file
View File

@ -0,0 +1,31 @@
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import useSWR from 'swr';
import { CurrentUserDto } from '../types/user-response.interface';
export default function useUser({
redirectTo = '',
redirectIfFound = false,
} = {}) {
const router = useRouter();
const {
data: user,
mutate: mutateUser,
error,
} = useSWR<CurrentUserDto>('/api/user');
useEffect(() => {
if (!redirectTo) return;
if (
// If redirectTo is set, redirect if the user was not found.
(redirectTo && !redirectIfFound && !user && error) ||
// If redirectIfFound is also set, redirect if the user was found
(redirectIfFound && user && !error)
) {
router.push(redirectTo);
}
}, [user, error, router, redirectIfFound, redirectTo]);
return { user, mutateUser };
}

24
lib/session.ts Normal file
View File

@ -0,0 +1,24 @@
import { GetServerSidePropsContext, GetServerSidePropsResult } from 'next';
import { getUserInfo } from './api/remote';
import { CurrentUserDto } from './types/user-response.interface';
import { getActiveCookie } from './utils/get-active-cookie';
function withUser(
handler: (
ctx: GetServerSidePropsContext,
user?: CurrentUserDto
) => Promise<GetServerSidePropsResult<any>>
) {
return async (ctx: GetServerSidePropsContext) => {
const accessToken = getActiveCookie(ctx, 'authorization');
let user: CurrentUserDto | undefined;
if (accessToken) {
try {
user = await getUserInfo(accessToken);
} catch (e) {}
}
return handler(ctx, user);
};
}
export default withUser;

View File

@ -0,0 +1,6 @@
import { Pagination } from './pagination.interface';
export interface PaginatedResponse<T> {
list: T[];
pagination: Pagination;
}

View File

@ -0,0 +1,6 @@
export interface Pagination {
page: number;
pageCount: number;
pageSize: number;
rowCount: number;
}

View File

@ -0,0 +1,4 @@
export interface Privilege {
id: number;
name: string;
}

View File

@ -0,0 +1,5 @@
export interface OAuth2TokenDto {
access_token: string;
refresh_token?: string;
expires_in: number;
}

View File

@ -0,0 +1,14 @@
export interface CurrentUserDto {
id: number;
uuid: string;
username: string;
display_name: string;
name: string;
preferred_username: string;
nickname: string;
email: string;
email_verified: boolean;
image?: string;
image_file?: string;
privileges?: string[];
}

View File

@ -0,0 +1,21 @@
import { Privilege } from './privilege.interface';
export interface UserPicture {
id: number;
file: number;
mimetype: string;
}
export interface UserListItem {
id: number;
uuid: string;
username: string;
display_name: string;
email: string;
created_at: string;
updated_at: string;
activity_at: string;
activated: boolean;
picture?: UserPicture;
privileges?: Privilege[];
}

View File

@ -0,0 +1,6 @@
export default function conditionalClass(
condition: boolean,
className: string
): string | undefined {
return condition ? className : undefined;
}

32
lib/utils/fetch.ts Normal file
View File

@ -0,0 +1,32 @@
import { API_URL, CLIENT_ID, CLIENT_SECRET, OAUTH_URL } from '../constants';
import { OAuth2TokenDto } from '../types/token-response.interface';
export async function fetchBearer<T>(
accessToken: string,
url: string,
...options: any[]
): Promise<T> {
return fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
...options,
}).then((res) => res.json());
}
export async function fetchToken(code: string): Promise<OAuth2TokenDto> {
return fetch(`${OAUTH_URL}/token`, {
headers: {
Authorization: `Basic ${Buffer.from(
`${CLIENT_ID}:${CLIENT_SECRET}`
).toString('base64')}`,
'Content-Type': 'application/json',
},
method: 'POST',
body: JSON.stringify({
grant_type: 'authorization_code',
code,
}),
}).then((res) => res.json());
}

View File

@ -0,0 +1,17 @@
import { GetServerSidePropsContext } from 'next';
import { getCookie } from './get-cookie';
export const getActiveCookie = (
ctx: GetServerSidePropsContext,
cookieName: string
): string | undefined => {
const setCookies = ctx.res.getHeader('set-cookie') as string[];
const fallback = ctx.req.cookies[cookieName];
const entry = setCookies?.find((item) => item.startsWith(cookieName));
if (entry === 'undefined') {
return;
}
return getCookie(cookieName, entry) || fallback;
};

14
lib/utils/get-cookie.ts Normal file
View File

@ -0,0 +1,14 @@
export function getCookie(
cname: string,
cookie: string | undefined
): string | undefined {
if (!cookie) {
return undefined;
}
const name = cname + '=';
return decodeURIComponent(cookie)
.split(';')
.find((entry) => entry.startsWith(name))
?.substring(name.length);
}

View File

@ -0,0 +1,28 @@
import { CurrentUserDto } from '../types/user-response.interface';
export default function userHasPrivileges(
user: CurrentUserDto | undefined,
...privileges: (string | string[])[]
): boolean {
if (!user) {
return false;
}
const userPrivileges = user?.privileges || [];
const withOrLogic = privileges.some((entry) => Array.isArray(entry));
if (withOrLogic) {
return privileges.some((entry) => {
if (Array.isArray(entry)) {
return entry.every((item) =>
userPrivileges.find((name) => name === item)
);
} else {
return userPrivileges.find((name) => name === entry);
}
});
}
return privileges.every((item) =>
userPrivileges.find((name) => name === item)
);
}

52
lib/utils/swr-fetcher.ts Normal file
View File

@ -0,0 +1,52 @@
export default async function fetchJson<JSON = unknown>(
input: RequestInfo,
init?: RequestInit
): Promise<JSON> {
const response = await fetch(input, { ...init, credentials: 'include' });
// if the server replies, there's always some data in json
// if there's a network error, it will throw at the previous line
const data = await response.json();
// response.ok is true when res.status is 2xx
// https://developer.mozilla.org/en-US/docs/Web/API/Response/ok
if (response.ok) {
return data;
}
throw new FetchError({
message: response.statusText,
response,
data,
});
}
export class FetchError extends Error {
response: Response;
data: {
message: string;
};
constructor({
message,
response,
data,
}: {
message: string;
response: Response;
data: {
message: string;
};
}) {
// Pass remaining arguments (including vendor specific ones) to parent constructor
super(message);
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, FetchError);
}
this.name = 'FetchError';
this.response = response;
this.data = data ?? { message: message };
}
}

View File

@ -2,6 +2,9 @@
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
}
images: {
domains: ['localhost', '127.0.0.1', 'icynet.eu'],
},
};
module.exports = nextConfig
module.exports = nextConfig;

6053
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -9,16 +9,24 @@
"lint": "next lint"
},
"dependencies": {
"cookies": "^0.8.0",
"http-proxy": "^1.18.1",
"next": "12.2.5",
"react": "18.2.0",
"react-dom": "18.2.0"
"react-dom": "18.2.0",
"swr": "^1.3.0"
},
"devDependencies": {
"@types/cookies": "^0.7.7",
"@types/http-proxy": "^1.17.9",
"@types/node": "18.7.13",
"@types/react": "18.0.17",
"@types/react-dom": "18.0.6",
"eslint": "8.23.0",
"eslint-config-next": "12.2.5",
"eslint-config-prettier": "^8.5.0",
"prettier": "^2.7.1",
"sass": "^1.54.5",
"typescript": "4.8.2"
}
}

View File

@ -1,8 +1,21 @@
import '../styles/globals.css'
import type { AppProps } from 'next/app'
import '../styles/globals.scss';
import type { AppProps } from 'next/app';
import { SWRConfig } from 'swr';
import fetchJson from '../lib/utils/swr-fetcher';
function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
return (
<SWRConfig
value={{
fetcher: fetchJson,
onError: (err) => {
console.error(err);
},
}}
>
<Component {...pageProps} />
</SWRConfig>
);
}
export default MyApp
export default MyApp;

13
pages/_document.tsx Normal file
View File

@ -0,0 +1,13 @@
import { Head, Html, Main, NextScript } from 'next/document';
// The stylesheet is for Univia-pro font from Adobe
export default function Document() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}

39
pages/api/[...path].ts Normal file
View File

@ -0,0 +1,39 @@
import { proxy } from '../../server/proxy';
import Cookies from 'cookies';
import { NextApiRequest, NextApiResponse } from 'next';
import { COOKIE_KEYS } from '../../lib/constants';
const handler = (req: NextApiRequest, res: NextApiResponse) => {
return new Promise((resolve, reject) => {
// removes the api prefix from url
// req.url = req.url!.replace(/^\/api/, '');
const cookies = new Cookies(req, res, { keys: COOKIE_KEYS });
const authorization = cookies.get('authorization', { signed: true });
// don't forwards the cookies to the target server
req.headers.cookie = '';
if (authorization) {
req.headers.authorization = `Bearer ${authorization}`;
}
/**
* if an error occurs in the proxy, we will reject the promise.
* it is so important. if you don't reject the promise,
* you're facing the stalled requests issue.
*/
proxy.once('error', reject);
proxy.web(req, res);
});
};
export default handler;
export const config = {
api: {
bodyParser: false,
},
};

24
pages/api/callback.ts Normal file
View File

@ -0,0 +1,24 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { getAccessToken } from '../../lib/api/remote';
import Cookies from 'cookies';
import { COOKIE_KEYS } from '../../lib/constants';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.query.code) {
// TODO: parse state
const getAuth = await getAccessToken(req.query.code as string);
const cookies = new Cookies(req, res, { keys: COOKIE_KEYS });
if (getAuth) {
cookies.set('authorization', getAuth.access_token, {
expires: new Date(Date.now() + getAuth.expires_in * 1000),
secure: process.env.NODE_ENV === 'production',
signed: true,
});
}
res.redirect('/');
}
}

15
pages/api/login.ts Normal file
View File

@ -0,0 +1,15 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { CLIENT_ID, OAUTH_URL, PUBLIC_URL } from '../../lib/constants';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const params = new URLSearchParams({
client_id: CLIENT_ID,
response_type: 'code',
redirect_uri: `${PUBLIC_URL}/api/callback`,
scope: 'management',
});
// TODO: generate state
res.redirect(`${OAUTH_URL}/authorize?${params.toString()}`);
}

16
pages/api/logout.ts Normal file
View File

@ -0,0 +1,16 @@
import { NextApiRequest, NextApiResponse } from 'next';
import Cookies from 'cookies';
import { COOKIE_KEYS } from '../../lib/constants';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const cookies = new Cookies(req, res, { keys: COOKIE_KEYS });
cookies.set('authorization', undefined, {
expires: new Date(0),
signed: true,
});
res.redirect('/');
}

View File

@ -1,72 +1,16 @@
import type { NextPage } from 'next'
import Head from 'next/head'
import Image from 'next/image'
import styles from '../styles/Home.module.css'
import type { NextPage } from 'next';
import { Container } from '../components/common/Container/Container';
import { Header } from '../components/common/Header/Header';
import useUser from '../lib/hooks/useUser';
const Home: NextPage = () => {
const { user } = useUser({ redirectTo: '/login' });
return (
<div className={styles.container}>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<>
<Header user={user}></Header>
<Container>Welcome back, {user?.display_name}!</Container>
</>
);
};
<main className={styles.main}>
<h1 className={styles.title}>
Welcome to <a href="https://nextjs.org">Next.js!</a>
</h1>
<p className={styles.description}>
Get started by editing{' '}
<code className={styles.code}>pages/index.tsx</code>
</p>
<div className={styles.grid}>
<a href="https://nextjs.org/docs" className={styles.card}>
<h2>Documentation &rarr;</h2>
<p>Find in-depth information about Next.js features and API.</p>
</a>
<a href="https://nextjs.org/learn" className={styles.card}>
<h2>Learn &rarr;</h2>
<p>Learn about Next.js in an interactive course with quizzes!</p>
</a>
<a
href="https://github.com/vercel/next.js/tree/canary/examples"
className={styles.card}
>
<h2>Examples &rarr;</h2>
<p>Discover and deploy boilerplate example Next.js projects.</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
>
<h2>Deploy &rarr;</h2>
<p>
Instantly deploy your Next.js site to a public URL with Vercel.
</p>
</a>
</div>
</main>
<footer className={styles.footer}>
<a
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Powered by{' '}
<span className={styles.logo}>
<Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
</span>
</a>
</footer>
</div>
)
}
export default Home
export default Home;

19
pages/login.tsx Normal file
View File

@ -0,0 +1,19 @@
import withUser from '../lib/session';
import { LoginPage } from '../components/LoginPage/LoginPage';
export default LoginPage;
export const getServerSideProps = withUser(async (ctx, user) => {
if (user) {
return {
redirect: {
destination: '/',
permanent: false,
},
};
}
return {
props: {},
};
});

3
pages/users.tsx Normal file
View File

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

BIN
public/avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

10
server/proxy.ts Normal file
View File

@ -0,0 +1,10 @@
import httpProxy from 'http-proxy';
export const proxy = httpProxy.createProxyServer({
/**
* Get the actual back-end service url from env variables.
* We shouldn't prefix the env variable with NEXT_PUBLIC_* to avoid exposing it to the client.
*/
target: process.env.SERVER_URL,
autoRewrite: false,
});

2832
yarn.lock

File diff suppressed because it is too large Load Diff