@snokam/auth 2.3.2 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +650 -0
  2. package/package.json +11 -14
  3. package/src/app-router/components/Login.tsx +2 -2
  4. package/src/app-router/components/Provider/index.tsx +1 -1
  5. package/src/app-router/index.tsx +4 -4
  6. package/src/app-router/server.ts +16 -64
  7. package/src/components/Authenticate.tsx +2 -2
  8. package/src/components/Consent/index.tsx +1 -1
  9. package/src/components/Error/index.tsx +1 -1
  10. package/src/components/index.ts +5 -0
  11. package/src/config.ts +1 -1
  12. package/src/handoff/handlers.ts +6 -3
  13. package/src/handoff/return-to.ts +1 -1
  14. package/src/handoff/ticket.ts +7 -69
  15. package/src/impersonation/cookies.ts +1 -1
  16. package/src/impersonation/handlers.ts +2 -2
  17. package/src/impersonation/routes.ts +1 -1
  18. package/src/impersonation/ticket.ts +6 -51
  19. package/src/index.ts +11 -0
  20. package/src/{build-proxy.tsx → middleware.tsx} +3 -4
  21. package/src/options/callbacks.ts +99 -0
  22. package/src/options/cookies.ts +13 -0
  23. package/src/options/employee-profile.ts +90 -0
  24. package/src/options/index.tsx +19 -0
  25. package/src/options/providers.ts +117 -0
  26. package/src/options/request.ts +42 -0
  27. package/src/{route-handler.ts → route-handler/index.ts} +7 -7
  28. package/src/route-handler/token.ts +3 -5
  29. package/src/scopes/index.ts +8 -0
  30. package/src/scopes/matcher.ts +4 -1
  31. package/src/session/index.ts +3 -0
  32. package/src/theme/employee-theme-sync.tsx +1 -1
  33. package/src/ticket/seal.ts +95 -0
  34. package/src/index.tsx +0 -31
  35. package/src/options.tsx +0 -389
  36. package/src/page-router/components/Login.tsx +0 -50
  37. package/src/page-router/components/Provider/index.tsx +0 -45
  38. package/src/page-router/index.tsx +0 -22
  39. package/src/page-router/server.ts +0 -6
  40. package/src/server/auth.tsx +0 -30
  41. package/src/server/handler.tsx +0 -68
  42. package/src/server.tsx +0 -16
  43. /package/src/{server → app-router}/token.tsx +0 -0
  44. /package/src/{utils/consent.ts → consent.ts} +0 -0
  45. /package/src/{utils/cors.ts → cors.ts} +0 -0
  46. /package/src/{utils → options}/params.ts +0 -0
  47. /package/src/{utils → session}/callback-url.ts +0 -0
  48. /package/src/{utils → session}/cookies.ts +0 -0
  49. /package/src/{utils → session}/url.ts +0 -0
  50. /package/src/{utils/types.ts → types.ts} +0 -0
