From ca0a6b2950a73d158654b9b7cc83a97faf428d5f Mon Sep 17 00:00:00 2001 From: john Date: Sun, 18 May 2025 19:11:32 +0200 Subject: [PATCH 01/87] fix signup code usage --- src/app/auth/authService.ts | 4 +++- src/app/auth/pages/SignupPage.tsx | 12 +++++++++++- src/types.d.ts | 7 +++++++ 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 src/types.d.ts diff --git a/src/app/auth/authService.ts b/src/app/auth/authService.ts index 3668822..0cb3c36 100644 --- a/src/app/auth/authService.ts +++ b/src/app/auth/authService.ts @@ -1,6 +1,7 @@ import { User } from '../user/userStore.ts' import { dispatchMessage } from '../messageBus/messageBus.ts' import client from '../api/client.ts' +import { ProblemDetails } from '../../types' export class AuthService { constructor(private readonly user: User | null) {} @@ -33,7 +34,8 @@ export class AuthService { }) if (!res.data) { - throw new Error('invalid credentials') + console.error(res.error) + throw new Error((res.error as ProblemDetails)?.detail ?? 'invalid credentials') } dispatchMessage('auth:registered', { ...res.data }) diff --git a/src/app/auth/pages/SignupPage.tsx b/src/app/auth/pages/SignupPage.tsx index dbfc481..ba3b994 100644 --- a/src/app/auth/pages/SignupPage.tsx +++ b/src/app/auth/pages/SignupPage.tsx @@ -20,7 +20,7 @@ export default function SignupPage({ authService }: SignupPageProps) { const { code } = useParams() const [signupCode, setSignupCode] = useState(null) const [isSubmitting, setIsSubmitting] = useState(false) - + const [error, setError] = useState('') const [username, setUsername, usernameError, validateUsername] = useValidatedInput(isValidUsername) @@ -44,6 +44,7 @@ export default function SignupPage({ authService }: SignupPageProps) { localStorage.setItem(SignupCodeKey, theSignupCode) } else { theSignupCode = localStorage.getItem(SignupCodeKey) + setSignupCode(theSignupCode) } if (!theSignupCode) { @@ -51,6 +52,10 @@ export default function SignupPage({ authService }: SignupPageProps) { } }, [code, signupCode]) + useEffect(() => { + console.debug('signup code', signupCode) + }, [signupCode]) + const onSubmit = async (e: FormEvent) => { e.preventDefault() @@ -78,6 +83,9 @@ export default function SignupPage({ authService }: SignupPageProps) { try { await authService.signup(username, password, signupCode) navigate('/') + } catch (e: unknown) { + const err = e as Error + setError(err.message) } finally { setIsSubmitting(false) } @@ -120,6 +128,8 @@ export default function SignupPage({ authService }: SignupPageProps) { login instead? + + {error} diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 0000000..ad3865e --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1,7 @@ +export interface ProblemDetails { + detail: string + title: string + status: number + type: string + traceId: string +} From 27098ec9fab7feb9617126897327e75eea63f0cf Mon Sep 17 00:00:00 2001 From: john Date: Sun, 18 May 2025 21:20:32 +0200 Subject: [PATCH 02/87] update authservice --- src/App.tsx | 4 +- src/app/api/schema.ts | 83 ++++++++++++++++++++++++++++++++++++- src/app/auth/authService.ts | 40 +++++++++++------- 3 files changed, 108 insertions(+), 19 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 8b27c8b..cb89675 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,15 +6,13 @@ import { MediaService } from './app/media/mediaService.ts' import SignupPage from './app/auth/pages/SignupPage.tsx' import LoginPage from './app/auth/pages/LoginPage.tsx' import { AuthService } from './app/auth/authService.ts' -import { useUser } from './app/user/userStore.ts' import LogoutPage from './app/auth/pages/LogoutPage.tsx' import UnauthorizedHandler from './app/auth/components/UnauthorizedHandler.tsx' function App() { - const { user } = useUser() const postService = new PostsService() const mediaService = new MediaService() - const authService = new AuthService(user) + const authService = new AuthService() return ( diff --git a/src/app/api/schema.ts b/src/app/api/schema.ts index 6023ed1..66b0ed1 100644 --- a/src/app/api/schema.ts +++ b/src/app/api/schema.ts @@ -87,8 +87,7 @@ export interface paths { requestBody: { content: { 'multipart/form-data': { - /** Format: binary */ - file?: string + file?: components['schemas']['IFormFile'] } } } @@ -266,6 +265,66 @@ export interface paths { patch?: never trace?: never } + '/auth/signup-codes': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': components['schemas']['ListSignupCodesResult'] + 'application/json': components['schemas']['ListSignupCodesResult'] + 'text/json': components['schemas']['ListSignupCodesResult'] + } + } + } + } + put?: never + post: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['CreateSignupCodeRequest'] + 'text/json': components['schemas']['CreateSignupCodeRequest'] + 'application/*+json': components['schemas']['CreateSignupCodeRequest'] + } + } + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown + } + content?: never + } + } + } + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } } export type webhooks = Record export interface components { @@ -292,11 +351,21 @@ export interface components { /** Format: uuid */ postId: string } + CreateSignupCodeRequest: { + code: string + email: string + name: string + } GetAllPublicPostsResponse: { posts: components['schemas']['PostDto'][] /** Format: uuid */ next: string | null } + /** Format: binary */ + IFormFile: string + ListSignupCodesResult: { + signupCodes: components['schemas']['SignupCodeDto'][] + } LoginRequest: { username: string password: string @@ -339,6 +408,16 @@ export interface components { userId: string username: string } + SignupCodeDto: { + code: string + email: string + name: string + /** Format: uuid */ + redeemingUserId: string | null + redeemingUsername: string | null + /** Format: date-time */ + expiresOn: string | null + } UploadMediaResponse: { /** Format: uuid */ mediaId: string diff --git a/src/app/auth/authService.ts b/src/app/auth/authService.ts index 0cb3c36..2fb7ed9 100644 --- a/src/app/auth/authService.ts +++ b/src/app/auth/authService.ts @@ -1,16 +1,11 @@ -import { User } from '../user/userStore.ts' import { dispatchMessage } from '../messageBus/messageBus.ts' import client from '../api/client.ts' import { ProblemDetails } from '../../types' export class AuthService { - constructor(private readonly user: User | null) {} + constructor() {} async login(username: string, password: string) { - if (this.user != null) { - throw new Error('already logged in') - } - const res = await client.POST('/auth/login', { body: { username, password }, credentials: 'include', @@ -24,10 +19,6 @@ export class AuthService { } async signup(username: string, password: string, signupCode: string) { - if (this.user != null) { - throw new Error('already logged in') - } - const res = await client.POST('/auth/register', { body: { username, password, signupCode, email: null }, credentials: 'include', @@ -42,12 +33,33 @@ export class AuthService { } async logout() { - if (this.user == null) { - return - } - await client.DELETE('/auth/session', { credentials: 'include' }) dispatchMessage('auth:logged-out', null) } + + async createSignupCode(code: string, email: string, name: string) { + const res = await client.POST('/auth/signup-codes', { + body: { code, email, name }, + credentials: 'include', + }) + + if (!res.data) { + console.error(res.error) + throw new Error('failed to create signup code') + } + } + + async listSignupCodes() { + const res = await client.GET('/auth/signup-codes', { + credentials: 'include', + }) + + if (!res.data) { + console.error(res.error) + throw new Error('error') + } + + return res.data.signupCodes + } } From 48788973061914ae7e02991f750c3a1ceeb1a98c Mon Sep 17 00:00:00 2001 From: john Date: Sun, 18 May 2025 22:45:27 +0200 Subject: [PATCH 03/87] signups --- src/App.tsx | 8 + src/app/admin/pages/AdminPage.tsx | 24 +++ .../subpages/SignupCodesManagementPage.tsx | 187 ++++++++++++++++++ src/app/api/schema.ts | 7 +- src/app/auth/authService.ts | 3 +- src/app/auth/signupCode.ts | 20 ++ src/app/messageBus/messageTypes.ts | 12 +- src/app/user/userStore.ts | 22 +-- src/components/NavBar.tsx | 8 +- src/components/buttons/NavButton.tsx | 9 +- 10 files changed, 268 insertions(+), 32 deletions(-) create mode 100644 src/app/admin/pages/AdminPage.tsx create mode 100644 src/app/admin/pages/subpages/SignupCodesManagementPage.tsx create mode 100644 src/app/auth/signupCode.ts diff --git a/src/App.tsx b/src/App.tsx index cb89675..74cc867 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,8 @@ import LoginPage from './app/auth/pages/LoginPage.tsx' import { AuthService } from './app/auth/authService.ts' import LogoutPage from './app/auth/pages/LogoutPage.tsx' import UnauthorizedHandler from './app/auth/components/UnauthorizedHandler.tsx' +import AdminPage from './app/admin/pages/AdminPage.tsx' +import SignupCodesManagementPage from './app/admin/pages/subpages/SignupCodesManagementPage.tsx' function App() { const postService = new PostsService() @@ -26,6 +28,12 @@ function App() { } /> } /> } /> + }> + } + /> + diff --git a/src/app/admin/pages/AdminPage.tsx b/src/app/admin/pages/AdminPage.tsx new file mode 100644 index 0000000..18fa310 --- /dev/null +++ b/src/app/admin/pages/AdminPage.tsx @@ -0,0 +1,24 @@ +import NavBar from '../../../components/NavBar' +import NavButton from '../../../components/buttons/NavButton' +import { Outlet } from 'react-router-dom' + +export default function AdminPage() { + return ( +
+ + home + + +
+ +
+ +
+
+
+ ) +} diff --git a/src/app/admin/pages/subpages/SignupCodesManagementPage.tsx b/src/app/admin/pages/subpages/SignupCodesManagementPage.tsx new file mode 100644 index 0000000..a04bb7f --- /dev/null +++ b/src/app/admin/pages/subpages/SignupCodesManagementPage.tsx @@ -0,0 +1,187 @@ +import { AuthService } from '../../../auth/authService.ts' +import { useEffect, useState, useRef } from 'react' +import { SignupCode } from '../../../auth/signupCode.ts' +import { Temporal } from '@js-temporal/polyfill' +import Button from '../../../../components/buttons/Button.tsx' + +interface SignupCodesManagementPageProps { + authService: AuthService +} + +export default function SignupCodesManagementPage({ authService }: SignupCodesManagementPageProps) { + const [codes, setCodes] = useState([]) + const [code, setCode] = useState('') + const [name, setName] = useState('') + const [email, setEmail] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const dialogRef = useRef(null) + + const fetchCodes = async () => { + try { + setCodes(await authService.listSignupCodes()) + } catch (err) { + console.error('Failed to fetch signup codes:', err) + } + } + + useEffect(() => { + const timeoutId = setTimeout(fetchCodes) + return () => clearTimeout(timeoutId) + }, [authService]) + + const handleCreateCode = async (e: React.FormEvent) => { + e.preventDefault() + setIsLoading(true) + setError(null) + + try { + await authService.createSignupCode(code, email, name) + setCode('') + setName('') + setEmail('') + dialogRef.current?.close() + fetchCodes() // Refresh the table + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to create signup code') + } finally { + setIsLoading(false) + } + } + + const openDialog = () => { + dialogRef.current?.showModal() + } + + const closeDialog = () => { + dialogRef.current?.close() + setError(null) + } + + const formatDate = (date: Temporal.Instant | null) => { + if (!date) return 'Never' + try { + // Convert Temporal.Instant to a JavaScript Date for formatting + const jsDate = new Date(date.epochMilliseconds) + + // Format as: "Jan 1, 2023, 12:00 PM" + return jsDate.toLocaleString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) + } catch (err: unknown) { + console.error(err) + return date.toString() + } + } + + return ( + <> +
+

Signup Codes

+ +
+ +
+ + + + + + + + + + + {codes.map((code) => ( + + + + + + + ))} + {codes.length === 0 && ( + + + + )} + +
CodeEmailRedeemed ByExpires On
+ {code.code} + {code.email}{code.redeemedBy || 'Not redeemed'}{formatDate(code.expiresOn)}
+ No signup codes found +
+
+ + +

Create New Signup Code

+ + {error && ( +
+ {error} +
+ )} + +
+
+ + setCode(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md" + required + /> +
+ +
+ + setName(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md" + required + /> +
+ +
+ + setEmail(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md" + required + /> +
+ +
+ + +
+
+
+ + ) +} diff --git a/src/app/api/schema.ts b/src/app/api/schema.ts index 66b0ed1..32d4ad8 100644 --- a/src/app/api/schema.ts +++ b/src/app/api/schema.ts @@ -87,7 +87,8 @@ export interface paths { requestBody: { content: { 'multipart/form-data': { - file?: components['schemas']['IFormFile'] + /** Format: binary */ + file?: string } } } @@ -361,8 +362,6 @@ export interface components { /** Format: uuid */ next: string | null } - /** Format: binary */ - IFormFile: string ListSignupCodesResult: { signupCodes: components['schemas']['SignupCodeDto'][] } @@ -374,6 +373,7 @@ export interface components { /** Format: uuid */ userId: string username: string + isSuperUser: boolean } PostAuthorDto: { /** Format: uuid */ @@ -407,6 +407,7 @@ export interface components { /** Format: uuid */ userId: string username: string + isSuperUser: boolean } SignupCodeDto: { code: string diff --git a/src/app/auth/authService.ts b/src/app/auth/authService.ts index 2fb7ed9..a5733d6 100644 --- a/src/app/auth/authService.ts +++ b/src/app/auth/authService.ts @@ -1,6 +1,7 @@ import { dispatchMessage } from '../messageBus/messageBus.ts' import client from '../api/client.ts' import { ProblemDetails } from '../../types' +import { SignupCode } from './signupCode.ts' export class AuthService { constructor() {} @@ -60,6 +61,6 @@ export class AuthService { throw new Error('error') } - return res.data.signupCodes + return res.data.signupCodes.map(SignupCode.fromDto) } } diff --git a/src/app/auth/signupCode.ts b/src/app/auth/signupCode.ts new file mode 100644 index 0000000..96801b5 --- /dev/null +++ b/src/app/auth/signupCode.ts @@ -0,0 +1,20 @@ +import { Temporal } from '@js-temporal/polyfill' +import { components } from '../api/schema.ts' + +export class SignupCode { + constructor( + public readonly code: string, + public readonly email: string, + public readonly redeemedBy: string | null, + public readonly expiresOn: Temporal.Instant | null, + ) {} + + static fromDto(dto: components['schemas']['SignupCodeDto']): SignupCode { + return new SignupCode( + dto.code, + dto.email, + dto.redeemingUsername, + dto.expiresOn ? Temporal.Instant.from(dto.expiresOn) : null, + ) + } +} diff --git a/src/app/messageBus/messageTypes.ts b/src/app/messageBus/messageTypes.ts index 3ecfdcb..8d15f6c 100644 --- a/src/app/messageBus/messageTypes.ts +++ b/src/app/messageBus/messageTypes.ts @@ -1,12 +1,8 @@ +import { User } from '../user/userStore.ts' + export interface MessageTypes { - 'auth:logged-in': { - userId: string - username: string - } - 'auth:registered': { - userId: string - username: string - } + 'auth:logged-in': User + 'auth:registered': User 'auth:logged-out': null 'auth:unauthorized': null } diff --git a/src/app/user/userStore.ts b/src/app/user/userStore.ts index 052de79..49107fc 100644 --- a/src/app/user/userStore.ts +++ b/src/app/user/userStore.ts @@ -4,6 +4,7 @@ import { addMessageListener } from '../messageBus/messageBus.ts' export interface User { userId: string username: string + isSuperUser: boolean } export type UserStore = Store @@ -16,23 +17,10 @@ userStore.subscribe((user) => { localStorage.setItem(UserKey, JSON.stringify(user)) }) -addMessageListener('auth:logged-in', (e) => { - userStore.setState({ - userId: e.userId, - username: e.username, - }) -}) - -addMessageListener('auth:registered', (e) => { - userStore.setState({ - userId: e.userId, - username: e.username, - }) -}) - -addMessageListener('auth:logged-out', () => { - userStore.setState(null) -}) +const setUser = (u: User | null) => userStore.setState(u) +addMessageListener('auth:logged-in', setUser) +addMessageListener('auth:registered', setUser) +addMessageListener('auth:logged-out', setUser) export const useUser = () => { const [user] = useStore(userStore) diff --git a/src/components/NavBar.tsx b/src/components/NavBar.tsx index 265e525..5dc99dd 100644 --- a/src/components/NavBar.tsx +++ b/src/components/NavBar.tsx @@ -1,9 +1,15 @@ import { PropsWithChildren } from 'react' +import { useUser } from '../app/user/userStore.ts' +import NavButton from './buttons/NavButton.tsx' type NavBarProps = unknown export default function NavBar({ children }: PropsWithChildren) { + const { user } = useUser() return ( - + ) } diff --git a/src/components/buttons/NavButton.tsx b/src/components/buttons/NavButton.tsx index fd2fe67..1ae5373 100644 --- a/src/components/buttons/NavButton.tsx +++ b/src/components/buttons/NavButton.tsx @@ -2,6 +2,7 @@ import { PropsWithChildren } from 'react' import { Link } from 'react-router-dom' interface NavLinkButtonProps { to: string | To + className?: string } interface To { @@ -10,9 +11,13 @@ interface To { hash?: string } -export default function NavButton({ to, children }: PropsWithChildren) { +export default function NavButton({ + to, + className: extraClasses = '', + children, +}: PropsWithChildren) { return ( - + {children} ) From bf07b6da4f791c0413ebba193e08fb50dc73d40e Mon Sep 17 00:00:00 2001 From: john Date: Sun, 18 May 2025 22:49:06 +0200 Subject: [PATCH 04/87] copy to clipboard --- .../subpages/SignupCodesManagementPage.tsx | 60 ++++++++++++++++++- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/src/app/admin/pages/subpages/SignupCodesManagementPage.tsx b/src/app/admin/pages/subpages/SignupCodesManagementPage.tsx index a04bb7f..693ebd6 100644 --- a/src/app/admin/pages/subpages/SignupCodesManagementPage.tsx +++ b/src/app/admin/pages/subpages/SignupCodesManagementPage.tsx @@ -1,5 +1,5 @@ import { AuthService } from '../../../auth/authService.ts' -import { useEffect, useState, useRef } from 'react' +import { useEffect, useState, useRef, MouseEvent } from 'react' import { SignupCode } from '../../../auth/signupCode.ts' import { Temporal } from '@js-temporal/polyfill' import Button from '../../../../components/buttons/Button.tsx' @@ -16,6 +16,8 @@ export default function SignupCodesManagementPage({ authService }: SignupCodesMa const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) const dialogRef = useRef(null) + const [tooltipPosition, setTooltipPosition] = useState<{ x: number; y: number } | null>(null) + const [activeCode, setActiveCode] = useState(null) const fetchCodes = async () => { try { @@ -61,7 +63,6 @@ export default function SignupCodesManagementPage({ authService }: SignupCodesMa const formatDate = (date: Temporal.Instant | null) => { if (!date) return 'Never' try { - // Convert Temporal.Instant to a JavaScript Date for formatting const jsDate = new Date(date.epochMilliseconds) // Format as: "Jan 1, 2023, 12:00 PM" @@ -78,6 +79,36 @@ export default function SignupCodesManagementPage({ authService }: SignupCodesMa } } + const copyCodeToClipboard = (code: string, e: MouseEvent) => { + e.preventDefault() + const host = window.location.origin + const url = `${host}?c=${code}` + navigator.clipboard.writeText(url) + .then(() => { + // Optional: Show a success message or notification + console.log('Copied to clipboard:', url) + }) + .catch(err => { + console.error('Failed to copy:', err) + }) + setTooltipPosition(null) + setActiveCode(null) + } + + const showTooltip = (code: string, e: MouseEvent) => { + const rect = (e.currentTarget as HTMLElement).getBoundingClientRect() + setTooltipPosition({ + x: rect.left + rect.width / 2, + y: rect.top - 10 + }) + setActiveCode(code) + } + + const hideTooltip = () => { + setTooltipPosition(null) + setActiveCode(null) + } + return ( <>
@@ -99,7 +130,14 @@ export default function SignupCodesManagementPage({ authService }: SignupCodesMa {codes.map((code) => ( - {code.code} + {code.email} {code.redeemedBy || 'Not redeemed'} @@ -182,6 +220,22 @@ export default function SignupCodesManagementPage({ authService }: SignupCodesMa
+ + {tooltipPosition && activeCode && ( + + Copy to clipboard + + )} ) } From f7558f3c93eef0c93d8c0ec500688ff7c1ca875f Mon Sep 17 00:00:00 2001 From: john Date: Sun, 18 May 2025 22:57:06 +0200 Subject: [PATCH 05/87] save code when it's there --- src/app/feed/pages/HomePage.tsx | 8 +++++++- src/hooks/useSaveSignupCodeToLocalStorage.ts | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 src/hooks/useSaveSignupCodeToLocalStorage.ts diff --git a/src/app/feed/pages/HomePage.tsx b/src/app/feed/pages/HomePage.tsx index 910e1db..5ba5d27 100644 --- a/src/app/feed/pages/HomePage.tsx +++ b/src/app/feed/pages/HomePage.tsx @@ -10,6 +10,7 @@ import { Temporal } from '@js-temporal/polyfill' import SingleColumnLayout from '../../../layouts/SingleColumnLayout.tsx' import NavBar from '../../../components/NavBar.tsx' import AuthNavButtons from '../../auth/components/AuthNavButtons.tsx' +import { useSaveSignupCodeToLocalStorage } from '../../../hooks/useSaveSignupCodeToLocalStorage.ts' interface HomePageProps { postsService: PostsService @@ -18,6 +19,7 @@ interface HomePageProps { export default function HomePage({ postsService, mediaService }: HomePageProps) { const { user } = useUser() + useSaveSignupCodeToLocalStorage() const [isSubmitting, setIsSubmitting] = useState(false) const fetchPosts = useCallback( @@ -30,7 +32,11 @@ export default function HomePage({ postsService, mediaService }: HomePageProps) const { pages, setPages, loadNextPage } = useFeedViewModel(fetchPosts) const onCreatePost = useCallback( - async (content: string, files: { file: File; width: number; height: number }[], isPublic: boolean) => { + async ( + content: string, + files: { file: File; width: number; height: number }[], + isPublic: boolean, + ) => { setIsSubmitting(true) if (user == null) throw new Error('Not logged in') try { diff --git a/src/hooks/useSaveSignupCodeToLocalStorage.ts b/src/hooks/useSaveSignupCodeToLocalStorage.ts new file mode 100644 index 0000000..823824f --- /dev/null +++ b/src/hooks/useSaveSignupCodeToLocalStorage.ts @@ -0,0 +1,14 @@ +import { useSearchParams } from 'react-router-dom' +import { useEffect } from 'react' + +export function useSaveSignupCodeToLocalStorage() { + const [searchParams] = useSearchParams() + + const code = searchParams.get('ck') + + useEffect(() => { + if (code) { + localStorage.setItem('signupCode', code) + } + }, [code]) +} From 6a216d7dd12a7e99c8b17a3c44eb03db9474fbd8 Mon Sep 17 00:00:00 2001 From: john Date: Sun, 18 May 2025 22:58:24 +0200 Subject: [PATCH 06/87] chore(release): v1.6.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c4c909d..abbe806 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "femto-webapp", "private": true, - "version": "1.5.0", + "version": "1.6.0", "type": "module", "scripts": { "dev": "vite --host 0.0.0.0", From 21983c3f44a9b5e6d081fe8902f8deb8c5ca72fe Mon Sep 17 00:00:00 2001 From: john Date: Sun, 18 May 2025 23:07:31 +0200 Subject: [PATCH 07/87] fix code thing dang --- src/hooks/useSaveSignupCodeToLocalStorage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useSaveSignupCodeToLocalStorage.ts b/src/hooks/useSaveSignupCodeToLocalStorage.ts index 823824f..1ad9872 100644 --- a/src/hooks/useSaveSignupCodeToLocalStorage.ts +++ b/src/hooks/useSaveSignupCodeToLocalStorage.ts @@ -4,7 +4,7 @@ import { useEffect } from 'react' export function useSaveSignupCodeToLocalStorage() { const [searchParams] = useSearchParams() - const code = searchParams.get('ck') + const code = searchParams.get('c') useEffect(() => { if (code) { From 57e56dc33dbabf783632a455ee7e1bc62435181e Mon Sep 17 00:00:00 2001 From: john Date: Sun, 18 May 2025 23:08:20 +0200 Subject: [PATCH 08/87] chore(release): v1.7.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index abbe806..8f9a477 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "femto-webapp", "private": true, - "version": "1.6.0", + "version": "1.7.0", "type": "module", "scripts": { "dev": "vite --host 0.0.0.0", From 17c9885ccce4fcd800cda8f51a2d357f729c0f83 Mon Sep 17 00:00:00 2001 From: john Date: Mon, 19 May 2025 09:23:15 +0200 Subject: [PATCH 09/87] refresh user --- scripts/generate-schema.mjs | 4 +- src/App.tsx | 31 ++++++++------- src/app/api/schema.ts | 52 +++++++++++++++++++++++-- src/app/auth/authService.ts | 31 +++++++++++++++ src/app/auth/components/RefreshUser.tsx | 27 +++++++++++++ src/app/messageBus/messageTypes.ts | 2 + 6 files changed, 127 insertions(+), 20 deletions(-) create mode 100644 src/app/auth/components/RefreshUser.tsx diff --git a/scripts/generate-schema.mjs b/scripts/generate-schema.mjs index 4381bbf..edc8972 100644 --- a/scripts/generate-schema.mjs +++ b/scripts/generate-schema.mjs @@ -14,9 +14,7 @@ export async function generateApiSchema(openapiUrl, outputFilePath, pathToPretti const request = new Request(openapiUrl) const response = await fetch(request) const json = await response.text() - const ast = await openapiTS(json, { - pathParamsAsTypes: true, - }) + const ast = await openapiTS(json, {}) const prettierConfig = await resolveConfig(pathToPrettierRc, { useCache: true, }) diff --git a/src/App.tsx b/src/App.tsx index 74cc867..6f04dca 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,7 @@ import LogoutPage from './app/auth/pages/LogoutPage.tsx' import UnauthorizedHandler from './app/auth/components/UnauthorizedHandler.tsx' import AdminPage from './app/admin/pages/AdminPage.tsx' import SignupCodesManagementPage from './app/admin/pages/subpages/SignupCodesManagementPage.tsx' +import RefreshUser from './app/auth/components/RefreshUser.tsx' function App() { const postService = new PostsService() @@ -19,22 +20,24 @@ function App() { return ( - - } - /> - } /> - } /> - } /> - } /> - }> + + } + path={'/'} + element={} /> - - + } /> + } /> + } /> + } /> + }> + } + /> + + + ) diff --git a/src/app/api/schema.ts b/src/app/api/schema.ts index 32d4ad8..f476eb0 100644 --- a/src/app/api/schema.ts +++ b/src/app/api/schema.ts @@ -87,8 +87,7 @@ export interface paths { requestBody: { content: { 'multipart/form-data': { - /** Format: binary */ - file?: string + file?: components['schemas']['IFormFile'] } } } @@ -112,7 +111,7 @@ export interface paths { patch?: never trace?: never } - [path: `/media/${string}`]: { + '/media/{id}': { parameters: { query?: never header?: never @@ -266,6 +265,45 @@ export interface paths { patch?: never trace?: never } + '/auth/user/{userId}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get: { + parameters: { + query?: never + header?: never + path: { + userId: string + } + cookie?: never + } + requestBody?: never + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': components['schemas']['RefreshUserResult'] + 'application/json': components['schemas']['RefreshUserResult'] + 'text/json': components['schemas']['RefreshUserResult'] + } + } + } + } + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } '/auth/signup-codes': { parameters: { query?: never @@ -362,6 +400,8 @@ export interface components { /** Format: uuid */ next: string | null } + /** Format: binary */ + IFormFile: string ListSignupCodesResult: { signupCodes: components['schemas']['SignupCodeDto'][] } @@ -397,6 +437,12 @@ export interface components { /** Format: int32 */ height: number | null } + RefreshUserResult: { + /** Format: uuid */ + userId: string + username: string + isSuperUser: boolean + } RegisterRequest: { username: string password: string diff --git a/src/app/auth/authService.ts b/src/app/auth/authService.ts index a5733d6..293c242 100644 --- a/src/app/auth/authService.ts +++ b/src/app/auth/authService.ts @@ -63,4 +63,35 @@ export class AuthService { return res.data.signupCodes.map(SignupCode.fromDto) } + + async refreshUser(userId: string) { + if (this.getCookie('hasSession') !== 'true') { + return + } + + const res = await client.GET(`/auth/user/{userId}`, { + params: { + path: { userId }, + }, + credentials: 'include', + }) + + if (!res.data) { + dispatchMessage('auth:user-refresh-failed', null) + } else { + dispatchMessage('auth:user-refreshed', { ...res.data }) + } + } + + private getCookie(cookieName: string): string | undefined { + const cookie = document.cookie + .split('; ') + .map((c) => { + const [name, value] = c.split('=') + return { name, value } + }) + .find((c) => c.name === cookieName) + + return cookie?.value + } } diff --git a/src/app/auth/components/RefreshUser.tsx b/src/app/auth/components/RefreshUser.tsx new file mode 100644 index 0000000..34dd268 --- /dev/null +++ b/src/app/auth/components/RefreshUser.tsx @@ -0,0 +1,27 @@ +import { PropsWithChildren, useEffect, useRef } from 'react' +import { AuthService } from '../authService.ts' +import { useUser } from '../../user/userStore.ts' + +interface RefreshUserProps { + authService: AuthService +} + +export default function RefreshUser({ + authService, + children, +}: PropsWithChildren) { + const { user } = useUser() + const didRefresh = useRef(false) + + useEffect(() => { + const timeoutId = setTimeout(async () => { + if (didRefresh.current) return + if (user == null) return + didRefresh.current = true + await authService.refreshUser(user.userId) + }) + return () => clearTimeout(timeoutId) + }, [authService, user]) + + return <>{children} +} diff --git a/src/app/messageBus/messageTypes.ts b/src/app/messageBus/messageTypes.ts index 8d15f6c..080964e 100644 --- a/src/app/messageBus/messageTypes.ts +++ b/src/app/messageBus/messageTypes.ts @@ -5,4 +5,6 @@ export interface MessageTypes { 'auth:registered': User 'auth:logged-out': null 'auth:unauthorized': null + 'auth:user-refreshed': User + 'auth:user-refresh-failed': null } From 5f47162a5068e9629ef1fbc4a75bdbce8de1c5e8 Mon Sep 17 00:00:00 2001 From: john Date: Mon, 19 May 2025 17:52:18 +0200 Subject: [PATCH 10/87] fix user refresher --- src/App.tsx | 65 +++++++++++++++++++++---------- src/app/auth/authService.ts | 11 ++---- src/app/auth/pages/SignupPage.tsx | 4 +- src/app/user/userStore.ts | 4 +- src/hooks/useOnMounted.ts | 17 ++++++++ src/utils/store.ts | 20 ++++++++-- 6 files changed, 85 insertions(+), 36 deletions(-) create mode 100644 src/hooks/useOnMounted.ts diff --git a/src/App.tsx b/src/App.tsx index 6f04dca..f7a78c6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,37 +10,60 @@ import LogoutPage from './app/auth/pages/LogoutPage.tsx' import UnauthorizedHandler from './app/auth/components/UnauthorizedHandler.tsx' import AdminPage from './app/admin/pages/AdminPage.tsx' import SignupCodesManagementPage from './app/admin/pages/subpages/SignupCodesManagementPage.tsx' -import RefreshUser from './app/auth/components/RefreshUser.tsx' +import { useUser } from './app/user/userStore.ts' +import { useEffect, useMemo } from 'react' -function App() { +export default function App() { const postService = new PostsService() const mediaService = new MediaService() - const authService = new AuthService() + const authService = useMemo(() => new AuthService(), []) + + const { user, setUser } = useUser() + + const userId = user?.userId ?? null + + useEffect(() => { + if (userId == null) { + return + } + + const timeouts: number[] = [] + + timeouts.push( + setTimeout(async function refreshUser() { + const userInfo = await authService.refreshUser(userId) + + setUser(userInfo) + + timeouts.push(setTimeout(refreshUser, 60_000)) + }), + ) + + return () => { + timeouts.forEach(clearTimeout) + } + }, [authService, setUser, userId]) return ( - - + + } + /> + } /> + } /> + } /> + } /> + }> } + path={'codes'} + element={} /> - } /> - } /> - } /> - } /> - }> - } - /> - - - + + ) } - -export default App diff --git a/src/app/auth/authService.ts b/src/app/auth/authService.ts index 293c242..bdd721c 100644 --- a/src/app/auth/authService.ts +++ b/src/app/auth/authService.ts @@ -2,6 +2,7 @@ import { dispatchMessage } from '../messageBus/messageBus.ts' import client from '../api/client.ts' import { ProblemDetails } from '../../types' import { SignupCode } from './signupCode.ts' +import { User } from '../user/userStore.ts' export class AuthService { constructor() {} @@ -64,9 +65,9 @@ export class AuthService { return res.data.signupCodes.map(SignupCode.fromDto) } - async refreshUser(userId: string) { + async refreshUser(userId: string): Promise { if (this.getCookie('hasSession') !== 'true') { - return + return null } const res = await client.GET(`/auth/user/{userId}`, { @@ -76,11 +77,7 @@ export class AuthService { credentials: 'include', }) - if (!res.data) { - dispatchMessage('auth:user-refresh-failed', null) - } else { - dispatchMessage('auth:user-refreshed', { ...res.data }) - } + return res.data ?? null } private getCookie(cookieName: string): string | undefined { diff --git a/src/app/auth/pages/SignupPage.tsx b/src/app/auth/pages/SignupPage.tsx index ba3b994..19b2ab4 100644 --- a/src/app/auth/pages/SignupPage.tsx +++ b/src/app/auth/pages/SignupPage.tsx @@ -52,9 +52,7 @@ export default function SignupPage({ authService }: SignupPageProps) { } }, [code, signupCode]) - useEffect(() => { - console.debug('signup code', signupCode) - }, [signupCode]) + useEffect(() => {}, [signupCode]) const onSubmit = async (e: FormEvent) => { e.preventDefault() diff --git a/src/app/user/userStore.ts b/src/app/user/userStore.ts index 49107fc..f52d391 100644 --- a/src/app/user/userStore.ts +++ b/src/app/user/userStore.ts @@ -23,9 +23,9 @@ addMessageListener('auth:registered', setUser) addMessageListener('auth:logged-out', setUser) export const useUser = () => { - const [user] = useStore(userStore) + const [user, setUser] = useStore(userStore) - return { user } + return { user, setUser } } function loadStoredUser(): User | null { diff --git a/src/hooks/useOnMounted.ts b/src/hooks/useOnMounted.ts new file mode 100644 index 0000000..2053977 --- /dev/null +++ b/src/hooks/useOnMounted.ts @@ -0,0 +1,17 @@ +import { useEffect, useRef } from 'react' + +export function useOnMounted(callback: () => void | Promise) { + const isMounted = useRef(false) + + useEffect(() => { + if (isMounted.current) return + isMounted.current = true + + const timeoutId = setTimeout(callback) + + return () => { + isMounted.current = false + clearTimeout(timeoutId) + } + }, [callback]) +} diff --git a/src/utils/store.ts b/src/utils/store.ts index 5db61dc..ae99ef7 100644 --- a/src/utils/store.ts +++ b/src/utils/store.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' export interface Store { getState: () => T @@ -37,7 +37,21 @@ export function createStore(initialState: T): Store export function useStore(store: Store) { const [selectedState, setSelectedState] = useState(() => store.getState()) - useEffect(() => store.subscribe((newState) => setSelectedState(newState)), [store]) + useEffect(() => { + const unsubscribe = store.subscribe((newState) => setSelectedState(newState)) - return [selectedState, setSelectedState] as const + return () => { + unsubscribe() + } + }, [store]) + + const setState = useCallback( + (nextState: T | ((prevState: T) => T)) => { + setSelectedState(nextState) + store.setState(nextState) + }, + [store], + ) + + return [selectedState, setState] as const } From 700eaf3eb20a500f73f5bc426a7b15828b2341dd Mon Sep 17 00:00:00 2001 From: john Date: Tue, 20 May 2025 10:06:18 +0200 Subject: [PATCH 11/87] use user from session --- src/App.tsx | 40 ++++-------------- src/app/auth/authService.ts | 26 ++++-------- src/app/auth/components/AuthNavButtons.tsx | 4 +- src/app/auth/components/Protected.tsx | 4 +- src/app/auth/components/RefreshUser.tsx | 4 +- src/app/auth/cookies.ts | 16 ++++++++ src/app/auth/pages/LoginPage.tsx | 4 +- src/app/auth/pages/LogoutPage.tsx | 5 ++- src/app/feed/pages/HomePage.tsx | 4 +- src/app/femtoApp.ts | 8 ++++ src/app/messageBus/messageTypes.ts | 10 ++--- src/app/user/user.ts | 48 ++++++++++++++++++++++ src/app/user/userStore.ts | 38 ----------------- src/components/NavBar.tsx | 4 +- src/types.d.ts | 12 ++++++ src/useRefreshSessionLoop.ts | 28 +++++++++++++ 16 files changed, 148 insertions(+), 107 deletions(-) create mode 100644 src/app/auth/cookies.ts create mode 100644 src/app/femtoApp.ts create mode 100644 src/app/user/user.ts delete mode 100644 src/app/user/userStore.ts create mode 100644 src/useRefreshSessionLoop.ts diff --git a/src/App.tsx b/src/App.tsx index f7a78c6..3e0a263 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,39 +10,17 @@ import LogoutPage from './app/auth/pages/LogoutPage.tsx' import UnauthorizedHandler from './app/auth/components/UnauthorizedHandler.tsx' import AdminPage from './app/admin/pages/AdminPage.tsx' import SignupCodesManagementPage from './app/admin/pages/subpages/SignupCodesManagementPage.tsx' -import { useUser } from './app/user/userStore.ts' -import { useEffect, useMemo } from 'react' +import { initUser } from './app/user/user.ts' +import { useRefreshSessionLoop } from './useRefreshSessionLoop.ts' + +const postService = new PostsService() +const mediaService = new MediaService() +const authService = new AuthService() + +initUser() export default function App() { - const postService = new PostsService() - const mediaService = new MediaService() - const authService = useMemo(() => new AuthService(), []) - - const { user, setUser } = useUser() - - const userId = user?.userId ?? null - - useEffect(() => { - if (userId == null) { - return - } - - const timeouts: number[] = [] - - timeouts.push( - setTimeout(async function refreshUser() { - const userInfo = await authService.refreshUser(userId) - - setUser(userInfo) - - timeouts.push(setTimeout(refreshUser, 60_000)) - }), - ) - - return () => { - timeouts.forEach(clearTimeout) - } - }, [authService, setUser, userId]) + useRefreshSessionLoop(authService) return ( diff --git a/src/app/auth/authService.ts b/src/app/auth/authService.ts index bdd721c..3557c52 100644 --- a/src/app/auth/authService.ts +++ b/src/app/auth/authService.ts @@ -2,7 +2,7 @@ import { dispatchMessage } from '../messageBus/messageBus.ts' import client from '../api/client.ts' import { ProblemDetails } from '../../types' import { SignupCode } from './signupCode.ts' -import { User } from '../user/userStore.ts' +import { getCookie } from './cookies.ts' export class AuthService { constructor() {} @@ -17,7 +17,7 @@ export class AuthService { throw new Error('invalid credentials') } - dispatchMessage('auth:logged-in', { ...res.data }) + dispatchMessage('auth:logged-in', null) } async signup(username: string, password: string, signupCode: string) { @@ -31,7 +31,7 @@ export class AuthService { throw new Error((res.error as ProblemDetails)?.detail ?? 'invalid credentials') } - dispatchMessage('auth:registered', { ...res.data }) + dispatchMessage('auth:registered', null) } async logout() { @@ -65,30 +65,18 @@ export class AuthService { return res.data.signupCodes.map(SignupCode.fromDto) } - async refreshUser(userId: string): Promise { - if (this.getCookie('hasSession') !== 'true') { + async refreshUser(userId: string) { + if (getCookie('hasSession') !== 'true') { return null } - const res = await client.GET(`/auth/user/{userId}`, { + await client.GET(`/auth/user/{userId}`, { params: { path: { userId }, }, credentials: 'include', }) - return res.data ?? null - } - - private getCookie(cookieName: string): string | undefined { - const cookie = document.cookie - .split('; ') - .map((c) => { - const [name, value] = c.split('=') - return { name, value } - }) - .find((c) => c.name === cookieName) - - return cookie?.value + dispatchMessage('auth:refreshed', null) } } diff --git a/src/app/auth/components/AuthNavButtons.tsx b/src/app/auth/components/AuthNavButtons.tsx index 33769c1..6e58d01 100644 --- a/src/app/auth/components/AuthNavButtons.tsx +++ b/src/app/auth/components/AuthNavButtons.tsx @@ -1,9 +1,9 @@ -import { useUser } from '../../user/userStore.ts' +import { useUser } from '../../user/user.ts' import NavButton from '../../../components/buttons/NavButton.tsx' import { useLocation } from 'react-router-dom' export default function AuthNavButtons() { - const { user } = useUser() + const user = useUser() const { pathname } = useLocation() diff --git a/src/app/auth/components/Protected.tsx b/src/app/auth/components/Protected.tsx index ad72109..f5ea238 100644 --- a/src/app/auth/components/Protected.tsx +++ b/src/app/auth/components/Protected.tsx @@ -1,9 +1,9 @@ -import { useUser } from '../../user/userStore.ts' +import { useUser } from '../../user/user.ts' import { useNavigate, Outlet } from 'react-router-dom' import { useEffect } from 'react' export default function Protected() { - const { user } = useUser() + const user = useUser() const navigate = useNavigate() diff --git a/src/app/auth/components/RefreshUser.tsx b/src/app/auth/components/RefreshUser.tsx index 34dd268..b49bb1e 100644 --- a/src/app/auth/components/RefreshUser.tsx +++ b/src/app/auth/components/RefreshUser.tsx @@ -1,6 +1,6 @@ import { PropsWithChildren, useEffect, useRef } from 'react' import { AuthService } from '../authService.ts' -import { useUser } from '../../user/userStore.ts' +import { useUser } from '../../user/user.ts' interface RefreshUserProps { authService: AuthService @@ -10,7 +10,7 @@ export default function RefreshUser({ authService, children, }: PropsWithChildren) { - const { user } = useUser() + const user = useUser() const didRefresh = useRef(false) useEffect(() => { diff --git a/src/app/auth/cookies.ts b/src/app/auth/cookies.ts new file mode 100644 index 0000000..ba861c7 --- /dev/null +++ b/src/app/auth/cookies.ts @@ -0,0 +1,16 @@ +export function getCookie(cookieName: string): string | undefined { + return getCookies().get(cookieName) +} + +export function getCookies(): Map { + return document.cookie + .split('; ') + .map((c) => { + const [name, value] = c.split('=') as [string, string] + return { name, value } + }) + .reduce((acc, c) => { + acc.set(c.name, c.value) + return acc + }, new Map()) +} diff --git a/src/app/auth/pages/LoginPage.tsx b/src/app/auth/pages/LoginPage.tsx index 954a2cf..5163d71 100644 --- a/src/app/auth/pages/LoginPage.tsx +++ b/src/app/auth/pages/LoginPage.tsx @@ -4,7 +4,7 @@ import TextInput from '../../../components/inputs/TextInput.tsx' import Button from '../../../components/buttons/Button.tsx' import { AuthService } from '../authService.ts' import { useNavigate } from 'react-router-dom' -import { useUser } from '../../user/userStore.ts' +import { useUser } from '../../user/user.ts' import NavBar from '../../../components/NavBar.tsx' import NavButton from '../../../components/buttons/NavButton.tsx' import LinkButton from '../../../components/buttons/LinkButton.tsx' @@ -22,7 +22,7 @@ export default function LoginPage({ authService }: LoginPageProps) { const passwordInputRef = useRef(null) const navigate = useNavigate() - const { user } = useUser() + const user = useUser() useEffect(() => { if (user) { diff --git a/src/app/auth/pages/LogoutPage.tsx b/src/app/auth/pages/LogoutPage.tsx index 8ae4ef4..b0e20b6 100644 --- a/src/app/auth/pages/LogoutPage.tsx +++ b/src/app/auth/pages/LogoutPage.tsx @@ -1,7 +1,7 @@ import { useNavigate } from 'react-router-dom' import { AuthService } from '../authService.ts' import { useEffect } from 'react' -import { useUser } from '../../user/userStore.ts' +import { useUser } from '../../user/user.ts' interface LogoutPageProps { authService: AuthService @@ -9,9 +9,10 @@ interface LogoutPageProps { export default function LogoutPage({ authService }: LogoutPageProps) { const navigate = useNavigate() - const { user } = useUser() + const user = useUser() useEffect(() => { + console.debug(user) if (!user) { navigate('/login') } diff --git a/src/app/feed/pages/HomePage.tsx b/src/app/feed/pages/HomePage.tsx index 5ba5d27..7abd10e 100644 --- a/src/app/feed/pages/HomePage.tsx +++ b/src/app/feed/pages/HomePage.tsx @@ -1,7 +1,7 @@ import { useCallback, useState } from 'react' import FeedView from '../components/FeedView.tsx' import { PostsService } from '../posts/postsService.ts' -import { useUser } from '../../user/userStore.ts' +import { useUser } from '../../user/user.ts' import { MediaService } from '../../media/mediaService.ts' import NewPostWidget from '../../../components/NewPostWidget.tsx' import { useFeedViewModel } from '../components/FeedView.ts' @@ -18,7 +18,7 @@ interface HomePageProps { } export default function HomePage({ postsService, mediaService }: HomePageProps) { - const { user } = useUser() + const user = useUser() useSaveSignupCodeToLocalStorage() const [isSubmitting, setIsSubmitting] = useState(false) diff --git a/src/app/femtoApp.ts b/src/app/femtoApp.ts new file mode 100644 index 0000000..cc9593a --- /dev/null +++ b/src/app/femtoApp.ts @@ -0,0 +1,8 @@ +import { FemtoApp } from '../types' +import { produce } from 'immer' + +export function setGlobal(k: K, v: FemtoApp[K]) { + window.$femto = produce(window.$femto ?? {}, (draft) => { + draft[k] = v + }) +} diff --git a/src/app/messageBus/messageTypes.ts b/src/app/messageBus/messageTypes.ts index 080964e..2338618 100644 --- a/src/app/messageBus/messageTypes.ts +++ b/src/app/messageBus/messageTypes.ts @@ -1,10 +1,10 @@ -import { User } from '../user/userStore.ts' +import { User } from '../user/user.ts' export interface MessageTypes { - 'auth:logged-in': User - 'auth:registered': User + 'auth:logged-in': null + 'auth:registered': null 'auth:logged-out': null 'auth:unauthorized': null - 'auth:user-refreshed': User - 'auth:user-refresh-failed': null + 'auth:refreshed': null + 'user:updated': User | null } diff --git a/src/app/user/user.ts b/src/app/user/user.ts new file mode 100644 index 0000000..a962607 --- /dev/null +++ b/src/app/user/user.ts @@ -0,0 +1,48 @@ +import { addMessageListener, dispatchMessage } from '../messageBus/messageBus.ts' +import { getCookie } from '../auth/cookies.ts' +import { useMessageListener } from '../../hooks/useMessageListener.ts' +import { useState } from 'react' +import { setGlobal } from '../femtoApp.ts' + +export interface User { + userId: string + username: string + isSuperUser: boolean +} + +let globalUser: User | null + +export function initUser() { + updateUser() +} + +function updateUser() { + globalUser = getUserFromCookie() + console.debug(globalUser) + setGlobal('user', globalUser) + dispatchMessage('user:updated', globalUser) +} + +addMessageListener('auth:logged-in', updateUser) +addMessageListener('auth:registered', updateUser) +addMessageListener('auth:logged-out', updateUser) +addMessageListener('auth:refreshed', updateUser) + +export function useUser(): User | null { + const [user, setUser] = useState(globalUser) + + useMessageListener('user:updated', (u) => { + setUser(u) + }) + + return user +} + +function getUserFromCookie(): User | null { + const userCookie = getCookie('user') + + if (!userCookie) return null + + // TODO validate but it should be fine + return JSON.parse(decodeURIComponent(userCookie)) as User +} diff --git a/src/app/user/userStore.ts b/src/app/user/userStore.ts deleted file mode 100644 index f52d391..0000000 --- a/src/app/user/userStore.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { createStore, Store, useStore } from '../../utils/store.ts' -import { addMessageListener } from '../messageBus/messageBus.ts' - -export interface User { - userId: string - username: string - isSuperUser: boolean -} - -export type UserStore = Store - -const UserKey = 'user' - -export const userStore = createStore(loadStoredUser()) - -userStore.subscribe((user) => { - localStorage.setItem(UserKey, JSON.stringify(user)) -}) - -const setUser = (u: User | null) => userStore.setState(u) -addMessageListener('auth:logged-in', setUser) -addMessageListener('auth:registered', setUser) -addMessageListener('auth:logged-out', setUser) - -export const useUser = () => { - const [user, setUser] = useStore(userStore) - - return { user, setUser } -} - -function loadStoredUser(): User | null { - const json = localStorage.getItem(UserKey) - if (json) { - return JSON.parse(json) as User - } else { - return null - } -} diff --git a/src/components/NavBar.tsx b/src/components/NavBar.tsx index 5dc99dd..8127122 100644 --- a/src/components/NavBar.tsx +++ b/src/components/NavBar.tsx @@ -1,11 +1,11 @@ import { PropsWithChildren } from 'react' -import { useUser } from '../app/user/userStore.ts' +import { useUser } from '../app/user/user.ts' import NavButton from './buttons/NavButton.tsx' type NavBarProps = unknown export default function NavBar({ children }: PropsWithChildren) { - const { user } = useUser() + const user = useUser() return (