Reauth
This commit is contained in:
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+3
-3
@@ -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">
|
||||
|
||||
+1
-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",
|
||||
|
||||
+5
-5
@@ -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",
|
||||
|
||||
Vendored
+1
-1
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": {
|
||||
|
||||
Generated
+18
-3
@@ -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;
|
||||
@@ -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')} />
|
||||
|
||||
+8
-65
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user