package/src/options.tsx DELETED
@@ -1,389 +0,0 @@
1
- import NextAuth, { NextAuthOptions } from "next-auth";
2
- import AzureADProvider from "next-auth/providers/azure-ad";
3
- import CredentialsProvider from "next-auth/providers/credentials";
4
- import { getDefaultAuthParams } from "./utils/params";
5
- import { ConfidentialClientApplication } from "@azure/msal-node";
6
- import { Configuration, EmployeesClient } from "@snokam/employees-client";
7
- import { headers } from "next/headers";
8
- import {
9
- getTenantDomains,
10
- getTenantDomainsByEntraTenantId,
11
- type TenantDomain,
12
- } from "@snokam/config";
13
- import { getManagedBearer } from "@snokam/identity";
14
- import { exchangePasswordToken } from "./password/api";
15
- import { exchangeSsoToken } from "./sso/exchange";
16
- import { getSanityImageUrl } from "@snokam/ui/media/sanity";
17
- import { getSessionCookieSettings } from "./utils/cookies";
18
- import { getAuthUrl } from "./utils/url";
19
- import { openHandoff } from "./handoff/ticket";
20
- import { passwordLogin, redeemBootstrapToken } from "./password/api";
21
- import { Auth } from "./utils/types";
22
-
23
- async function resolveEmployeesBackend(
24
- config: Auth.Config
25
- ): Promise<{ base: string; scope: string }> {
26
- const scope = config.clientId
27
- ? `api://${config.clientId}/user_impersonation`
28
- : "";
29
- const requestHeaders = await headers();
30
- const host =
31
- requestHeaders.get("x-forwarded-host") ?? requestHeaders.get("host");
32
- const platformApiBase = process.env.NEXT_PUBLIC_PLATFORM_API_BASE_URL ?? "";
33
- const authToken = await getManagedBearer(scope);
34
- const employeesBackend = (domains: TenantDomain[]) =>
35
- domains.find(
36
- (domain) => domain.type === "backend" && domain.app === "employees"
37
- );
38
-
39
- let match = employeesBackend(
40
- await getTenantDomains({ host, platformApiBase, authToken })
41
- );
42
- if (!match?.url && config.tenantId) {
43
- match = employeesBackend(
44
- await getTenantDomainsByEntraTenantId({
45
- entraTenantId: config.tenantId,
46
- platformApiBase,
47
- authToken,
48
- })
49
- );
50
- }
51
- if (match?.url) return { base: match.url, scope };
52
-
53
- const bare = host?.split(":")[0];
54
- const effective =
55
- !bare || bare === "localhost" || bare === "127.0.0.1"
56
- ? process.env.NEXT_PUBLIC_TENANT_HOST?.split(":")[0]
57
- : host;
58
- return { base: effective ? `https://${effective}/api/employees` : "", scope };
59
- }
60
-
61
- type ColorScheme = "light" | "dark" | "system";
62
-
63
- function isColorScheme(value: unknown): value is ColorScheme {
64
- return value === "light" || value === "dark" || value === "system";
65
- }
66
-
67
- function requestHost(req?: {
68
- headers?: Record<string, string | string[] | undefined>;
69
- }): string | undefined {
70
- const header = req?.headers?.["x-forwarded-host"] ?? req?.headers?.host;
71
- const value = Array.isArray(header) ? header[0] : header;
72
- return value?.split(",")[0].trim().split(":")[0] || undefined;
73
- }
74
-
75
- function requestClientIp(req?: {
76
- headers?: Record<string, string | string[] | undefined>;
77
- }): string | undefined {
78
- const pick = (name: string): string | undefined => {
79
- const header = req?.headers?.[name];
80
- const value = Array.isArray(header) ? header[0] : header;
81
- return value?.split(",")[0].trim() || undefined;
82
- };
83
- // Trusted edge headers first (Front Door, unspoofable); rightmost XFF entry as a fallback.
84
- const edge = pick("x-azure-clientip") ?? pick("x-azure-socketip");
85
- if (edge) return edge;
86
- const forwarded = req?.headers?.["x-forwarded-for"];
87
- const value = Array.isArray(forwarded) ? forwarded[0] : forwarded;
88
- return value?.split(",").pop()?.trim() || undefined;
89
- }
90
-
91
- async function requestOrigin(): Promise<string | undefined> {
92
- const h = await headers();
93
- const host = (h.get("x-forwarded-host") ?? h.get("host") ?? "")
94
- .split(",")[0]
95
- .trim();
96
- if (!host) return undefined;
97
- const scheme =
98
- h.get("x-forwarded-proto") ??
99
- (host.startsWith("localhost") ? "http" : "https");
100
- return `${scheme}://${host}`;
101
- }
102
-
103
- interface EmployeeProfile {
104
- colorScheme?: ColorScheme;
105
- imageUrl?: string;
106
- }
107
-
108
- async function fetchEmployeeProfile(
109
- config: Auth.Config,
110
- refreshToken?: string,
111
- authMethod?: string
112
- ): Promise<EmployeeProfile> {
113
- if (!refreshToken) {
114
- return {};
115
- }
116
- try {
117
- const { base, scope } = await resolveEmployeesBackend(config);
118
- if (!base || !scope) return {};
119
-
120
- let accessToken: string | undefined;
121
- if (authMethod === "password") {
122
- accessToken = (await exchangePasswordToken(refreshToken))?.accessToken;
123
- } else {
124
- const entra = (
125
- await new ConfidentialClientApplication({
126
- auth: {
127
- clientId: config.clientId,
128
- clientSecret: config.clientSecret,
129
- authority: `https://login.microsoftonline.com/${config.tenantId}`,
130
- },
131
- }).acquireTokenByRefreshToken({ scopes: [scope], refreshToken })
132
- )?.accessToken;
133
- accessToken = entra
134
- ? (await exchangeSsoToken(entra))?.accessToken
135
- : undefined;
136
- }
137
-
138
- if (!accessToken) return {};
139
- const client = new EmployeesClient(
140
- new Configuration({
141
- basePath: base,
142
- accessToken: `Bearer ${accessToken}`,
143
- })
144
- );
145
- const me = await client.getMeProtected();
146
- const profile = {
147
- colorScheme: isColorScheme(me.settings?.colorScheme)
148
- ? me.settings.colorScheme
149
- : undefined,
150
- imageUrl: getSanityImageUrl(me.image?.asset?.url, {
151
- width: 128,
152
- height: 128,
153
- fit: "crop",
154
- }),
155
- };
156
- if (!profile.imageUrl) {
157
- console.warn("[auth] employee profile resolved without an image");
158
- }
159
- return profile;
160
- } catch (error) {
161
- console.warn("[auth] employee profile fetch failed", error);
162
- return {};
163
- }
164
- }
165
-
166
- export const getNextAuthOptions = (config: Auth.Config): NextAuthOptions => {
167
- const authUrl = getAuthUrl();
168
- const { name, domain, secure } = getSessionCookieSettings(authUrl);
169
-
170
- return {
171
- secret: config.nextAuthSecret,
172
- session: {
173
- maxAge: 7 * 24 * 60 * 60,
174
- updateAge: 24 * 60 * 60,
175
- },
176
- pages: {
177
- signIn: "/auth/login",
178
- },
179
- providers: [
180
- AzureADProvider({
181
- clientId: config.clientId,
182
- clientSecret: config.clientSecret,
183
- tenantId: config.tenantId,
184
- authorization: {
185
- params: getDefaultAuthParams(config.tenantId),
186
- },
187
- checks: ["pkce", "state"],
188
- profile(profile) {
189
- return {
190
- id: profile.sub,
191
- name: profile.name,
192
- email: profile.email,
193
- image: null,
194
- };
195
- },
196
- }),
197
- CredentialsProvider({
198
- id: "handoff",
199
- name: "Snokam",
200
- credentials: { ticket: { label: "ticket", type: "text" } },
201
- async authorize(credentials, req) {
202
- const expectedBusiness = requestHost(req);
203
- const claims =
204
- credentials?.ticket && expectedBusiness
205
- ? openHandoff(
206
- credentials.ticket,
207
- config.nextAuthSecret,
208
- expectedBusiness
209
- )
210
- : null;
211
- if (!claims) {
212
- return null;
213
- }
214
- return {
215
- id: claims.oid,
216
- email: claims.email,
217
- name: claims.name,
218
- image: claims.image ?? null,
219
- oid: claims.oid,
220
- tid: claims.tid,
221
- refreshToken: claims.refreshToken,
222
- authMethod: claims.authMethod || undefined,
223
- } as unknown as { id: string };
224
- },
225
- }),
226
- CredentialsProvider({
227
- id: "password",
228
- name: "Passord",
229
- credentials: {
230
- email: { label: "email", type: "text" },
231
- password: { label: "password", type: "password" },
232
- code: { label: "code", type: "text" },
233
- host: { label: "host", type: "text" },
234
- bootstrap: { label: "bootstrap", type: "text" },
235
- },
236
- async authorize(credentials, req) {
237
- if (credentials?.bootstrap) {
238
- const redeemed = await redeemBootstrapToken(credentials.bootstrap);
239
- if (!redeemed) {
240
- return null;
241
- }
242
- return {
243
- id: redeemed.userId,
244
- email: redeemed.email,
245
- name: redeemed.name,
246
- oid: redeemed.userId,
247
- tid: redeemed.businessId,
248
- refreshToken: redeemed.refreshToken,
249
- authMethod: "password",
250
- } as unknown as { id: string };
251
- }
252
-
253
- if (
254
- !credentials?.email ||
255
- !credentials.password ||
256
- !credentials.host
257
- ) {
258
- return null;
259
- }
260
-
261
- const outcome = await passwordLogin(
262
- {
263
- host: credentials.host,
264
- email: credentials.email,
265
- password: credentials.password,
266
- code: credentials.code || undefined,
267
- },
268
- requestClientIp(req)
269
- );
270
- if (!outcome.login) {
271
- throw new Error(outcome.error ?? "invalid_credentials");
272
- }
273
-
274
- return {
275
- id: outcome.login.userId,
276
- email: outcome.login.email,
277
- name: outcome.login.name,
278
- oid: outcome.login.userId,
279
- tid: outcome.login.businessId,
280
- refreshToken: outcome.login.refreshToken,
281
- authMethod: "password",
282
- } as unknown as { id: string };
283
- },
284
- }),
285
- ],
286
- cookies: {
287
- sessionToken: {
288
- name,
289
- options: {
290
- path: "/",
291
- httpOnly: true,
292
- sameSite: "lax",
293
- secure,
294
- domain,
295
- },
296
- },
297
- },
298
- callbacks: {
299
- async redirect({ url, baseUrl }) {
300
- const origin = (await requestOrigin()) ?? baseUrl;
301
- const { pathname, search, hash } = new URL(url, origin);
302
- return origin + pathname + search + hash;
303
- },
304
- async jwt({ token, account, profile, user, trigger, session }) {
305
- if (account?.access_token) {
306
- token.refreshToken = account.refresh_token;
307
- token.expiresAt = account.expires_at;
308
- const claims = profile as { oid?: string; tid?: string } | undefined;
309
- if (claims?.oid) token.oid = claims.oid;
310
- if (claims?.tid) token.tid = claims.tid;
311
- const employee = await fetchEmployeeProfile(
312
- { ...config, tenantId: claims?.tid ?? config.tenantId },
313
- account.refresh_token as string | undefined
314
- );
315
- if (employee.colorScheme) token.colorScheme = employee.colorScheme;
316
- token.picture = employee.imageUrl ?? null;
317
- }
318
-
319
- const handoff = user as
320
- | {
321
- oid?: string;
322
- tid?: string;
323
- refreshToken?: string;
324
- authMethod?: string;
325
- }
326
- | undefined;
327
- if (handoff?.refreshToken) {
328
- token.oid = handoff.oid;
329
- token.tid = handoff.tid;
330
- token.refreshToken = handoff.refreshToken;
331
- if (handoff.authMethod) {
332
- token.authMethod = handoff.authMethod;
333
- }
334
- const employee = await fetchEmployeeProfile(
335
- { ...config, tenantId: handoff.tid ?? config.tenantId },
336
- handoff.refreshToken,
337
- handoff.authMethod
338
- );
339
- if (employee.colorScheme) token.colorScheme = employee.colorScheme;
340
- if (employee.imageUrl) token.picture = employee.imageUrl;
341
- }
342
-
343
- if (trigger === "update") {
344
- if (isColorScheme(session?.colorScheme)) {
345
- token.colorScheme = session.colorScheme;
346
- }
347
- if (session?.refreshProfile && token.refreshToken) {
348
- const employee = await fetchEmployeeProfile(
349
- {
350
- ...config,
351
- tenantId: (token.tid as string | undefined) ?? config.tenantId,
352
- },
353
- token.refreshToken as string,
354
- token.authMethod as string | undefined
355
- );
356
- if (employee.imageUrl) token.picture = employee.imageUrl;
357
- }
358
- }
359
-
360
- return token;
361
- },
362
- async session({ session, token }) {
363
- const claims = token as {
364
- colorScheme?: unknown;
365
- oid?: string;
366
- tid?: string;
367
- authMethod?: string;
368
- };
369
- const target = session as {
370
- colorScheme?: ColorScheme;
371
- oid?: string;
372
- tid?: string;
373
- authMethod?: string;
374
- };
375
- if (isColorScheme(claims.colorScheme)) {
376
- target.colorScheme = claims.colorScheme;
377
- }
378
- target.oid = claims.oid;
379
- target.tid = claims.tid;
380
- if (claims.authMethod) {
381
- target.authMethod = claims.authMethod;
382
- }
383
- return session;
384
- },
385
- },
386
- };
387
- };
388
-
389
- export default (config: Auth.Config) => NextAuth(getNextAuthOptions(config));
@@ -1,50 +0,0 @@
1
- import { useEffect, useState, useRef } from "react";
2
- import { useRouter } from "next/router";
3
- import { useAuth } from "../../hooks";
4
- import { signIn } from "next-auth/react";
5
- import { PageLoader } from "@snokam/ui";
6
- import Error from "../../components/Error";
7
-
8
- export const Login = () => {
9
- const { isAuthenticated } = useAuth();
10
- const [callbackUrl, setCallbackUrl] = useState<string | null>(null);
11
- const [authParams, setAuthParams] = useState<Record<string, string>>({});
12
- const [isError, setIsError] = useState<boolean>(false);
13
- const hasSignedIn = useRef(false);
14
- const router = useRouter();
15
-
16
- useEffect(() => {
17
- const params = new URLSearchParams(window.location.search);
18
- setCallbackUrl(params.get("callbackUrl"));
19
- setIsError(!!params.get("error"));
20
-
21
- const authParamsObj: Record<string, string> = {};
22
- params.forEach((value, key) => {
23
- if (key !== "error" && key !== "callbackUrl") {
24
- authParamsObj[key] = value;
25
- }
26
- });
27
- setAuthParams(authParamsObj);
28
- }, []);
29
-
30
- useEffect(() => {
31
- if (!callbackUrl) return;
32
- if (isError) return;
33
- if (hasSignedIn.current) return;
34
-
35
- if (isAuthenticated) {
36
- router.replace(callbackUrl || "/");
37
- } else {
38
- hasSignedIn.current = true;
39
- signIn("azure-ad", { callbackUrl }, authParams);
40
- }
41
- }, [isAuthenticated, callbackUrl, authParams, isError, router, signIn]);
42
-
43
- if (isError) {
44
- return <Error />;
45
- }
46
-
47
- return <PageLoader />;
48
- };
49
-
50
- export default Login;
@@ -1,45 +0,0 @@
1
- import { memo, ReactNode, useEffect } from "react";
2
- import { useRouter } from "next/router";
3
- import { Auth } from "../../../utils/types";
4
- import { Authenticate } from "../../../components/Authenticate";
5
- import { SessionProvider } from "next-auth/react";
6
- import { useAuth } from "../../../hooks";
7
-
8
- export const Provider = memo((props: Auth.Provider.Props) => {
9
- const { session, children, enforceLogin } = props;
10
- const router = useRouter();
11
- const query = router?.query;
12
-
13
- if (query?.enforceLogin || enforceLogin) {
14
- return (
15
- <SessionProvider session={session}>
16
- <Authenticate provider="azure-ad" {...props}>
17
- {children}
18
- </Authenticate>
19
- </SessionProvider>
20
- );
21
- }
22
-
23
- return (
24
- <SessionProvider session={session}>
25
- <Wrapper>{children}</Wrapper>
26
- </SessionProvider>
27
- );
28
- });
29
-
30
- export const Wrapper = ({ children }: { children: ReactNode }) => {
31
- const { signOut } = useAuth();
32
- const router = useRouter();
33
-
34
- useEffect(() => {
35
- if (router?.query?.logout === "true") {
36
- signOut({
37
- callbackUrl: "/auth/login?callbackUrl=/",
38
- });
39
- }
40
- }, [router?.query?.logout]);
41
-
42
- return <>{children}</>;
43
- };
44
-
45
- export default Provider;
@@ -1,22 +0,0 @@
1
- "use client";
2
-
3
- import { Provider } from "./components/Provider";
4
- import { Authenticate } from "../components/Authenticate";
5
- import { Login } from "./components/Login";
6
- import { Error } from "../components/Error";
7
-
8
- export { Unauthenticated } from "../components/Unauthenticated";
9
- export { Authenticated } from "../components/Authenticated";
10
- export { useAuth } from "../hooks";
11
- export { signIn, SessionProvider } from "next-auth/react";
12
- export { Login, Error, Provider, Authenticate };
13
-
14
- export default {
15
- Login,
16
- Error,
17
- Provider,
18
- Authenticate,
19
- };
20
-
21
- export type { Session } from "next-auth";
22
- export type { Auth } from "../utils/types";
@@ -1,6 +0,0 @@
1
- export { getServerToken } from "../server/token";
2
- export { getServerSession } from "next-auth/next";
3
- export { getNextAuthOptions } from "../options";
4
- export { getAuth } from "../server/auth";
5
-
6
- export type { Auth } from "../utils/types";
@@ -1,30 +0,0 @@
1
- import { GetServerSidePropsContext, GetStaticPropsContext } from "next";
2
- import { getServerSession as nextAuthGetServerSession } from "next-auth";
3
- import { Auth } from "../utils/types";
4
- import { getNextAuthOptions } from "../options";
5
-
6
- export const getAuth = async (
7
- context: GetServerSidePropsContext | GetStaticPropsContext,
8
- authConfig: Auth.Config
9
- ) => {
10
- if (!("req" in context && "res" in context)) {
11
- return {
12
- isAuthenticated: false,
13
- session: null,
14
- user: null,
15
- };
16
- }
17
-
18
- const options = getNextAuthOptions(authConfig);
19
- const session = await nextAuthGetServerSession(
20
- context.req,
21
- context.res,
22
- options
23
- );
24
-
25
- return {
26
- isAuthenticated: !!session?.user,
27
- user: session?.user || null,
28
- session,
29
- };
30
- };
@@ -1,68 +0,0 @@
1
- import { getToken } from "next-auth/jwt";
2
- import { ConfidentialClientApplication } from "@azure/msal-node";
3
- import type { NextApiRequest, NextApiResponse } from "next";
4
- import { getServerSession } from "next-auth";
5
- import { Auth } from "../utils/types";
6
- import { getNextAuthOptions } from "../options";
7
- import { exchangeSsoToken } from "../sso/exchange";
8
-
9
- export interface Props {
10
- req: NextApiRequest;
11
- res: NextApiResponse<{ accessToken: string; expiresOn: number } | string>;
12
- authConfig: Auth.Config;
13
- }
14
-
15
- export default async function handler(props: Props) {
16
- const { req, res, authConfig } = props;
17
- const options = getNextAuthOptions(authConfig);
18
- const session = await getServerSession(req, res, options);
19
-
20
- if (!session?.user) {
21
- return res.status(401).end("Not authenticated");
22
- }
23
-
24
- const scope = process.env.IDENTITY_AD_SCOPE;
25
- if (!scope) {
26
- return res.status(500).end("IDENTITY_AD_SCOPE is not configured");
27
- }
28
-
29
- try {
30
- const { nextAuthSecret } = authConfig;
31
- const refetchTokenResult = await getToken({
32
- req,
33
- secret: nextAuthSecret,
34
- cookieName: options.cookies?.sessionToken?.name,
35
- });
36
-
37
- if (!refetchTokenResult) {
38
- return res.status(500).end("Failed to get refetch token");
39
- }
40
-
41
- const cca = new ConfidentialClientApplication({
42
- auth: {
43
- clientId: authConfig.clientId,
44
- clientSecret: authConfig.clientSecret,
45
- authority: `https://login.microsoftonline.com/${authConfig.tenantId}`,
46
- },
47
- });
48
-
49
- const accessTokenResult = await cca.acquireTokenByRefreshToken({
50
- scopes: [scope],
51
- refreshToken: refetchTokenResult?.refreshToken as string,
52
- });
53
-
54
- if (!accessTokenResult) {
55
- return res.status(500).end("Failed to get access token");
56
- }
57
-
58
- const exchanged = await exchangeSsoToken(accessTokenResult.accessToken);
59
- if (!exchanged) {
60
- return res.status(502).end("SSO token exchange failed");
61
- }
62
-
63
- return res.status(200).json(exchanged);
64
- } catch (e) {
65
- console.error("Token exchange error:", e);
66
- res.status(500).end("Token exchange failed");
67
- }
68
- }
package/src/server.tsx DELETED
@@ -1,16 +0,0 @@
1
- import { getServerToken } from "./server/token";
2
- import { default as Routes } from "./options";
3
- import { default as Token } from "./server/handler";
4
- import { Auth } from "./utils/types";
5
-
6
- export { getServerToken };
7
- export { getServerSession } from "next-auth/next";
8
- export { getNextAuthOptions } from "./options";
9
- export { getAuth } from "./server/auth";
10
-
11
- export type { Auth };
12
-
13
- export default {
14
- Routes,
15
- Token,
16
- };
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes