Compare commits
8 Commits
1938e95948
...
030fc1a808
Author | SHA1 | Date | |
---|---|---|---|
![]() |
030fc1a808 | ||
![]() |
05c69481b5 | ||
![]() |
0fcc194860 | ||
![]() |
56f0f30ffb | ||
![]() |
0aefbfaeae | ||
![]() |
628c72190b | ||
![]() |
886a314df7 | ||
![]() |
efc2d31f7c |
@@ -2,7 +2,7 @@
|
||||
tags: cms, login-flow
|
||||
---
|
||||
|
||||
# login flow
|
||||
# CMS login flow
|
||||
|
||||
## description
|
||||
|
||||
|
37
001_documentation/Requirements/REQ0020/index.md
Normal file
37
001_documentation/Requirements/REQ0020/index.md
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
tags: mobile, login-flow
|
||||
---
|
||||
|
||||
# Mobile login flow
|
||||
|
||||
## description
|
||||
|
||||
```mermaid
|
||||
graph TD;
|
||||
Start-->A;
|
||||
A-->B;
|
||||
B-->C;
|
||||
B-->D;
|
||||
D-->E;
|
||||
E-->F;
|
||||
C-->G;
|
||||
G-->A
|
||||
|
||||
F-->End;
|
||||
|
||||
A[greeting, asking username and password]
|
||||
B[check if username and password is valid]
|
||||
C[pasword failed]
|
||||
D[pasword ok]
|
||||
E[login success]
|
||||
F[redirect to '/dashboard']
|
||||
|
||||
G[prompt user wrong username and password]
|
||||
|
||||
Start((start));
|
||||
End((end))
|
||||
```
|
||||
|
||||
### relations
|
||||
|
||||
[REQ0016](../REQ0016/index.md)
|
@@ -17,7 +17,8 @@
|
||||
- [REQ0013: cms dashboard](./REQ0013/index.md)
|
||||
- [REQ0014: mobile client](./REQ0014/index.md)
|
||||
- [REQ0015: pocketbase json schema to dbml converter](./REQ0015/index.md)
|
||||
- [REQ0016: login flow](./REQ0016/index.md)
|
||||
- [REQ0016: CMS login flow](./REQ0016/index.md)
|
||||
- [REQ0017: lesson page documentation](./REQ0017/index.md)
|
||||
- [REQ0018: family photo of frameworks](./REQ0018/index.md)
|
||||
- [REQ0019: System architecture](./REQ0019/index.md)
|
||||
- [REQ0020: Mobile login flow](./REQ0020/index.md)
|
||||
|
@@ -1,3 +1,4 @@
|
||||
# TODO
|
||||
|
||||
- [ ] add login mechanism
|
||||
- [ ] add task server handle callback tasks
|
||||
|
@@ -1,9 +1,14 @@
|
||||
const Paths = {
|
||||
AuthHome: `/auth/Home`,
|
||||
AuthHome: `/auth/home`,
|
||||
AuthLogin: `/auth/login`,
|
||||
AuthSignUp: `/auth/signup`,
|
||||
SignUpSuccess: `/auth/sign_up_success`,
|
||||
AuthorizedTest: `/auth/authorized_test`,
|
||||
//
|
||||
StudentInfo: `/auth/student_info/:id`,
|
||||
GetStudentInfoLink: (id: string) => `/auth/student_info/${id}`,
|
||||
//
|
||||
Setting: `/setting`,
|
||||
};
|
||||
|
||||
export { Paths };
|
||||
|
@@ -45,14 +45,16 @@ import Page from './pages/Page';
|
||||
import QuizzesMainMenu from './pages/QuizzesMainMenu';
|
||||
//
|
||||
import MyAchievementPage from './pages/Record/index';
|
||||
import Setting from './pages/Setting/indx';
|
||||
import Setting from './pages/Setting';
|
||||
import Tab1 from './pages/Tab1';
|
||||
import Tab2 from './pages/Tab2';
|
||||
import Tab3 from './pages/Tab3';
|
||||
import { Paths } from './Paths';
|
||||
import SignUpSuccess from './pages/auth/SignUpSuccess';
|
||||
import AuthorizedTest from './pages/auth/AuthorizedTest';
|
||||
import { AuthGuard } from './pages/auth/AuthorizedTest/auth-guard';
|
||||
import { AuthGuard } from './components/auth/auth-guard';
|
||||
import StudentInfo from './pages/auth/StudentInfo';
|
||||
// import { AuthGuard } from './pages/auth/AuthorizedTest/auth-guard';
|
||||
// import WordPageWithLayout from './pages/Lesson/WordPageWithLayout.del';
|
||||
|
||||
function RouteConfig() {
|
||||
@@ -180,11 +182,15 @@ function RouteConfig() {
|
||||
<SignUpSuccess />
|
||||
</Route>
|
||||
|
||||
<Route exact path={Paths.AuthorizedTest}>
|
||||
<AuthGuard>
|
||||
{/* protected page */}
|
||||
<AuthGuard>
|
||||
<Route exact path={Paths.AuthorizedTest}>
|
||||
<AuthorizedTest />
|
||||
</AuthGuard>
|
||||
</Route>
|
||||
</Route>
|
||||
<Route exact path={Paths.StudentInfo}>
|
||||
<StudentInfo />
|
||||
</Route>
|
||||
</AuthGuard>
|
||||
|
||||
{/* TODO: remove below */}
|
||||
<Route exact path="/tab1">
|
||||
@@ -196,7 +202,7 @@ function RouteConfig() {
|
||||
<Route path="/tab3">
|
||||
<Tab3 />
|
||||
</Route>
|
||||
<Route path="/setting">
|
||||
<Route path={Paths.Setting}>
|
||||
<Setting />
|
||||
</Route>
|
||||
<Route path="/page/:name" exact={true}>
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { useIonRouter } from '@ionic/react';
|
||||
import * as React from 'react';
|
||||
import { IonAlert, IonButton } from '@ionic/react';
|
||||
import { useUser } from '../../../hooks/use-user';
|
||||
import { Paths } from '../../../Paths';
|
||||
import { useUser } from '../../hooks/use-user';
|
||||
import { Paths } from '../../Paths';
|
||||
|
||||
export interface AuthGuardProps {
|
||||
children: React.ReactNode;
|
@@ -4,5 +4,8 @@ import { COL_USER_METAS } from '../../constants';
|
||||
import { pb } from '../../lib/pb';
|
||||
|
||||
export async function getUserMetaById(id: string): Promise<RecordModel> {
|
||||
return pb.collection(COL_USER_METAS).getOne(id);
|
||||
return pb.collection(COL_USER_METAS).getOne(id, {
|
||||
expand: 'billingAddress',
|
||||
requestKey: null,
|
||||
});
|
||||
}
|
||||
|
@@ -1,5 +1,19 @@
|
||||
import type { BillingAddress } from '@/components/dashboard/user_meta/type.d';
|
||||
|
||||
// DBUserMeta type definitions
|
||||
export interface DBUserMeta {
|
||||
name: string;
|
||||
avatar: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
quota: number;
|
||||
status: 'active' | 'blocked' | 'pending';
|
||||
//
|
||||
collectionId: string;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
// UserMeta type definitions
|
||||
export interface UserMeta {
|
||||
id: string;
|
||||
|
5
002_source/ionic_mobile/src/lib/getStudentAvatar.tsx
Normal file
5
002_source/ionic_mobile/src/lib/getStudentAvatar.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { DBUserMeta } from '../db/UserMetas/type';
|
||||
|
||||
export function getStudentAvatar(studentMeta: DBUserMeta) {
|
||||
return `url(http://localhost:8090/api/files/${studentMeta.collectionId}/${studentMeta.id}/${studentMeta.avatar})`;
|
||||
}
|
@@ -35,7 +35,7 @@ const LessonContainer: React.FC<ContainerProps> = ({ lesson_type_id: lesson_type
|
||||
|
||||
if (loading) return <LoadingScreen />;
|
||||
if (!selected_content) return <LoadingScreen />;
|
||||
if (selected_content.length == 0) return <>loading</>;
|
||||
if (selected_content.length == 0) return <LoadingScreen />;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@@ -22,12 +22,25 @@ import _ from 'lodash';
|
||||
import { Router, useParams } from 'react-router';
|
||||
import { Wave } from '../../../components/Wave';
|
||||
import { Paths } from '../../../Paths';
|
||||
import { useTransition } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useUser } from '../../../hooks/use-user';
|
||||
|
||||
function AuthorizedTest(): React.JSX.Element {
|
||||
const router = useIonRouter();
|
||||
const { t } = useTranslation();
|
||||
const { user } = useUser();
|
||||
|
||||
function handleBackToLogin() {
|
||||
router.push(Paths.AuthLogin);
|
||||
}
|
||||
|
||||
function handleViewStudentInfoOnClick() {
|
||||
if (user?.id) {
|
||||
router.push(Paths.GetStudentInfoLink(user.id));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage className={styles.loginPage}>
|
||||
<IonHeader>{/* */}</IonHeader>
|
||||
@@ -36,6 +49,12 @@ function AuthorizedTest(): React.JSX.Element {
|
||||
<IonGrid className="ion-padding">
|
||||
<IonCol>
|
||||
<IonRow>Authorized page test</IonRow>
|
||||
{JSON.stringify({ user })}
|
||||
{/* */}
|
||||
<IonRow>
|
||||
<IonButton onClick={handleViewStudentInfoOnClick}>{t('view-student-info')}</IonButton>
|
||||
</IonRow>
|
||||
{/* */}
|
||||
<IonRow>
|
||||
<IonButton onClick={handleBackToLogin}>Back to login</IonButton>
|
||||
</IonRow>
|
||||
|
107
002_source/ionic_mobile/src/pages/auth/Home/index copy.tsx
Normal file
107
002_source/ionic_mobile/src/pages/auth/Home/index copy.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonCardTitle,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonFooter,
|
||||
IonGrid,
|
||||
IonHeader,
|
||||
IonImg,
|
||||
IonPage,
|
||||
IonRouterLink,
|
||||
IonRow,
|
||||
IonToolbar,
|
||||
useIonRouter,
|
||||
} from '@ionic/react';
|
||||
// import { Action } from '../components/Action';
|
||||
import styles from './style.module.scss';
|
||||
import { Action } from '../../../components/Action';
|
||||
import { Paths } from '../../../Paths';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useUser } from '../../../hooks/use-user';
|
||||
import { LoadingScreen } from '../../../components/LoadingScreen';
|
||||
|
||||
const AuthHome = () => {
|
||||
const { t } = useTranslation();
|
||||
const { user, checkSession, isLoading } = useUser();
|
||||
const router = useIonRouter();
|
||||
|
||||
const [showLoading, setShowLoading] = useState<boolean>(true);
|
||||
const [showError, setShowErrr] = useState<{ show: boolean; message: string }>({
|
||||
show: false,
|
||||
message: '',
|
||||
});
|
||||
|
||||
const [checkingSession, setCheckingSession] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!checkingSession) {
|
||||
if (!user) {
|
||||
router.push(Paths.AuthLogin);
|
||||
} else {
|
||||
router.push(Paths.AuthorizedTest);
|
||||
}
|
||||
setShowLoading(false);
|
||||
}
|
||||
}, [user, checkingSession]);
|
||||
|
||||
useEffect(() => {
|
||||
checkSession?.()
|
||||
.then(() => {
|
||||
setCheckingSession(false);
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
}, [checkSession]);
|
||||
|
||||
if (showLoading) return <LoadingScreen />;
|
||||
// if (showError) return <>{showError.message}</>;
|
||||
|
||||
return (
|
||||
<IonPage className={'styles.homePage'}>
|
||||
<IonHeader>
|
||||
{/* <IonToolbar className="ion-no-margin ion-no-padding"> */}
|
||||
<IonImg src="/assets/login2.jpeg" />
|
||||
{/* </IonToolbar> */}
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<div className={styles.getStarted}>
|
||||
<IonGrid>
|
||||
<IonRow className={`ion-text-center ion-justify-content-center ${styles.heading}`}>
|
||||
<IonCol size="11" className={styles.headingText}>
|
||||
<IonCardTitle>
|
||||
{/* */}
|
||||
Join millions of other people discovering their creative side
|
||||
</IonCardTitle>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<IonRow className={`ion-text-center ion-justify-content-center`}>
|
||||
<IonRouterLink routerLink={Paths.AuthSignUp} className="custom-link">
|
||||
<IonCol size="11">
|
||||
<IonButton className={`${styles.getStartedButton} custom-button`}>
|
||||
{/* */}
|
||||
Get started →
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
</IonRouterLink>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</div>
|
||||
</IonContent>
|
||||
|
||||
<IonFooter>
|
||||
<IonGrid style={{ marginBottom: '1rem' }}>
|
||||
<Action
|
||||
message={t('already-got-an-account')}
|
||||
text={t('login')}
|
||||
link={Paths.AuthLogin}
|
||||
//
|
||||
/>
|
||||
</IonGrid>
|
||||
</IonFooter>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthHome;
|
@@ -11,16 +11,33 @@ import {
|
||||
IonRouterLink,
|
||||
IonRow,
|
||||
IonToolbar,
|
||||
useIonRouter,
|
||||
} from '@ionic/react';
|
||||
// import { Action } from '../components/Action';
|
||||
import styles from './style.module.scss';
|
||||
import { Action } from '../../../components/Action';
|
||||
import { Paths } from '../../../Paths';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useUser } from '../../../hooks/use-user';
|
||||
import { LoadingScreen } from '../../../components/LoadingScreen';
|
||||
|
||||
const AuthHome = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [showLoading, setShowLoading] = useState<boolean>(true);
|
||||
const [showError, setShowErrr] = useState<{ show: boolean; message: string }>({
|
||||
show: false,
|
||||
message: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setShowLoading(false);
|
||||
}, []);
|
||||
|
||||
if (showLoading) return <LoadingScreen />;
|
||||
if (showError.show) return <>{showError.message}</>;
|
||||
|
||||
return (
|
||||
<IonPage className={'styles.homePage'}>
|
||||
<IonHeader>
|
||||
|
@@ -38,6 +38,7 @@ import { z as zod } from 'zod';
|
||||
import { pb } from '../../../lib/pb';
|
||||
import { ClientResponseError } from 'pocketbase';
|
||||
import { COL_USER_METAS, COL_USERS } from '../../../constants';
|
||||
import { Paths } from '../../../Paths';
|
||||
|
||||
function AuthSignUp(): React.JSX.Element {
|
||||
const params = useParams();
|
||||
@@ -109,6 +110,8 @@ function AuthSignUp(): React.JSX.Element {
|
||||
};
|
||||
const userMetaRecord = await pb.collection(COL_USER_METAS).create(userMeta);
|
||||
await pb.collection('users').requestVerification(user.email);
|
||||
|
||||
router.push(Paths.SignUpSuccess);
|
||||
} catch (err: any) {
|
||||
const res_err = err as unknown as ClientResponseError;
|
||||
const {
|
||||
@@ -133,19 +136,7 @@ function AuthSignUp(): React.JSX.Element {
|
||||
|
||||
return (
|
||||
<IonPage className={styles.loginPage}>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonBackButton icon={arrowBack} text="" className="custom-back" />
|
||||
</IonButtons>
|
||||
|
||||
<IonButtons slot="end">
|
||||
<IonButton className="custom-button">
|
||||
<IonIcon icon={shapesOutline} />
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonHeader></IonHeader>
|
||||
{/* */}
|
||||
<IonContent fullscreen>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
|
154
002_source/ionic_mobile/src/pages/auth/StudentInfo/index.tsx
Normal file
154
002_source/ionic_mobile/src/pages/auth/StudentInfo/index.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonFooter,
|
||||
IonGrid,
|
||||
IonHeader,
|
||||
IonPage,
|
||||
IonRow,
|
||||
IonText,
|
||||
useIonRouter,
|
||||
} from '@ionic/react';
|
||||
import styles from './style.module.scss';
|
||||
import { useParams } from 'react-router';
|
||||
import { Wave } from '../../../components/Wave';
|
||||
import { Paths } from '../../../Paths';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getUserMetaById } from '../../../db/UserMetas/GetById';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DBUserMeta } from '../../../db/UserMetas/type';
|
||||
import { LoadingScreen } from '../../../components/LoadingScreen';
|
||||
import { getStudentAvatar } from '../../../lib/getStudentAvatar';
|
||||
import { authClient } from '../../../lib/auth/custom/client';
|
||||
import { useUser } from '../../../hooks/use-user';
|
||||
|
||||
function StudentInfo(): React.JSX.Element {
|
||||
const router = useIonRouter();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [studentMeta, setStudentMeta] = useState<DBUserMeta>();
|
||||
const test = useUser();
|
||||
|
||||
const [showLoading, setShowLoading] = useState<boolean>(true);
|
||||
const [showError, setShowError] = useState<{ show: boolean; message: string }>({ show: false, message: '' });
|
||||
|
||||
function handleBackToLogin() {
|
||||
router.push(Paths.AuthLogin);
|
||||
}
|
||||
|
||||
function handleBackOnClick() {
|
||||
router.push(Paths.Setting);
|
||||
}
|
||||
|
||||
async function handleFetchUserMeta() {
|
||||
try {
|
||||
const result = await getUserMetaById(id);
|
||||
const tempStudentMeta = result as unknown as DBUserMeta;
|
||||
|
||||
setStudentMeta(tempStudentMeta);
|
||||
setShowLoading(false);
|
||||
} catch (error) {
|
||||
setShowError({ show: true, message: JSON.stringify({ error }, null, 2) });
|
||||
setShowLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogoutOnClick() {
|
||||
try {
|
||||
await authClient.signOut();
|
||||
router.push(Paths.AuthLogin);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void handleFetchUserMeta();
|
||||
}, []);
|
||||
|
||||
if (showLoading) return <LoadingScreen />;
|
||||
if (!studentMeta) return <LoadingScreen />;
|
||||
if (showError.show) return <>{showError.message}</>;
|
||||
|
||||
return (
|
||||
<IonPage className={styles.loginPage}>
|
||||
<IonHeader>{/* */}</IonHeader>
|
||||
{/* */}
|
||||
<IonContent fullscreen>
|
||||
<IonGrid className="ion-padding">
|
||||
<IonRow className="ion-justify-content-center">
|
||||
<IonCol size={'3'}>
|
||||
<div
|
||||
style={{
|
||||
backgroundImage: getStudentAvatar(studentMeta),
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
//
|
||||
width: '25vw',
|
||||
height: '25vw',
|
||||
//
|
||||
borderRadius: 'calc( 25vw / 2 )',
|
||||
}}
|
||||
></div>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
{/* */}
|
||||
<IonRow
|
||||
className="ion-justify-content-between"
|
||||
style={{
|
||||
marginTop: '1rem',
|
||||
marginBottom: '1rem',
|
||||
//
|
||||
}}
|
||||
>
|
||||
<IonText>{t('student-name')}</IonText>
|
||||
<IonText>{studentMeta.name}</IonText>
|
||||
</IonRow>
|
||||
{/* */}
|
||||
<IonRow
|
||||
className="ion-justify-content-between"
|
||||
style={{
|
||||
marginTop: '1rem',
|
||||
marginBottom: '1rem',
|
||||
//
|
||||
}}
|
||||
>
|
||||
<IonText>{t('student-email')}</IonText>
|
||||
<IonText>{studentMeta.email}</IonText>
|
||||
</IonRow>
|
||||
{/* */}
|
||||
<IonRow
|
||||
className="ion-justify-content-between"
|
||||
style={{
|
||||
marginTop: '1rem',
|
||||
marginBottom: '1rem',
|
||||
//
|
||||
}}
|
||||
>
|
||||
<IonText>{t('student-phone')}</IonText>
|
||||
<IonText>{studentMeta.phone}</IonText>
|
||||
</IonRow>
|
||||
{/* */}
|
||||
<IonRow className="ion-justify-content-center">
|
||||
<IonButton onClick={handleBackOnClick}>{t('back')}</IonButton>
|
||||
</IonRow>
|
||||
|
||||
<IonRow className="ion-justify-content-center">
|
||||
<IonButton onClick={handleLogoutOnClick}>{t('logout')}</IonButton>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</IonContent>
|
||||
{/* */}
|
||||
<IonFooter>
|
||||
<IonGrid className="ion-no-margin ion-no-padding">
|
||||
<Wave />
|
||||
</IonGrid>
|
||||
</IonFooter>
|
||||
</IonPage>
|
||||
);
|
||||
}
|
||||
|
||||
export default StudentInfo;
|
@@ -0,0 +1,17 @@
|
||||
.signupPage {
|
||||
ion-toolbar {
|
||||
--border-style: none;
|
||||
--border-color: transparent;
|
||||
--padding-top: 1rem;
|
||||
--padding-bottom: 1rem;
|
||||
--padding-start: 1rem;
|
||||
--padding-end: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.headingText {
|
||||
h5 {
|
||||
margin-top: 0.2rem;
|
||||
// color: #d3a6c7;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user