create-wenuts-cli 2.2.0 → 2.3.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.
- package/package.json +1 -1
- package/template/default/eslint.config.mjs +16 -4
- package/template/default/package.json +4 -1
- package/template/default/postcss.config.mjs +12 -12
- package/template/default/src/api/user.ts +145 -145
- package/template/default/src/app/[local]/(default-layout)/layout.tsx +1 -1
- package/template/default/src/app/[local]/(default-layout)/page.tsx +1 -1
- package/template/default/src/app/[local]/(no-layout)/home/page.tsx +11 -11
- package/template/default/src/app/[local]/(no-layout)/login/components/LoginHandle/index.tsx +40 -44
- package/template/default/src/app/[local]/(no-layout)/login/page.tsx +32 -32
- package/template/default/src/app/[local]/layout.tsx +38 -38
- package/template/default/src/app/api/auth/[...nextauth]/route.ts +38 -41
- package/template/default/src/app/layout.tsx +17 -17
- package/template/default/src/components/System/ClientLayout/index.tsx +42 -42
- package/template/default/src/components/System/LoginModal/index.tsx +48 -53
- package/template/default/src/components/System/ThemeProvider/index.tsx +33 -44
- package/template/default/src/config/CookieMap.ts +1 -1
- package/template/default/src/i18n/routing.ts +8 -9
- package/template/default/src/libs/casdoor_provider.ts +31 -33
- package/template/default/src/libs/fetchCookie/serverCookies.ts +5 -5
- package/template/default/src/libs/nextauth.ts +38 -41
- package/template/default/src/middleware.ts +6 -6
- package/template/default/src/store/useUserStore.ts +35 -35
- package/template/default/src/types/payment.ts +139 -139
- package/template/default/src/types/user.ts +81 -87
package/package.json
CHANGED
|
@@ -3,15 +3,27 @@
|
|
|
3
3
|
* eslint-config-prettier: 关闭所有与 Prettier 冲突的 ESLint 格式化规则,
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
6
8
|
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
|
7
9
|
import { FlatCompat } from '@eslint/eslintrc';
|
|
10
|
+
import nextVitals from 'eslint-config-next/core-web-vitals';
|
|
8
11
|
|
|
9
|
-
const
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
|
|
15
|
+
const compat = new FlatCompat({
|
|
16
|
+
baseDirectory: __dirname,
|
|
17
|
+
});
|
|
10
18
|
|
|
11
19
|
const eslintConfig = [
|
|
12
|
-
...compat.config({
|
|
13
|
-
|
|
14
|
-
|
|
20
|
+
// ...compat.config({
|
|
21
|
+
// plugins: [],
|
|
22
|
+
// extends: ['next/core-web-vitals', 'prettier'],
|
|
23
|
+
// }),
|
|
24
|
+
// ...compat.extends('next/core-web-vitals'),
|
|
25
|
+
...nextVitals,
|
|
26
|
+
...compat.extends('prettier'),
|
|
15
27
|
{
|
|
16
28
|
rules: {
|
|
17
29
|
'react-hooks/exhaustive-deps': 'off',
|
|
@@ -37,6 +37,8 @@
|
|
|
37
37
|
"concurrently": "^9.2.1",
|
|
38
38
|
"eslint": "^9",
|
|
39
39
|
"eslint-config-next": "16.1.1",
|
|
40
|
+
"eslint-config-prettier": "^10.1.8",
|
|
41
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
40
42
|
"husky": "^9.0.11",
|
|
41
43
|
"lint-staged": "^15.2.0",
|
|
42
44
|
"postcss": "^8.5.6",
|
|
@@ -44,12 +46,13 @@
|
|
|
44
46
|
"postcss-simple-vars": "^7.0.1",
|
|
45
47
|
"prettier": "^3.7.4",
|
|
46
48
|
"prettier-eslint": "^16.4.2",
|
|
49
|
+
"prettier-plugin-tailwindcss": "^0.7.2",
|
|
47
50
|
"tailwindcss": "^4",
|
|
48
51
|
"typescript": "^5"
|
|
49
52
|
},
|
|
50
53
|
"lint-staged": {
|
|
51
54
|
"*.{js,jsx,ts,tsx}": [
|
|
52
|
-
"eslint --max-warnings=0",
|
|
55
|
+
"eslint --max-warnings=0 --fix",
|
|
53
56
|
"prettier --write"
|
|
54
57
|
],
|
|
55
58
|
"*.{json,css,scss,md}": [
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
const config = {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
plugins: {
|
|
3
|
+
'@tailwindcss/postcss': {},
|
|
4
|
+
'postcss-preset-mantine': {},
|
|
5
|
+
'postcss-simple-vars': {
|
|
6
|
+
variables: {
|
|
7
|
+
'mantine-breakpoint-xs': '36em',
|
|
8
|
+
'mantine-breakpoint-sm': '48em',
|
|
9
|
+
'mantine-breakpoint-md': '62em',
|
|
10
|
+
'mantine-breakpoint-lg': '75em',
|
|
11
|
+
'mantine-breakpoint-xl': '88em',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
13
14
|
},
|
|
14
|
-
},
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
export default config;
|
|
@@ -1,149 +1,149 @@
|
|
|
1
|
-
import fetch from
|
|
1
|
+
import fetch from '@/libs/fetch';
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} from
|
|
3
|
+
IApiKey,
|
|
4
|
+
CreateInvoiceResult,
|
|
5
|
+
FeedBackReqParams,
|
|
6
|
+
IManageSubscriptionResult,
|
|
7
|
+
INoticeResult,
|
|
8
|
+
IUpdateUserNameParams,
|
|
9
|
+
IUserDetailResp,
|
|
10
|
+
IUserLoginArgs,
|
|
11
|
+
IUserLoginState,
|
|
12
|
+
IUserProfile,
|
|
13
|
+
IChargeParams,
|
|
14
|
+
IChargeResult,
|
|
15
|
+
} from '@/types/user';
|
|
16
16
|
|
|
17
17
|
export default class UserApi {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
18
|
+
/**
|
|
19
|
+
* 登录接口
|
|
20
|
+
* @param args.srcType 登录类型 'casdoor' | 'discord' | 'facebook' | 'google' | 'googleOneTap' | 'twitter';
|
|
21
|
+
* @param args.code 测试环境和本地环境专用 casdoor 登录专用
|
|
22
|
+
* @param args.token google 登录专用, google 回调的 token.access_token
|
|
23
|
+
* @param args.credential googleOneTap 登录专用, google one tap 回调的 credential
|
|
24
|
+
*/
|
|
25
|
+
static userLogin(args: IUserLoginArgs) {
|
|
26
|
+
return fetch.post<IUserLoginState>('/user/auth', args);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 获取用户信息接口
|
|
31
|
+
*/
|
|
32
|
+
static userProfile() {
|
|
33
|
+
return fetch.get<IUserProfile>('/user/profile');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 更新用户信息
|
|
38
|
+
* @param args.avatar 头像地址,需要拼接好 domain 的全地址
|
|
39
|
+
* @param args.name 用户名
|
|
40
|
+
*/
|
|
41
|
+
static userUpdate(args: { avatar?: string; name?: string }) {
|
|
42
|
+
return fetch.post<{ avatar: string }>('/user/update', args);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 注销账户
|
|
47
|
+
*/
|
|
48
|
+
static userDelete() {
|
|
49
|
+
return fetch.post('/user/delete');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 自动分享到社区
|
|
54
|
+
* @returns
|
|
55
|
+
*/
|
|
56
|
+
static updateShareCommunity() {
|
|
57
|
+
return fetch.post('/user/update-auto-send');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 关注用户
|
|
62
|
+
* @param args followerId
|
|
63
|
+
* @returns 结果
|
|
64
|
+
*/
|
|
65
|
+
static followUser(args: { followerId: string }) {
|
|
66
|
+
return fetch.post('/user/favourite', args);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 取消关注用户
|
|
71
|
+
* @param args followerId
|
|
72
|
+
* @returns 结果
|
|
73
|
+
*/
|
|
74
|
+
static unFollowUser(args: { followerId: string }) {
|
|
75
|
+
return fetch.post('/user/unfavourite', args);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static getUserInfo(params: { userId: string }) {
|
|
79
|
+
return fetch.get<IUserDetailResp>('/user/info', params);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
static updateNsfw() {
|
|
83
|
+
return fetch.post<{
|
|
84
|
+
displayNSFW: boolean;
|
|
85
|
+
}>('/user/update-nsfw');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static noticeList(args: { page: string }) {
|
|
89
|
+
return fetch.get<INoticeResult>('/notice/list', args);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
static updateAvatar(imgPath: string) {
|
|
93
|
+
return fetch.post('/user/update-avatar', { imgPath });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
static updateUsername(params: IUpdateUserNameParams) {
|
|
97
|
+
return fetch.post('/user/update-username', params);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
static deleteUser() {
|
|
101
|
+
return fetch.post('/user/delete');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
static manageSubscription() {
|
|
105
|
+
return fetch.get<IManageSubscriptionResult>('/user/portal');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static cancelPaypalSub() {
|
|
109
|
+
return fetch.post('/payment/cancel-sub');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
static createFeedBack(args: FeedBackReqParams) {
|
|
113
|
+
return fetch.post('/feedback/create', args);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 创建API密钥
|
|
118
|
+
* @returns
|
|
119
|
+
*/
|
|
120
|
+
static createKey() {
|
|
121
|
+
return fetch.post<IApiKey>('/user/create-key');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 删除API密钥
|
|
126
|
+
* @param args.id
|
|
127
|
+
* @returns
|
|
128
|
+
*/
|
|
129
|
+
static deleteKey(args: { id: string }) {
|
|
130
|
+
return fetch.post('/user/delete-key', args);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 获取API密钥列表
|
|
135
|
+
* @returns
|
|
136
|
+
*/
|
|
137
|
+
static listKey() {
|
|
138
|
+
return fetch.get<{ keys: IApiKey[] }>('/user/list-key');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 创建支付凭证
|
|
142
|
+
static createInvoice() {
|
|
143
|
+
return fetch.post<CreateInvoiceResult>('/payment/create-invoice');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
static checkIn(args: IChargeParams) {
|
|
147
|
+
return fetch.post<IChargeResult>('/user/charge', args);
|
|
148
|
+
}
|
|
149
149
|
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
import useUserStore from
|
|
3
|
-
import { Button } from
|
|
4
|
-
import { useTranslations } from
|
|
1
|
+
'use client';
|
|
2
|
+
import useUserStore from '@/store/useUserStore';
|
|
3
|
+
import { Button } from '@mantine/core';
|
|
4
|
+
import { useTranslations } from 'next-intl';
|
|
5
5
|
|
|
6
6
|
const Page = () => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
const t = useTranslations('HomePage');
|
|
8
|
+
const openLoginModal = useUserStore(state => state.openLoginModal);
|
|
9
|
+
return (
|
|
10
|
+
<div>
|
|
11
|
+
<Button onClick={openLoginModal}>{t('title')}</Button>
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
export default Page;
|
|
@@ -1,55 +1,51 @@
|
|
|
1
|
-
|
|
2
|
-
import React, { useEffect } from
|
|
3
|
-
import { signIn } from
|
|
1
|
+
'use client';
|
|
2
|
+
import React, { useEffect } from 'react';
|
|
3
|
+
import { signIn } from 'next-auth/react';
|
|
4
4
|
|
|
5
|
-
import { Loader } from
|
|
5
|
+
import { Loader } from '@mantine/core';
|
|
6
6
|
|
|
7
7
|
interface IProps {
|
|
8
|
-
|
|
8
|
+
isLogin: boolean;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
const LoginHandle: React.FC<IProps> = ({ isLogin }) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
process.env.NEXT_PUBLIC_DOMAIN
|
|
18
|
-
);
|
|
19
|
-
window.close(); // 子窗口自己关闭,不受 COOP 限制
|
|
20
|
-
} else {
|
|
21
|
-
// 防止用户直接在浏览器打开登录页面
|
|
22
|
-
window.location.href = process.env.NEXT_PUBLIC_DOMAIN + "/";
|
|
23
|
-
}
|
|
24
|
-
} else {
|
|
25
|
-
loginHandler();
|
|
26
|
-
}
|
|
27
|
-
}, []);
|
|
12
|
+
/**
|
|
13
|
+
* 登录事件代理
|
|
14
|
+
*/
|
|
15
|
+
const loginHandler = () => {
|
|
16
|
+
const isPro = process.env.NEXT_PUBLIC_NODE_ENV !== 'dev';
|
|
28
17
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
18
|
+
if (isPro) {
|
|
19
|
+
signIn('google', {
|
|
20
|
+
callbackUrl: process.env.NEXT_PUBLIC_DOMAIN + '/login/',
|
|
21
|
+
redirect: true,
|
|
22
|
+
});
|
|
23
|
+
} else {
|
|
24
|
+
signIn('casdoor', {
|
|
25
|
+
callbackUrl: process.env.NEXT_PUBLIC_DOMAIN + '/login/',
|
|
26
|
+
redirect: true,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (isLogin) {
|
|
32
|
+
if (window.opener) {
|
|
33
|
+
window.opener.postMessage('login_success', process.env.NEXT_PUBLIC_DOMAIN);
|
|
34
|
+
window.close(); // 子窗口自己关闭,不受 COOP 限制
|
|
35
|
+
} else {
|
|
36
|
+
// 防止用户直接在浏览器打开登录页面
|
|
37
|
+
window.location.href = process.env.NEXT_PUBLIC_DOMAIN + '/';
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
loginHandler();
|
|
41
|
+
}
|
|
42
|
+
}, []);
|
|
34
43
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
} else {
|
|
41
|
-
signIn("casdoor", {
|
|
42
|
-
callbackUrl: process.env.NEXT_PUBLIC_DOMAIN + "/login/",
|
|
43
|
-
redirect: true,
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
return (
|
|
49
|
-
<div className="w-screen h-screen flex justify-center items-center">
|
|
50
|
-
<Loader type="dots" />
|
|
51
|
-
</div>
|
|
52
|
-
);
|
|
44
|
+
return (
|
|
45
|
+
<div className="flex h-screen w-screen items-center justify-center">
|
|
46
|
+
<Loader type="dots" />
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
53
49
|
};
|
|
54
50
|
|
|
55
51
|
export default LoginHandle;
|
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
import React from
|
|
2
|
-
import LoginHandle from
|
|
3
|
-
import { ScrollArea } from
|
|
4
|
-
import { cookies } from
|
|
5
|
-
import CookieMap from
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import LoginHandle from './components/LoginHandle';
|
|
3
|
+
import { ScrollArea } from '@mantine/core';
|
|
4
|
+
import { cookies } from 'next/headers';
|
|
5
|
+
import CookieMap from '@/config/CookieMap';
|
|
6
6
|
|
|
7
|
-
import style from
|
|
7
|
+
import style from './style.module.css';
|
|
8
8
|
|
|
9
9
|
interface IProps {
|
|
10
|
-
|
|
10
|
+
searchParams: { redirect: string };
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const LoginPage: React.FC<IProps> = async ({ searchParams }) => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
14
|
+
const cookieStore = await cookies();
|
|
15
|
+
|
|
16
|
+
const userState = cookieStore.get(CookieMap.UserState);
|
|
17
|
+
|
|
18
|
+
let isLogin = false;
|
|
19
|
+
|
|
20
|
+
if (userState) {
|
|
21
|
+
isLogin = true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<ScrollArea
|
|
26
|
+
className="h-screen w-screen"
|
|
27
|
+
type="never"
|
|
28
|
+
classNames={{
|
|
29
|
+
viewport: `${style.custom_scroll}`,
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
<div className="h-full w-full bg-white p-3 md:p-6">
|
|
33
|
+
<div className="flex flex-col-reverse items-center gap-6 md:grid md:h-full md:gap-12">
|
|
34
|
+
<LoginHandle isLogin={isLogin} />
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</ScrollArea>
|
|
38
|
+
);
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
export default LoginPage;
|