Reauth
This commit is contained in:
parent
7723dd0722
commit
0b661c7ccc
1
exam/dist/assets/index-BliQ0crd.js
vendored
Normal file
1
exam/dist/assets/index-BliQ0crd.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
exam/dist/assets/index-CfSUaRjT.js
vendored
1
exam/dist/assets/index-CfSUaRjT.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
6
exam/dist/index.html
vendored
6
exam/dist/index.html
vendored
@ -5,10 +5,10 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/phpCourse/exam/dist/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
<script type="module" crossorigin src="/phpCourse/exam/dist/assets/index-CfSUaRjT.js"></script>
|
||||
<script type="module" crossorigin src="/phpCourse/exam/dist/assets/index-BliQ0crd.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/react-C_FdcE2X.js">
|
||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/mui-C4H8cxTH.js">
|
||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-C0csOcmc.js">
|
||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/mui-53GMXZgr.js">
|
||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-ZGp-Rrdw.js">
|
||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/i18n-DyW0LrNj.js">
|
||||
<link rel="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/mui-CKDNpdid.css">
|
||||
<link rel="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/index-D83Ey19k.css">
|
||||
|
||||
2
exam/dist/locales/de/translation.json
vendored
2
exam/dist/locales/de/translation.json
vendored
@ -14,7 +14,7 @@
|
||||
"FailedUpdate_Duplicate:username:userUpdate": "Ein Benutzer mit diesem Benutzernamen existiert schon",
|
||||
"FailedUpdate_Duplicate:email:userUpdate": "Ein Benutzer mit dieser E-Mail existiert schon",
|
||||
|
||||
"NotAllowed_newPost": "Keine Berechtigung",
|
||||
"Unauthorized_newPost": "Keine Berechtigung",
|
||||
"MissingField_content:newPost": "Beitrag darf nicht leer sein",
|
||||
|
||||
"NotAllowed_postUpdate": "Keine Berechtigung",
|
||||
|
||||
10
exam/dist/locales/en/translation.json
vendored
10
exam/dist/locales/en/translation.json
vendored
@ -1,23 +1,23 @@
|
||||
{
|
||||
"Unauthorized": "NotAllowed",
|
||||
"Unauthorized": "Not allowed",
|
||||
|
||||
"NotAllowed_login": "Invalid email or password",
|
||||
"NotFound_user:login": "User does not exist",
|
||||
"MissingField_email:login": "E-Mail required",
|
||||
"MissingField_password:login": "Password required",
|
||||
|
||||
"NotAllowed_deletPost": "NotAllowed",
|
||||
"NotAllowed_deletPost": "Not allowed",
|
||||
"NotFound_post:deletePost": "Post not found",
|
||||
|
||||
"NotAllowed_userUpdate": "NotAllowed",
|
||||
"NotAllowed_userUpdate": "Not allowed",
|
||||
"NotFound_user:userUpdate": "User not found",
|
||||
"FailedUpdate_Duplicate:userUpdate": "A user with this username already exists",
|
||||
"FailedUpdate_Duplicate:email:userUpdate": "A user with this email already exists",
|
||||
|
||||
"NotAllowed_newPost": "NotAllowed",
|
||||
"Unauthorized_newPost": "Not allowed",
|
||||
"MissingField_content:newPost": "Content required",
|
||||
|
||||
"NotAllowed_postUpdate": "NotAllowed",
|
||||
"NotAllowed_postUpdate": "Not allowed",
|
||||
"NotFound_post:postUpdate": "Post not found",
|
||||
|
||||
"Duplicate_user:register": "A user with this username or email already exists",
|
||||
|
||||
2
exam/dist/stats.html
vendored
2
exam/dist/stats.html
vendored
File diff suppressed because one or more lines are too long
@ -12,5 +12,7 @@ module.exports = {
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
||||
'no-shadow': 'off',
|
||||
'@typescript-eslint/no-shadow': 'error',
|
||||
},
|
||||
};
|
||||
|
||||
@ -24,6 +24,7 @@
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-i18next": "^15.0.0",
|
||||
"use-local-storage-state": "^19.3.1",
|
||||
"zustand": "^4.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
21
exam/react/pnpm-lock.yaml
generated
21
exam/react/pnpm-lock.yaml
generated
@ -32,9 +32,6 @@ importers:
|
||||
'@tanstack/react-router':
|
||||
specifier: ^1.45.8
|
||||
version: 1.45.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@types/node':
|
||||
specifier: ^20.14.12
|
||||
version: 20.14.12
|
||||
i18next:
|
||||
specifier: ^23.12.2
|
||||
version: 23.12.2
|
||||
@ -53,6 +50,9 @@ importers:
|
||||
react-i18next:
|
||||
specifier: ^15.0.0
|
||||
version: 15.0.0(i18next@23.12.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
use-local-storage-state:
|
||||
specifier: ^19.3.1
|
||||
version: 19.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
zustand:
|
||||
specifier: ^4.5.4
|
||||
version: 4.5.4(@types/react@18.3.3)(react@18.3.1)
|
||||
@ -72,6 +72,9 @@ importers:
|
||||
'@tanstack/router-plugin':
|
||||
specifier: ^1.45.8
|
||||
version: 1.45.8(vite@5.3.4(@types/node@20.14.12))
|
||||
'@types/node':
|
||||
specifier: ^20.14.12
|
||||
version: 20.14.12
|
||||
'@types/react':
|
||||
specifier: ^18.3.3
|
||||
version: 18.3.3
|
||||
@ -1842,6 +1845,13 @@ packages:
|
||||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
||||
use-local-storage-state@19.3.1:
|
||||
resolution: {integrity: sha512-y3Z1dODXvZXZB4qtLDNN8iuXbsYD6TAxz61K58GWB9/yKwrNG9ynI0GzCTHi/Je1rMiyOwMimz0oyFsZn+Kj7Q==}
|
||||
engines: {node: '>=14'}
|
||||
peerDependencies:
|
||||
react: '>=18'
|
||||
react-dom: '>=18'
|
||||
|
||||
use-sync-external-store@1.2.0:
|
||||
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
|
||||
peerDependencies:
|
||||
@ -3721,6 +3731,11 @@ snapshots:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
use-local-storage-state@19.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
use-sync-external-store@1.2.0(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
"FailedUpdate_Duplicate:username:userUpdate": "Ein Benutzer mit diesem Benutzernamen existiert schon",
|
||||
"FailedUpdate_Duplicate:email:userUpdate": "Ein Benutzer mit dieser E-Mail existiert schon",
|
||||
|
||||
"NotAllowed_newPost": "Keine Berechtigung",
|
||||
"Unauthorized_newPost": "Keine Berechtigung",
|
||||
"MissingField_content:newPost": "Beitrag darf nicht leer sein",
|
||||
|
||||
"NotAllowed_postUpdate": "Keine Berechtigung",
|
||||
|
||||
@ -1,23 +1,23 @@
|
||||
{
|
||||
"Unauthorized": "NotAllowed",
|
||||
"Unauthorized": "Not allowed",
|
||||
|
||||
"NotAllowed_login": "Invalid email or password",
|
||||
"NotFound_user:login": "User does not exist",
|
||||
"MissingField_email:login": "E-Mail required",
|
||||
"MissingField_password:login": "Password required",
|
||||
|
||||
"NotAllowed_deletPost": "NotAllowed",
|
||||
"NotAllowed_deletPost": "Not allowed",
|
||||
"NotFound_post:deletePost": "Post not found",
|
||||
|
||||
"NotAllowed_userUpdate": "NotAllowed",
|
||||
"NotAllowed_userUpdate": "Not allowed",
|
||||
"NotFound_user:userUpdate": "User not found",
|
||||
"FailedUpdate_Duplicate:userUpdate": "A user with this username already exists",
|
||||
"FailedUpdate_Duplicate:email:userUpdate": "A user with this email already exists",
|
||||
|
||||
"NotAllowed_newPost": "NotAllowed",
|
||||
"Unauthorized_newPost": "Not allowed",
|
||||
"MissingField_content:newPost": "Content required",
|
||||
|
||||
"NotAllowed_postUpdate": "NotAllowed",
|
||||
"NotAllowed_postUpdate": "Not allowed",
|
||||
"NotFound_post:postUpdate": "Post not found",
|
||||
|
||||
"Duplicate_user:register": "A user with this username or email already exists",
|
||||
|
||||
@ -1,186 +0,0 @@
|
||||
import { POST_LIMIT, PROFILE_POST_LIMIT } from '../constanst';
|
||||
import { PostAuth, PostCreate, PostDelete, PostListAuth, PostListNonAuth, PostNew, PostUpdate } from '../types/Post';
|
||||
import { User, UserCreate, UserImageUpdate, UserUpdate } from '../types/User';
|
||||
|
||||
const BASE = 'https://khofmann.userpage.fu-berlin.de/phpCourse/exam/api/';
|
||||
|
||||
let instance: ApiImpl;
|
||||
|
||||
class ApiImpl {
|
||||
private token?: string;
|
||||
private refreshToken?: string;
|
||||
private self?: User;
|
||||
private userListeners: ((user?: User) => void)[] = [];
|
||||
|
||||
constructor() {
|
||||
if (instance) {
|
||||
throw new Error('New instance cannot be created!!');
|
||||
}
|
||||
|
||||
//eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
instance = this;
|
||||
}
|
||||
|
||||
public hasAuth = () => this.token !== undefined;
|
||||
//FIXME: TESTING
|
||||
public isAdmin = () => this.hasAuth() && this.self?.isAdmin;
|
||||
public getAuthenticatedUser = () => this.self;
|
||||
public getCurrentSession = () => [this.token, this.refreshToken];
|
||||
|
||||
public subscribeToAuthenticatedUser = (callback: (user?: User) => void) => {
|
||||
this.userListeners.push(callback);
|
||||
|
||||
return () => {
|
||||
this.userListeners = this.userListeners.filter((item) => item !== callback);
|
||||
};
|
||||
};
|
||||
|
||||
public logIn = async (email: string, password: string): Promise<void> => {
|
||||
const { user, token } = await (await this.post('login', { email, password })).json();
|
||||
this.self = user;
|
||||
this.token = token;
|
||||
this.userListeners.forEach((listener) => listener(user));
|
||||
};
|
||||
|
||||
public logOut = async (): Promise<boolean> => {
|
||||
try {
|
||||
if (this.token) return await (await this.postAuth('logout')).json();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
this.self = undefined;
|
||||
this.token = undefined;
|
||||
this.userListeners.forEach((listener) => listener());
|
||||
}
|
||||
};
|
||||
|
||||
public posts = async (page?: number): Promise<PostListNonAuth | PostListAuth> => {
|
||||
const url = `posts?p=${page ?? 0}&l=${POST_LIMIT}`;
|
||||
|
||||
if (this.token) return await (await this.getAuth(url)).json();
|
||||
|
||||
return await (await this.get(url)).json();
|
||||
};
|
||||
|
||||
public deletePost = async (id: number): Promise<PostDelete> => {
|
||||
return await (await this.delete(`posts/${id}?l=${POST_LIMIT}`)).json();
|
||||
};
|
||||
|
||||
public user = async (id?: number): Promise<User> => {
|
||||
return await (await this.getAuth(`users/${id ?? this.self?.id}`)).json();
|
||||
};
|
||||
|
||||
public updateUser = async (data: UserUpdate, id?: number): Promise<User> => {
|
||||
const user = await (await this.patch(`users/${id ?? 'self'}`, data as Record<string, unknown>)).json();
|
||||
this.self = user;
|
||||
this.userListeners.forEach((listener) => listener(user));
|
||||
return user;
|
||||
};
|
||||
|
||||
public updateUserImage = async (data: UserImageUpdate, id?: number): Promise<User> => {
|
||||
const formData = new FormData();
|
||||
if (data.image) formData.append('image', data.image);
|
||||
if (!data.image && data.predefined) formData.append('predefined', data.predefined);
|
||||
|
||||
const user = await (await this.postAuthRaw(`users/${id ?? 'self'}/image`, formData)).json();
|
||||
this.self = user;
|
||||
this.userListeners.forEach((listener) => listener(user));
|
||||
return user;
|
||||
};
|
||||
|
||||
public newPost = async (data: PostCreate): Promise<PostNew> => {
|
||||
return await (await this.postAuth(`posts?l=${POST_LIMIT}`, data as unknown as Record<string, unknown>)).json();
|
||||
};
|
||||
|
||||
public updatePost = async (data: PostUpdate, id: number): Promise<PostAuth> => {
|
||||
return await (await this.patch(`posts/${id}`, data as Record<string, unknown>)).json();
|
||||
};
|
||||
|
||||
public userPosts = async (id?: number): Promise<PostListAuth> => {
|
||||
return await (await this.getAuth(`users/${id}/posts?l=${PROFILE_POST_LIMIT}&s=desc`)).json();
|
||||
};
|
||||
|
||||
public createUser = async (data: UserCreate): Promise<User> => {
|
||||
return await (await this.post(`register`, data as unknown as Record<string, unknown>)).json();
|
||||
};
|
||||
|
||||
/* Internal */
|
||||
|
||||
private post = async (endpoint: string, body?: Record<string, unknown>, headers?: HeadersInit) => {
|
||||
const response = await fetch(`${BASE}${endpoint}`, {
|
||||
mode: 'cors',
|
||||
method: 'post',
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (response.ok) return response;
|
||||
throw await response.json();
|
||||
};
|
||||
|
||||
private postAuth = async (endpoint: string, body?: Record<string, unknown>, headers?: HeadersInit) => {
|
||||
const response = await fetch(`${BASE}${endpoint}`, {
|
||||
mode: 'cors',
|
||||
method: 'post',
|
||||
headers: { token: this.token ?? '', ...headers },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (response.ok) return response;
|
||||
throw await response.json();
|
||||
};
|
||||
|
||||
private postAuthRaw = async (endpoint: string, body?: FormData, headers?: HeadersInit) => {
|
||||
const response = await fetch(`${BASE}${endpoint}`, {
|
||||
mode: 'cors',
|
||||
method: 'post',
|
||||
headers: { token: this.token ?? '', ...headers },
|
||||
body,
|
||||
});
|
||||
if (response.ok) return response;
|
||||
throw await response.json();
|
||||
};
|
||||
|
||||
private get = async (endpoint: string, headers?: HeadersInit) => {
|
||||
const response = await fetch(`${BASE}${endpoint}`, {
|
||||
mode: 'cors',
|
||||
method: 'get',
|
||||
headers,
|
||||
});
|
||||
if (response.ok) return response;
|
||||
throw await response.json();
|
||||
};
|
||||
|
||||
private getAuth = async (endpoint: string, headers?: HeadersInit) => {
|
||||
const response = await fetch(`${BASE}${endpoint}`, {
|
||||
mode: 'cors',
|
||||
method: 'get',
|
||||
headers: { token: this.token ?? '', ...headers },
|
||||
});
|
||||
if (response.ok) return response;
|
||||
throw await response.json();
|
||||
};
|
||||
|
||||
private delete = async (endpoint: string, headers?: HeadersInit) => {
|
||||
const response = await fetch(`${BASE}${endpoint}`, {
|
||||
mode: 'cors',
|
||||
method: 'delete',
|
||||
headers: { token: this.token ?? '', ...headers },
|
||||
});
|
||||
if (response.ok) return response;
|
||||
throw await response.json();
|
||||
};
|
||||
|
||||
private patch = async (endpoint: string, body?: Record<string, unknown>, headers?: HeadersInit) => {
|
||||
const response = await fetch(`${BASE}${endpoint}`, {
|
||||
mode: 'cors',
|
||||
method: 'patch',
|
||||
headers: { token: this.token ?? '', ...headers },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (response.ok) return response;
|
||||
throw await response.json();
|
||||
};
|
||||
}
|
||||
|
||||
const Api = new ApiImpl();
|
||||
export default Api;
|
||||
326
exam/react/src/api/Api.tsx
Normal file
326
exam/react/src/api/Api.tsx
Normal file
@ -0,0 +1,326 @@
|
||||
import { createContext, FC, PropsWithChildren, useContext, useEffect, useRef, useState } from 'react';
|
||||
import useLocalStorageState from 'use-local-storage-state';
|
||||
import { POST_LIMIT, PROFILE_POST_LIMIT } from '../constanst';
|
||||
import { PostAuth, PostCreate, PostDelete, PostListAuth, PostListNonAuth, PostNew, PostUpdate } from '../types/Post';
|
||||
import { User, UserCreate, UserImageUpdate, UserUpdate } from '../types/User';
|
||||
|
||||
const BASE = 'https://khofmann.userpage.fu-berlin.de/phpCourse/exam/api/';
|
||||
|
||||
interface ApiContext {
|
||||
hasAuth: boolean;
|
||||
authenticatedUser?: User;
|
||||
currentSession: [string?, string?];
|
||||
|
||||
logIn?: (email: string, password: string) => Promise<void>;
|
||||
logOut?: () => Promise<boolean>;
|
||||
|
||||
posts?: (page?: number) => Promise<PostListNonAuth | PostListAuth>;
|
||||
newPost?: (data: PostCreate) => Promise<PostNew>;
|
||||
updatePost?: (data: PostUpdate, id: number) => Promise<PostAuth>;
|
||||
deletePost?: (id: number) => Promise<PostDelete>;
|
||||
|
||||
user?: (id?: number) => Promise<User>;
|
||||
createUser?: (data: UserCreate) => Promise<User>;
|
||||
updateUser?: (data: UserUpdate, id?: number) => Promise<User>;
|
||||
updateUserImage?: (data: UserImageUpdate, id?: number) => Promise<User>;
|
||||
|
||||
userPosts?: (id?: number) => Promise<PostListAuth>;
|
||||
}
|
||||
|
||||
const ApiContext = createContext<ApiContext>({
|
||||
hasAuth: false,
|
||||
currentSession: [undefined, undefined],
|
||||
});
|
||||
|
||||
//eslint-disable-next-line react-refresh/only-export-components
|
||||
export const useApi = () => {
|
||||
const {
|
||||
hasAuth,
|
||||
authenticatedUser,
|
||||
currentSession,
|
||||
|
||||
logIn,
|
||||
logOut,
|
||||
posts,
|
||||
newPost,
|
||||
updatePost,
|
||||
deletePost,
|
||||
|
||||
user,
|
||||
createUser,
|
||||
updateUser,
|
||||
updateUserImage,
|
||||
|
||||
userPosts,
|
||||
} = useContext(ApiContext);
|
||||
|
||||
if (
|
||||
logIn &&
|
||||
logOut &&
|
||||
posts &&
|
||||
newPost &&
|
||||
updatePost &&
|
||||
deletePost &&
|
||||
user &&
|
||||
createUser &&
|
||||
updateUser &&
|
||||
updateUserImage &&
|
||||
userPosts
|
||||
) {
|
||||
return {
|
||||
hasAuth,
|
||||
authenticatedUser,
|
||||
currentSession,
|
||||
|
||||
logIn,
|
||||
logOut,
|
||||
posts,
|
||||
newPost,
|
||||
updatePost,
|
||||
deletePost,
|
||||
|
||||
user,
|
||||
createUser,
|
||||
updateUser,
|
||||
updateUserImage,
|
||||
|
||||
userPosts,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error("Couldn't find context. Is your component inside an ApiProvider?");
|
||||
};
|
||||
|
||||
export const ApiProvider: FC<PropsWithChildren<Record<string, unknown>>> = ({ children }) => {
|
||||
const [hasAuth, setHasAuth] = useState(false);
|
||||
const [authenticatedUser, setAuthenticatedUser] = useState<User>();
|
||||
const [currentSession, setCurrentSession] = useLocalStorageState<[string | undefined, string | undefined]>(
|
||||
'egb_session',
|
||||
{ defaultValue: [undefined, undefined] }
|
||||
);
|
||||
|
||||
const token = useRef<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (currentSession[0] && !token.current) {
|
||||
token.current = currentSession[0];
|
||||
refresh();
|
||||
}
|
||||
}, [currentSession]); //eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const logIn = async (email: string, password: string): Promise<void> => {
|
||||
const { user, token: _token, refreshToken } = await (await post('login', { email, password })).json();
|
||||
setAuthenticatedUser(user);
|
||||
setCurrentSession([_token, refreshToken]);
|
||||
setHasAuth(true);
|
||||
token.current = _token;
|
||||
};
|
||||
|
||||
const logOut = async (): Promise<boolean> => {
|
||||
try {
|
||||
if (token.current) return await (await reAuth(() => postAuth('logout'))).json();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
setAuthenticatedUser(undefined);
|
||||
setCurrentSession([undefined, undefined]);
|
||||
setHasAuth(false);
|
||||
token.current = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const posts = async (page?: number): Promise<PostListNonAuth | PostListAuth> => {
|
||||
const url = `posts?p=${page ?? 0}&l=${POST_LIMIT}`;
|
||||
|
||||
if (hasAuth) return await (await reAuth(() => getAuth(url))).json();
|
||||
|
||||
return await (await get(url)).json();
|
||||
};
|
||||
|
||||
const newPost = async (data: PostCreate): Promise<PostNew> => {
|
||||
return await (
|
||||
await reAuth(() => postAuth(`posts?l=${POST_LIMIT}`, data as unknown as Record<string, unknown>))
|
||||
).json();
|
||||
};
|
||||
|
||||
const updatePost = async (data: PostUpdate, id: number): Promise<PostAuth> => {
|
||||
return await (await reAuth(() => patch(`posts/${id}`, data as Record<string, unknown>))).json();
|
||||
};
|
||||
|
||||
const deletePost = async (id: number): Promise<PostDelete> => {
|
||||
return await (await reAuth(() => _delete(`posts/${id}?l=${POST_LIMIT}`))).json();
|
||||
};
|
||||
|
||||
const user = async (id?: number): Promise<User> => {
|
||||
return await (await reAuth(() => getAuth(`users/${id ?? authenticatedUser?.id}`))).json();
|
||||
};
|
||||
|
||||
const createUser = async (data: UserCreate): Promise<User> => {
|
||||
return await (await post(`register`, data as unknown as Record<string, unknown>)).json();
|
||||
};
|
||||
|
||||
const updateUser = async (data: UserUpdate, id?: number): Promise<User> => {
|
||||
const _user = await (await reAuth(() => patch(`users/${id ?? 'self'}`, data as Record<string, unknown>))).json();
|
||||
setAuthenticatedUser(_user);
|
||||
return _user;
|
||||
};
|
||||
|
||||
const updateUserImage = async (data: UserImageUpdate, id?: number): Promise<User> => {
|
||||
const formData = new FormData();
|
||||
if (data.image) formData.append('image', data.image);
|
||||
if (!data.image && data.predefined) formData.append('predefined', data.predefined);
|
||||
|
||||
const _user = await (await reAuth(() => postAuthRaw(`users/${id ?? 'self'}/image`, formData))).json();
|
||||
setAuthenticatedUser(_user);
|
||||
return _user;
|
||||
};
|
||||
|
||||
const userPosts = async (id?: number): Promise<PostListAuth> => {
|
||||
return await (await reAuth(() => getAuth(`users/${id}/posts?l=${PROFILE_POST_LIMIT}&s=desc`))).json();
|
||||
};
|
||||
|
||||
/* IMPL */
|
||||
|
||||
const post = async (endpoint: string, body?: Record<string, unknown>, headers?: HeadersInit) => {
|
||||
const response = await fetch(`${BASE}${endpoint}`, {
|
||||
mode: 'cors',
|
||||
method: 'post',
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (response.ok) return response;
|
||||
throw await response.json();
|
||||
};
|
||||
|
||||
const postAuth = async (endpoint: string, body?: Record<string, unknown>, headers?: HeadersInit) => {
|
||||
const response = await fetch(`${BASE}${endpoint}`, {
|
||||
mode: 'cors',
|
||||
method: 'post',
|
||||
headers: { token: token.current ?? '', ...headers },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (response.ok) return response;
|
||||
throw await response.json();
|
||||
};
|
||||
|
||||
const postAuthRaw = async (endpoint: string, body?: FormData, headers?: HeadersInit) => {
|
||||
const response = await fetch(`${BASE}${endpoint}`, {
|
||||
mode: 'cors',
|
||||
method: 'post',
|
||||
headers: { token: token.current ?? '', ...headers },
|
||||
body,
|
||||
});
|
||||
if (response.ok) return response;
|
||||
throw await response.json();
|
||||
};
|
||||
|
||||
const get = async (endpoint: string, headers?: HeadersInit) => {
|
||||
const response = await fetch(`${BASE}${endpoint}`, {
|
||||
mode: 'cors',
|
||||
method: 'get',
|
||||
headers,
|
||||
});
|
||||
if (response.ok) return response;
|
||||
throw await response.json();
|
||||
};
|
||||
|
||||
const getAuth = async (endpoint: string, headers?: HeadersInit) => {
|
||||
const response = await fetch(`${BASE}${endpoint}`, {
|
||||
mode: 'cors',
|
||||
method: 'get',
|
||||
headers: { token: token.current ?? '', ...headers },
|
||||
});
|
||||
if (response.ok) return response;
|
||||
throw await response.json();
|
||||
};
|
||||
|
||||
const _delete = async (endpoint: string, headers?: HeadersInit) => {
|
||||
const response = await fetch(`${BASE}${endpoint}`, {
|
||||
mode: 'cors',
|
||||
method: 'delete',
|
||||
headers: { token: token.current ?? '', ...headers },
|
||||
});
|
||||
if (response.ok) return response;
|
||||
throw await response.json();
|
||||
};
|
||||
|
||||
const patch = async (endpoint: string, body?: Record<string, unknown>, headers?: HeadersInit) => {
|
||||
const response = await fetch(`${BASE}${endpoint}`, {
|
||||
mode: 'cors',
|
||||
method: 'patch',
|
||||
headers: { token: token.current ?? '', ...headers },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (response.ok) return response;
|
||||
throw await response.json();
|
||||
};
|
||||
|
||||
const reAuth = async <T,>(callback: () => Promise<T>): Promise<T> => {
|
||||
try {
|
||||
console.log('[REAUTH] fetching');
|
||||
const ret = await callback();
|
||||
|
||||
return ret;
|
||||
} catch {
|
||||
try {
|
||||
console.log('[REAUTH] fail, refreshing');
|
||||
// REAUTH
|
||||
await refresh();
|
||||
// DO AGAIN
|
||||
console.log('[REAUTH] refreshed, fetching again');
|
||||
const ret = await callback();
|
||||
|
||||
return ret;
|
||||
} catch (error) {
|
||||
console.log('[REAUTH] terminating session', error);
|
||||
setAuthenticatedUser(undefined);
|
||||
setHasAuth(false);
|
||||
setCurrentSession([undefined, undefined]);
|
||||
token.current = undefined;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const refresh = async () => {
|
||||
const {
|
||||
user: _user,
|
||||
token: _token,
|
||||
refreshToken,
|
||||
} = await (await postAuth('refresh', { refreshToken: currentSession[1] ?? 'INVALID DEFAULT' })).json();
|
||||
setAuthenticatedUser(_user);
|
||||
setCurrentSession([_token, refreshToken]);
|
||||
setHasAuth(true);
|
||||
token.current = _token;
|
||||
};
|
||||
|
||||
return (
|
||||
<ApiContext.Provider
|
||||
value={{
|
||||
hasAuth,
|
||||
authenticatedUser,
|
||||
currentSession,
|
||||
|
||||
logIn,
|
||||
logOut,
|
||||
|
||||
posts,
|
||||
newPost,
|
||||
updatePost,
|
||||
deletePost,
|
||||
|
||||
user,
|
||||
createUser,
|
||||
updateUser,
|
||||
updateUserImage,
|
||||
|
||||
userPosts,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ApiContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiProvider;
|
||||
@ -14,7 +14,7 @@ import { useForm } from '@tanstack/react-form';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { t } from 'i18next';
|
||||
import { FC, FormEvent, useState } from 'react';
|
||||
import Api from '../../../api/Api';
|
||||
import { useApi } from '../../../api/Api';
|
||||
import { PostAuth, PostUpdate } from '../../../types/Post';
|
||||
import ErrorComponent from '../../Error/ErrorComponent';
|
||||
|
||||
@ -29,6 +29,11 @@ const PostEditDialog: FC<Props> = ({ post, open, onClose }) => {
|
||||
const [error, setError] = useState<any>();
|
||||
const [characterCount, setCharacterCount] = useState(post.content.length);
|
||||
|
||||
const theme = useTheme();
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.only('xs'), { noSsr: true });
|
||||
const queryClient = useQueryClient();
|
||||
const Api = useApi();
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ data, id }: { data: PostUpdate; id: number }) => {
|
||||
return Api.updatePost(data, id);
|
||||
@ -54,16 +59,12 @@ const PostEditDialog: FC<Props> = ({ post, open, onClose }) => {
|
||||
);
|
||||
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
setError(error);
|
||||
} catch (_error: any) {
|
||||
setError(_error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const theme = useTheme();
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.only('xs'), { noSsr: true });
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleClose = () => {
|
||||
form.reset();
|
||||
setError(undefined);
|
||||
|
||||
@ -16,7 +16,7 @@ import { useForm } from '@tanstack/react-form';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { FC, FormEvent, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Api from '../../../api/Api';
|
||||
import { useApi } from '../../../api/Api';
|
||||
import { UserCreate } from '../../../types/User';
|
||||
import ErrorComponent from '../../Error/ErrorComponent';
|
||||
|
||||
@ -29,16 +29,17 @@ const RegisterDialog: FC<Props> = ({ open, onClose }) => {
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const [error, setError] = useState<any>();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.only('xs'), { noSsr: true });
|
||||
const Api = useApi();
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: ({ data }: { data: UserCreate }) => {
|
||||
return Api.createUser(data);
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.only('xs'), { noSsr: true });
|
||||
|
||||
const form = useForm<UserCreate>({
|
||||
defaultValues: {
|
||||
username: '',
|
||||
@ -55,8 +56,8 @@ const RegisterDialog: FC<Props> = ({ open, onClose }) => {
|
||||
}
|
||||
);
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
setError(error);
|
||||
} catch (_error: any) {
|
||||
setError(_error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -13,7 +13,7 @@ import { useForm } from '@tanstack/react-form';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { t } from 'i18next';
|
||||
import { FC, FormEvent, useState } from 'react';
|
||||
import Api from '../../../api/Api';
|
||||
import { useApi } from '../../../api/Api';
|
||||
import { User, UserUpdate } from '../../../types/User';
|
||||
import ErrorComponent from '../../Error/ErrorComponent';
|
||||
|
||||
@ -27,6 +27,11 @@ const UserEditDialog: FC<Props> = ({ user, open, onClose }) => {
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const [error, setError] = useState<any>();
|
||||
|
||||
const theme = useTheme();
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.only('xs'), { noSsr: true });
|
||||
const queryClient = useQueryClient();
|
||||
const Api = useApi();
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ data, id }: { data: UserUpdate; id?: number }) => {
|
||||
return Api.updateUser(data, id);
|
||||
@ -41,12 +46,12 @@ const UserEditDialog: FC<Props> = ({ user, open, onClose }) => {
|
||||
onSubmit: async ({ value }) => {
|
||||
try {
|
||||
updateMutation.mutate(
|
||||
{ data: value, id: Api.getAuthenticatedUser()?.id === user.id ? undefined : user.id },
|
||||
{ data: value, id: Api.authenticatedUser?.id === user.id ? undefined : user.id },
|
||||
{
|
||||
onSuccess: () => {
|
||||
handleClose();
|
||||
|
||||
const queryKey = Api.getAuthenticatedUser()?.id === user.id ? ['profile'] : ['profile', { id: user.id }];
|
||||
const queryKey = Api.authenticatedUser?.id === user.id ? ['profile'] : ['profile', { id: user.id }];
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
},
|
||||
onError: setError,
|
||||
@ -54,16 +59,12 @@ const UserEditDialog: FC<Props> = ({ user, open, onClose }) => {
|
||||
);
|
||||
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
setError(error);
|
||||
} catch (_error: any) {
|
||||
setError(_error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const theme = useTheme();
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.only('xs'), { noSsr: true });
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleClose = () => {
|
||||
form.reset();
|
||||
setError(undefined);
|
||||
|
||||
@ -24,7 +24,7 @@ import { useForm } from '@tanstack/react-form';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { t } from 'i18next';
|
||||
import { FC, FormEvent, useState } from 'react';
|
||||
import Api from '../../../api/Api';
|
||||
import { useApi } from '../../../api/Api';
|
||||
import { User, UserImageUpdate } from '../../../types/User';
|
||||
import ErrorComponent from '../../Error/ErrorComponent';
|
||||
|
||||
@ -38,6 +38,11 @@ const UserImageDialog: FC<Props> = ({ user, open, onClose }) => {
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const [error, setError] = useState<any>();
|
||||
|
||||
const theme = useTheme();
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.only('xs'), { noSsr: true });
|
||||
const queryClient = useQueryClient();
|
||||
const Api = useApi();
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ data, id }: { data: UserImageUpdate; id?: number }) => {
|
||||
return Api.updateUserImage(data, id);
|
||||
@ -48,27 +53,24 @@ const UserImageDialog: FC<Props> = ({ user, open, onClose }) => {
|
||||
onSubmit: async ({ value }) => {
|
||||
try {
|
||||
updateMutation.mutate(
|
||||
{ data: value, id: Api.getAuthenticatedUser()?.id === user.id ? undefined : user.id },
|
||||
{ data: value, id: Api.authenticatedUser?.id === user.id ? undefined : user.id },
|
||||
{
|
||||
onSuccess: () => {
|
||||
handleClose();
|
||||
|
||||
const queryKey = Api.getAuthenticatedUser()?.id === user.id ? ['profile'] : ['profile', { id: user.id }];
|
||||
const queryKey = Api.authenticatedUser?.id === user.id ? ['profile'] : ['profile', { id: user.id }];
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
},
|
||||
onError: setError,
|
||||
}
|
||||
);
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
setError(error);
|
||||
} catch (_error: any) {
|
||||
setError(_error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const theme = useTheme();
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.only('xs'), { noSsr: true });
|
||||
const queryClient = useQueryClient();
|
||||
const formState = form.useStore((state) => ({ image: state.values.image, predefined: state.values.predefined }));
|
||||
|
||||
const handleClose = () => {
|
||||
|
||||
@ -20,6 +20,8 @@ const ErrorComponent: FC<Props> = ({ error, context, color = 'error.main' }) =>
|
||||
case ERRORS.NOT_FOUND:
|
||||
return <Typography color={color}>{t(error.code, { context: `${error.entity}:${context}` })}</Typography>;
|
||||
case ERRORS.NOT_ALLOWED:
|
||||
case ERRORS.UNAUTHORIZED:
|
||||
console.log(error, context);
|
||||
return <Typography color={color}>{t(error.code, { context })}</Typography>;
|
||||
case ERRORS.FAILED_UPDATE:
|
||||
return error.fields.map((field: string, index: number) => (
|
||||
|
||||
@ -3,7 +3,7 @@ import { useForm } from '@tanstack/react-form';
|
||||
import { useRouter } from '@tanstack/react-router';
|
||||
import { FC, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Api from '../../../api/Api';
|
||||
import { useApi } from '../../../api/Api';
|
||||
import { Login } from '../../../types/User';
|
||||
import ErrorComponent from '../../Error/ErrorComponent';
|
||||
|
||||
@ -16,6 +16,7 @@ const LoginForm: FC<Props> = ({ handleClose }) => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const Api = useApi();
|
||||
|
||||
const form = useForm<Login>({
|
||||
defaultValues: {
|
||||
@ -28,8 +29,8 @@ const LoginForm: FC<Props> = ({ handleClose }) => {
|
||||
router.invalidate();
|
||||
handleClose();
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
setError(error);
|
||||
} catch (_error: any) {
|
||||
setError(_error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { Box, Button, CircularProgress, LinearProgress, TextField } from '@mui/material';
|
||||
import { Alert, Box, Button, CircularProgress, LinearProgress, Snackbar, TextField } from '@mui/material';
|
||||
import { useForm } from '@tanstack/react-form';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { FC, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Api from '../../../api/Api';
|
||||
import { useApi } from '../../../api/Api';
|
||||
import { PostCreate } from '../../../types/Post';
|
||||
import ErrorComponent from '../../Error/ErrorComponent';
|
||||
|
||||
@ -16,6 +16,7 @@ const PostForm: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const Api = useApi();
|
||||
|
||||
const newMutation = useMutation({
|
||||
mutationFn: ({ data }: { data: PostCreate }) => {
|
||||
@ -42,8 +43,8 @@ const PostForm: FC = () => {
|
||||
);
|
||||
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
setError(error);
|
||||
} catch (_error: any) {
|
||||
setError(_error);
|
||||
}
|
||||
},
|
||||
});
|
||||
@ -120,7 +121,21 @@ const PostForm: FC = () => {
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
{error && <ErrorComponent error={error} context="newPost" />}
|
||||
|
||||
<Snackbar
|
||||
open={newMutation.isError}
|
||||
autoHideDuration={2000}
|
||||
onClose={() => {
|
||||
newMutation.reset();
|
||||
}}
|
||||
TransitionProps={{
|
||||
onExited: () => setError(undefined),
|
||||
}}
|
||||
>
|
||||
<Alert severity="error" variant="filled" sx={{ width: '100%' }}>
|
||||
{error && <ErrorComponent error={error} context="newPost" color="white" />}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</Box>
|
||||
</form>
|
||||
);
|
||||
|
||||
@ -10,10 +10,9 @@ import {
|
||||
useScrollTrigger,
|
||||
} from '@mui/material';
|
||||
import { Link, useRouterState } from '@tanstack/react-router';
|
||||
import { cloneElement, FC, ReactElement, useEffect, useState } from 'react';
|
||||
import { cloneElement, FC, ReactElement, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Api from '../../api/Api';
|
||||
import { User } from '../../types/User';
|
||||
import { useApi } from '../../api/Api';
|
||||
import LanguageMenu from '../Menus/Language/LanguageMenu';
|
||||
import UserMenu from '../Menus/User/UserMenu';
|
||||
|
||||
@ -31,12 +30,10 @@ const ElevationScroll = ({ children }: { children: ReactElement }) => {
|
||||
const Header: FC = () => {
|
||||
const [anchorUserMenu, setAnchorUserMenu] = useState<null | HTMLElement>(null);
|
||||
const [anchorLanguageMenu, setAnchorLanguageMenu] = useState<null | HTMLElement>(null);
|
||||
const [user, setUser] = useState<User>();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const isLoading = useRouterState({ select: (s) => s.status === 'pending' });
|
||||
|
||||
useEffect(() => Api.subscribeToAuthenticatedUser((user) => setUser(user)), []);
|
||||
const Api = useApi();
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorLanguageMenu(null);
|
||||
@ -59,9 +56,9 @@ const Header: FC = () => {
|
||||
<IconButton size="large" onClick={(event) => setAnchorLanguageMenu(event.currentTarget)}>
|
||||
<Translate sx={{ color: 'white' }} />
|
||||
</IconButton>
|
||||
{user ? (
|
||||
{Api.authenticatedUser ? (
|
||||
<IconButton onClick={(event) => setAnchorUserMenu(event.currentTarget)} sx={{ p: 0 }}>
|
||||
<Avatar alt={user.username} src={`${user.image}`}>
|
||||
<Avatar alt={Api.authenticatedUser.username} src={`${Api.authenticatedUser.image}`}>
|
||||
<Person />
|
||||
</Avatar>
|
||||
</IconButton>
|
||||
|
||||
@ -3,7 +3,7 @@ import { useMatch, useNavigate, useRouter } from '@tanstack/react-router';
|
||||
import { t } from 'i18next';
|
||||
import { FC, useState } from 'react';
|
||||
import { Trans } from 'react-i18next/TransWithoutContext';
|
||||
import Api from '../../../api/Api';
|
||||
import { useApi } from '../../../api/Api';
|
||||
import { ROUTES } from '../../../types/Routes';
|
||||
import RegisterDialog from '../../Dialogs/Register/RegisterDialog';
|
||||
import LoginForm from '../../Forms/Login/LoginForm';
|
||||
@ -20,7 +20,7 @@ const UserMenu: FC<Props> = ({ anchorEl, handleClose }) => {
|
||||
const router = useRouter();
|
||||
const match = useMatch({ from: '/profile/', strict: true, shouldThrow: false });
|
||||
|
||||
const user = Api.getAuthenticatedUser();
|
||||
const Api = useApi();
|
||||
|
||||
const _handleClose = () => {
|
||||
setRegister(false);
|
||||
@ -47,7 +47,7 @@ const UserMenu: FC<Props> = ({ anchorEl, handleClose }) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{user ? (
|
||||
{Api.authenticatedUser ? (
|
||||
[
|
||||
<MenuItem
|
||||
selected={!!match}
|
||||
|
||||
@ -20,7 +20,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Link, useNavigate } from '@tanstack/react-router';
|
||||
import { FC, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Api from '../../api/Api';
|
||||
import { useApi } from '../../api/Api';
|
||||
import { PostAuth, PostNonAuth } from '../../types/Post';
|
||||
import convertDate from '../../utils/date';
|
||||
import PostEditDialog from '../Dialogs/PostEdit/PostEditDialog';
|
||||
@ -37,22 +37,23 @@ const Post: FC<Props> = ({ post, disableActions }) => {
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const [error, setError] = useState<any>();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const Api = useApi();
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: number) => {
|
||||
return Api.deletePost(id);
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
avatar={
|
||||
!disableActions && 'id' in post.user ? (
|
||||
post.user.id !== Api.getAuthenticatedUser()?.id ? (
|
||||
post.user.id !== Api.authenticatedUser?.id ? (
|
||||
<MUILink component={Link} to="/profile/$id" params={{ id: post.user.id }}>
|
||||
<Avatar alt={post.user.username} src={`${post.user.image}`}>
|
||||
<Person />
|
||||
@ -73,7 +74,7 @@ const Post: FC<Props> = ({ post, disableActions }) => {
|
||||
}
|
||||
title={
|
||||
!disableActions && 'id' in post.user ? (
|
||||
post.user.id !== Api.getAuthenticatedUser()?.id ? (
|
||||
post.user.id !== Api.authenticatedUser?.id ? (
|
||||
<MUILink component={Link} to="/profile/$id" params={{ id: post.user.id }}>
|
||||
{post.user.username}
|
||||
</MUILink>
|
||||
@ -96,7 +97,7 @@ const Post: FC<Props> = ({ post, disableActions }) => {
|
||||
|
||||
<CardActions>
|
||||
{!disableActions &&
|
||||
(Api.isAdmin() || ('id' in post.user && post.user.id === Api.getAuthenticatedUser()?.id)) && (
|
||||
(Api.authenticatedUser?.isAdmin || ('id' in post.user && post.user.id === Api.authenticatedUser?.id)) && (
|
||||
<>
|
||||
<Button size="small" onClick={() => setEditOpen(true)}>
|
||||
{t('Edit')}
|
||||
@ -104,7 +105,7 @@ const Post: FC<Props> = ({ post, disableActions }) => {
|
||||
<PostEditDialog post={post as PostAuth} open={editOpen} onClose={() => setEditOpen(false)} />
|
||||
</>
|
||||
)}
|
||||
{!disableActions && Api.isAdmin() && (
|
||||
{!disableActions && Api.authenticatedUser?.isAdmin && (
|
||||
<>
|
||||
<Button size="small" color="error" onClick={() => setDeleteOpen(true)}>
|
||||
{t('Delete')}
|
||||
@ -152,7 +153,7 @@ const Post: FC<Props> = ({ post, disableActions }) => {
|
||||
}}
|
||||
>
|
||||
<Alert severity="error" variant="filled" sx={{ width: '100%' }}>
|
||||
{error && <ErrorComponent error={error} context="delete" color="white" />}
|
||||
{error && <ErrorComponent error={error} context="deletePost" color="white" />}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
<Snackbar open={deleteMutation.isPending} message={t('Deleting')} />
|
||||
|
||||
@ -1,13 +1,9 @@
|
||||
import { QueryClient, QueryClientProvider, useQueryClient, useQueryErrorResetBoundary } from '@tanstack/react-query';
|
||||
import { ErrorRouteComponent, RouterProvider, createRouter, useRouter } from '@tanstack/react-router';
|
||||
import { StrictMode, useEffect } from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import Api from './api/Api';
|
||||
import { ERRORS } from './components/Error/Errors';
|
||||
|
||||
// Import the generated route tree
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import { routeTree } from './routeTree.gen';
|
||||
import { StrictMode } from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import ApiProvider from './api/Api';
|
||||
import Router from './router';
|
||||
|
||||
// Import i18n
|
||||
import './i18n';
|
||||
@ -21,61 +17,6 @@ import '@fontsource/roboto/700.css';
|
||||
// Query Client
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
//TODO: REAUTH HERE
|
||||
|
||||
//TODO: Make nice
|
||||
export const Error: ErrorRouteComponent = ({ error }) => {
|
||||
const router = useRouter();
|
||||
const queryErrorResetBoundary = useQueryErrorResetBoundary();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
// Reset the query error boundary
|
||||
queryErrorResetBoundary.reset();
|
||||
}, [queryErrorResetBoundary]);
|
||||
|
||||
//TODO: Split display, show something went wrong with retry on all, show session expired and back to main on auth
|
||||
if ('code' in error && error.code === ERRORS.NOT_ALLOWED) {
|
||||
Api.logOut().finally(() => {
|
||||
queryClient.clear();
|
||||
router.invalidate();
|
||||
});
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{error.message}
|
||||
<button
|
||||
onClick={() => {
|
||||
router.invalidate();
|
||||
}}
|
||||
>
|
||||
retry
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Create a new router instance
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: {
|
||||
queryClient,
|
||||
},
|
||||
defaultPreload: 'intent',
|
||||
// Since we're using React Query, we don't want loader calls to ever be stale
|
||||
// This will ensure that the loader is always called when the route is preloaded or visited
|
||||
defaultPreloadStaleTime: 0,
|
||||
basepath: process.env.NODE_ENV === 'development' ? 'phpCourse/exam/dist' : '/phpCourse/exam',
|
||||
defaultErrorComponent: Error,
|
||||
});
|
||||
|
||||
// Register the router instance for type safety
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: typeof router;
|
||||
}
|
||||
}
|
||||
|
||||
// Render the app
|
||||
const rootElement = document.getElementById('root')!;
|
||||
if (!rootElement.innerHTML) {
|
||||
@ -83,7 +24,9 @@ if (!rootElement.innerHTML) {
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
<ApiProvider>
|
||||
<Router />
|
||||
</ApiProvider>
|
||||
{process.env.NODE_ENV === 'development' && <ReactQueryDevtools initialIsOpen={false} />}
|
||||
</QueryClientProvider>
|
||||
</StrictMode>
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { queryOptions } from '@tanstack/react-query';
|
||||
import Api from '../api/Api';
|
||||
import { useApi } from '../api/Api';
|
||||
|
||||
export const postsQueryOptions = (page?: number) =>
|
||||
export const postsQueryOptions = (Api: ReturnType<typeof useApi>, page?: number) =>
|
||||
queryOptions({
|
||||
queryKey: ['posts', { page: page ?? 0, hasAuth: Api.hasAuth() }],
|
||||
queryFn: () => Api.posts(page),
|
||||
queryKey: ['posts', { page: page ?? 0, hasAuth: Api.hasAuth }],
|
||||
queryFn: async () => await Api.posts(page),
|
||||
});
|
||||
|
||||
@ -1,19 +1,20 @@
|
||||
import { queryOptions } from '@tanstack/react-query';
|
||||
import Api from '../api/Api';
|
||||
import { useApi } from '../api/Api';
|
||||
|
||||
export const profileSelfQueryOptions = queryOptions({
|
||||
queryKey: ['profile'],
|
||||
queryFn: () => Api.user(),
|
||||
});
|
||||
export const profileSelfQueryOptions = (Api: ReturnType<typeof useApi>) =>
|
||||
queryOptions({
|
||||
queryKey: ['profile'],
|
||||
queryFn: async () => await Api.user(),
|
||||
});
|
||||
|
||||
export const profileQueryOptions = (id?: number) =>
|
||||
export const profileQueryOptions = (Api: ReturnType<typeof useApi>, id?: number) =>
|
||||
queryOptions({
|
||||
queryKey: ['profile', { id }],
|
||||
queryFn: () => Api.user(id),
|
||||
queryFn: async () => await Api.user(id),
|
||||
});
|
||||
|
||||
export const profilePostsQueryOptions = (id: number) =>
|
||||
export const profilePostsQueryOptions = (Api: ReturnType<typeof useApi>, id: number) =>
|
||||
queryOptions({
|
||||
queryKey: ['profilePosts', { id }],
|
||||
queryFn: () => Api.userPosts(id),
|
||||
queryFn: async () => await Api.userPosts(id),
|
||||
});
|
||||
|
||||
70
exam/react/src/router.tsx
Normal file
70
exam/react/src/router.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { useQueryClient, useQueryErrorResetBoundary } from '@tanstack/react-query';
|
||||
import { createRouter, ErrorRouteComponent, RouterProvider, useRouter } from '@tanstack/react-router';
|
||||
import { useEffect } from 'react';
|
||||
import { useApi } from './api/Api';
|
||||
|
||||
import { routeTree } from './routeTree.gen';
|
||||
|
||||
//TODO: REAUTH HERE
|
||||
|
||||
//TODO: Make nice
|
||||
export const Error: ErrorRouteComponent = ({ error }) => {
|
||||
const router = useRouter();
|
||||
const queryErrorResetBoundary = useQueryErrorResetBoundary();
|
||||
const _queryClient = useQueryClient();
|
||||
const Api = useApi();
|
||||
|
||||
useEffect(() => {
|
||||
// Reset the query error boundary
|
||||
queryErrorResetBoundary.reset();
|
||||
}, [queryErrorResetBoundary]);
|
||||
|
||||
//TODO: Split display, show something went wrong with retry on all, show session expired and back to main on auth
|
||||
return (
|
||||
<div>
|
||||
{error.message}
|
||||
<button
|
||||
onClick={async () => {
|
||||
_queryClient.clear();
|
||||
if (Api.hasAuth) await Api.logOut();
|
||||
router.invalidate();
|
||||
}}
|
||||
>
|
||||
retry
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Create a new router instance
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: {
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
queryClient: undefined as any,
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
Api: undefined as any,
|
||||
},
|
||||
defaultPreload: 'intent',
|
||||
// Since we're using React Query, we don't want loader calls to ever be stale
|
||||
// This will ensure that the loader is always called when the route is preloaded or visited
|
||||
defaultPreloadStaleTime: 0,
|
||||
basepath: process.env.NODE_ENV === 'development' ? 'phpCourse/exam/dist' : '/phpCourse/exam',
|
||||
//defaultErrorComponent: Error,
|
||||
});
|
||||
|
||||
// Register the router instance for type safety
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: typeof router;
|
||||
}
|
||||
}
|
||||
|
||||
const Router = () => {
|
||||
const Api = useApi();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return <RouterProvider router={router} context={{ queryClient, Api }} />;
|
||||
};
|
||||
|
||||
export default Router;
|
||||
@ -1,7 +1,9 @@
|
||||
import { Box } from '@mui/material';
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router';
|
||||
import { QueryClient, useQueryErrorResetBoundary } from '@tanstack/react-query';
|
||||
import { createRootRouteWithContext, ErrorRouteComponent, Outlet, useRouter } from '@tanstack/react-router';
|
||||
import { TanStackRouterDevtools } from '@tanstack/router-devtools';
|
||||
import { useEffect } from 'react';
|
||||
import { useApi } from '../api/Api';
|
||||
import Header from '../components/Header/Header';
|
||||
|
||||
const Root = () => {
|
||||
@ -18,6 +20,31 @@ const Root = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({
|
||||
const ErrorComponent: ErrorRouteComponent = ({ error }) => {
|
||||
const router = useRouter();
|
||||
const queryErrorResetBoundary = useQueryErrorResetBoundary();
|
||||
|
||||
useEffect(() => {
|
||||
// Reset the query error boundary
|
||||
queryErrorResetBoundary.reset();
|
||||
}, [queryErrorResetBoundary]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{error.message}
|
||||
<button
|
||||
onClick={() => {
|
||||
// Invalidate the route to reload the loader, and reset any router error boundaries
|
||||
router.invalidate();
|
||||
}}
|
||||
>
|
||||
retry
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createRootRouteWithContext<{ queryClient: QueryClient; Api: ReturnType<typeof useApi> }>()({
|
||||
component: Root,
|
||||
errorComponent: ErrorComponent,
|
||||
});
|
||||
|
||||
@ -12,15 +12,16 @@ import { useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Api from '../api/Api';
|
||||
import { useApi } from '../api/Api';
|
||||
import PostForm from '../components/Forms/Post/PostForm';
|
||||
import Post from '../components/Post/Post';
|
||||
import { postsQueryOptions } from '../queries/postsQuery';
|
||||
import { ROUTES } from '../types/Routes';
|
||||
|
||||
const Home = () => {
|
||||
const Api = useApi();
|
||||
const { page } = Route.useSearch();
|
||||
const { data: postsQuery, isFetching } = useSuspenseQuery(postsQueryOptions(page));
|
||||
const { data: postsQuery, isFetching } = useSuspenseQuery(postsQueryOptions(Api, page));
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
@ -62,7 +63,7 @@ const Home = () => {
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
{Api.hasAuth() ? (
|
||||
{Api.hasAuth ? (
|
||||
<Grid item xs={12}>
|
||||
<PostForm />
|
||||
</Grid>
|
||||
@ -79,7 +80,8 @@ const Home = () => {
|
||||
|
||||
export const Route = createFileRoute(ROUTES.INDEX)({
|
||||
loaderDeps: ({ search: { page } }) => ({ page }),
|
||||
loader: ({ context: { queryClient }, deps: { page } }) => queryClient.ensureQueryData(postsQueryOptions(page)),
|
||||
loader: ({ context: { queryClient, Api }, deps: { page } }) =>
|
||||
queryClient.ensureQueryData(postsQueryOptions(Api, page)),
|
||||
validateSearch: (search: Record<string, unknown>): { page?: number } => {
|
||||
return {
|
||||
page: search?.page !== undefined ? Number(search?.page ?? 0) : undefined,
|
||||
|
||||
@ -2,21 +2,35 @@ import { Snackbar } from '@mui/material';
|
||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { createFileRoute, redirect } from '@tanstack/react-router';
|
||||
import { t } from 'i18next';
|
||||
import Api from '../../api/Api';
|
||||
import { useApi } from '../../api/Api';
|
||||
import { ERRORS } from '../../components/Error/Errors';
|
||||
import Profile from '../../components/Profile/Profile';
|
||||
import { profilePostsQueryOptions, profileQueryOptions } from '../../queries/profileQuery';
|
||||
import { PostAuth } from '../../types/Post';
|
||||
import { ROUTES } from '../../types/Routes';
|
||||
|
||||
const ProfilePage = () => {
|
||||
const Api = useApi();
|
||||
const { id } = Route.useParams();
|
||||
const { data: profileQuery, isFetching: isFetchingProfile } = useSuspenseQuery(profileQueryOptions(id));
|
||||
const { data: profilePostsQuery, isFetching: isFetchingPosts } = useSuspenseQuery(profilePostsQueryOptions(id));
|
||||
const {
|
||||
data: profileQuery,
|
||||
isFetching: isFetchingProfile,
|
||||
failureReason,
|
||||
} = useSuspenseQuery(profileQueryOptions(Api, id));
|
||||
const { data: profilePostsQuery, isFetching: isFetchingPosts } = useSuspenseQuery(profilePostsQueryOptions(Api, id));
|
||||
|
||||
if (failureReason && 'code' in failureReason && failureReason.code === ERRORS.UNAUTHORIZED) {
|
||||
throw failureReason;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Snackbar open={isFetchingProfile || isFetchingPosts} message={t('Updating')} />
|
||||
<Profile user={profileQuery} posts={profilePostsQuery.data as PostAuth[]} canEdit={Api.isAdmin()} />
|
||||
<Profile
|
||||
user={profileQuery}
|
||||
posts={profilePostsQuery.data as PostAuth[]}
|
||||
canEdit={Api.authenticatedUser?.isAdmin}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -26,13 +40,15 @@ export const Route = createFileRoute(`${ROUTES.PROFILE}/$id`)({
|
||||
parse: ({ id }) => ({ id: parseInt(id) }),
|
||||
stringify: ({ id }) => ({ id: id.toString() }),
|
||||
},
|
||||
loader: ({ context: { queryClient }, params: { id } }) => {
|
||||
queryClient.ensureQueryData(profileQueryOptions(id));
|
||||
queryClient.ensureQueryData(profilePostsQueryOptions(id));
|
||||
loader: ({ context: { queryClient, Api }, params: { id } }) => {
|
||||
console.log('LOAD PROFILE');
|
||||
queryClient.ensureQueryData(profileQueryOptions(Api, id));
|
||||
queryClient.ensureQueryData(profilePostsQueryOptions(Api, id));
|
||||
},
|
||||
beforeLoad: ({ params: { id } }) => {
|
||||
if (!Api.hasAuth()) throw redirect({ to: ROUTES.INDEX });
|
||||
if (id === Api.getAuthenticatedUser()?.id) throw redirect({ to: ROUTES.PROFILE });
|
||||
beforeLoad: ({ params: { id }, context: { Api } }) => {
|
||||
console.log('BEFORE PROFILE');
|
||||
if (!Api.hasAuth) throw redirect({ to: ROUTES.INDEX });
|
||||
if (id === Api.authenticatedUser?.id) throw redirect({ to: ROUTES.PROFILE });
|
||||
},
|
||||
component: ProfilePage,
|
||||
});
|
||||
|
||||
@ -2,18 +2,28 @@ import { Snackbar } from '@mui/material';
|
||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { createFileRoute, redirect } from '@tanstack/react-router';
|
||||
import { t } from 'i18next';
|
||||
import Api from '../../api/Api';
|
||||
import { useApi } from '../../api/Api';
|
||||
import { ERRORS } from '../../components/Error/Errors';
|
||||
import Profile from '../../components/Profile/Profile';
|
||||
import { profilePostsQueryOptions, profileSelfQueryOptions } from '../../queries/profileQuery';
|
||||
import { PostAuth } from '../../types/Post';
|
||||
import { ROUTES } from '../../types/Routes';
|
||||
|
||||
const ProfilePage = () => {
|
||||
const { data: profileQuery, isFetching: isFetchingProfile } = useSuspenseQuery(profileSelfQueryOptions);
|
||||
const Api = useApi();
|
||||
const {
|
||||
data: profileQuery,
|
||||
isFetching: isFetchingProfile,
|
||||
failureReason,
|
||||
} = useSuspenseQuery(profileSelfQueryOptions(Api));
|
||||
const { data: profilePostsQuery, isFetching: isFetchingPosts } = useSuspenseQuery(
|
||||
profilePostsQueryOptions(Api.getAuthenticatedUser()?.id ?? 0)
|
||||
profilePostsQueryOptions(Api, Api.authenticatedUser?.id ?? 0)
|
||||
);
|
||||
|
||||
if (failureReason && 'code' in failureReason && failureReason.code === ERRORS.UNAUTHORIZED) {
|
||||
throw failureReason;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Snackbar open={isFetchingProfile || isFetchingPosts} message={t('Updating')} />
|
||||
@ -23,12 +33,12 @@ const ProfilePage = () => {
|
||||
};
|
||||
|
||||
export const Route = createFileRoute(`${ROUTES.PROFILE}/`)({
|
||||
loader: ({ context: { queryClient } }) => {
|
||||
queryClient.ensureQueryData(profileSelfQueryOptions);
|
||||
queryClient.ensureQueryData(profilePostsQueryOptions(Api.getAuthenticatedUser()?.id ?? 0));
|
||||
loader: ({ context: { queryClient, Api } }) => {
|
||||
queryClient.ensureQueryData(profileSelfQueryOptions(Api));
|
||||
queryClient.ensureQueryData(profilePostsQueryOptions(Api, Api.authenticatedUser?.id ?? 0));
|
||||
},
|
||||
beforeLoad: () => {
|
||||
if (!Api.hasAuth()) throw redirect({ to: ROUTES.INDEX });
|
||||
beforeLoad: ({ context: { Api } }) => {
|
||||
if (!Api.hasAuth) throw redirect({ to: ROUTES.INDEX });
|
||||
},
|
||||
component: ProfilePage,
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user