This commit is contained in:
john 2025-05-06 18:13:12 +02:00
parent 4573048a47
commit d4a1492d56
16 changed files with 463 additions and 98 deletions

View file

@ -1,16 +1,21 @@
import { BrowserRouter, Route, Routes } from 'react-router' import { BrowserRouter, Route, Routes } from 'react-router'
import HomePage from './pages/HomePage.tsx' import HomePage from './feed/HomePage.tsx'
import { PostsService } from './feed/models/posts/postsService.ts' import { PostsService } from './feed/models/posts/postsService.ts'
import AuthorPage from './pages/AuthorPage.tsx' import AuthorPage from './feed/AuthorPage.tsx'
import { MediaService } from './model/media/mediaService.ts' import { MediaService } from './model/media/mediaService.ts'
import SignupPage from './pages/SignupPage.tsx' import SignupPage from './auth/SignupPage.tsx'
import LoginPage from './pages/LoginPage.tsx' import LoginPage from './auth/LoginPage.tsx'
import { AuthService } from './auth/authService.ts' import { AuthService } from './auth/authService.ts'
import { useUser } from './store/userStore.ts'
import LogoutPage from './auth/LogoutPage.tsx'
import { ApiImpl } from './api/api.ts'
function App() { function App() {
const postService = new PostsService() const [user] = useUser()
const mediaService = new MediaService() const api = new ApiImpl()
const authService = new AuthService() const postService = new PostsService(api)
const mediaService = new MediaService(api)
const authService = new AuthService(api, user)
return ( return (
<BrowserRouter> <BrowserRouter>
@ -21,7 +26,8 @@ function App() {
/> />
<Route path="/u/:username" element={<AuthorPage postsService={postService} />} /> <Route path="/u/:username" element={<AuthorPage postsService={postService} />} />
<Route path="/login" element={<LoginPage authService={authService} />} /> <Route path="/login" element={<LoginPage authService={authService} />} />
<Route path="/signup/:code?" element={<SignupPage />} /> <Route path="/logout" element={<LogoutPage authService={authService} />} />
<Route path="/signup/:code?" element={<SignupPage authService={authService} />} />
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
) )

View file

@ -1,57 +1,132 @@
import { components } from './schema.ts' import { components } from './schema.ts'
// TODO for now we just assume that the API and client are running on the same host export interface Api {
// i think this might change but dependes on what we do with deployment readonly auth: AuthApi
const ApiHost = `http://${location.hostname}:5181`
console.debug('API HOST IS', ApiHost) loadPublicFeed(
cursor: string | null,
amount: number | null,
author: string | null,
): Promise<components['schemas']['GetAllPublicPostsResponse']>
export async function loadPublicFeed( uploadMedia(file: File): Promise<components['schemas']['UploadMediaResponse']>
cursor: string | null,
amount: number | null,
author: string | null,
): Promise<components['schemas']['GetAllPublicPostsResponse']> {
const url = new URL(`posts`, ApiHost)
if (amount != null) url.searchParams.set('amount', amount.toString())
if (cursor != null) url.searchParams.set('from', cursor)
if (author != null) url.searchParams.set('author', author)
const res = await doGetRequest(url) createPost(
authorId: string,
return res as components['schemas']['GetAllPublicPostsResponse'] content: string,
media: components['schemas']['CreatePostRequestMedia'][],
): Promise<components['schemas']['CreatePostResponse']>
} }
export async function uploadMedia( export interface AuthApi {
file: File, login(username: string, password: string): Promise<components['schemas']['LoginResponse']>
): Promise<components['schemas']['UploadMediaResponse']> {
const url = new URL('media', ApiHost)
const body = new FormData()
body.append('file', file)
const response = await fetch(
new Request(url, {
method: 'POST',
body: body,
}),
)
if (!response.ok) throw new Error(await response.text()) signup(
return await response.json() username: string,
password: string,
signupCode: string,
email: string | null,
): Promise<components['schemas']['SignupResponse']>
deleteSession(sessionToken: string): Promise<void>
} }
export async function createPost( export class ApiImpl implements Api {
authorId: string, private readonly apiHost: string
content: string,
media: components['schemas']['CreatePostRequestMedia'][], public get auth(): AuthApi {
): Promise<components['schemas']['CreatePostResponse']> { return new AuthApiImpl(this.apiHost)
const url = new URL('posts', ApiHost)
const body: components['schemas']['CreatePostRequest'] = {
authorId,
content,
media,
} }
const res = await doPostRequest(url, body) constructor() {
return res as components['schemas']['CreatePostResponse'] // TODO for now we just assume that the API and client are running on the same host
// i think this might change but depends on what we do with deployment
this.apiHost = `http://${location.hostname}:5181`
console.debug('API HOST IS', this.apiHost)
}
async loadPublicFeed(
cursor: string | null,
amount: number | null,
author: string | null,
): Promise<components['schemas']['GetAllPublicPostsResponse']> {
const url = new URL(`posts`, this.apiHost)
if (amount != null) url.searchParams.set('amount', amount.toString())
if (cursor != null) url.searchParams.set('from', cursor)
if (author != null) url.searchParams.set('author', author)
const res = await doGetRequest(url)
return res as components['schemas']['GetAllPublicPostsResponse']
}
async uploadMedia(file: File): Promise<components['schemas']['UploadMediaResponse']> {
const url = new URL('media', this.apiHost)
const body = new FormData()
body.append('file', file)
const response = await fetch(
new Request(url, {
method: 'POST',
body: body,
}),
)
if (!response.ok) throw new Error(await response.text())
return await response.json()
}
async createPost(
authorId: string,
content: string,
media: components['schemas']['CreatePostRequestMedia'][],
): Promise<components['schemas']['CreatePostResponse']> {
const url = new URL('posts', this.apiHost)
const body: components['schemas']['CreatePostRequest'] = {
authorId,
content,
media,
}
const res = await doPostRequest(url, body)
return res as components['schemas']['CreatePostResponse']
}
}
class AuthApiImpl implements AuthApi {
constructor(private readonly apiHost: string) {}
async login(username: string, password: string): Promise<components['schemas']['LoginResponse']> {
const url = new URL('/auth/login', this.apiHost)
const body: components['schemas']['LoginRequest'] = {
username,
password,
}
const res = await doPostRequest(url, body)
return res as components['schemas']['LoginResponse']
}
async signup(
username: string,
password: string,
signupCode: string,
email: string | null,
): Promise<components['schemas']['SignupResponse']> {
const url = new URL('/auth/signup', this.apiHost)
const body: components['schemas']['SignupRequest'] = {
username,
password,
signupCode,
email,
}
const res = await doPostRequest(url, body)
return res as components['schemas']['SignupResponse']
}
async deleteSession(sessionToken: string): Promise<void> {
const url = new URL('/auth/delete-session', this.apiHost)
const body: components['schemas']['DeleteSessionRequest'] = {
sessionToken,
}
await doPostRequest(url, body)
}
} }
async function doGetRequest(url: URL): Promise<unknown> { async function doGetRequest(url: URL): Promise<unknown> {

View file

@ -147,6 +147,131 @@ export interface paths {
patch?: never patch?: never
trace?: never trace?: never
} }
'/auth/login': {
parameters: {
query?: never
header?: never
path?: never
cookie?: never
}
get?: never
put?: never
post: {
parameters: {
query?: never
header?: never
path?: never
cookie?: never
}
requestBody: {
content: {
'application/json': components['schemas']['LoginRequest']
'text/json': components['schemas']['LoginRequest']
'application/*+json': components['schemas']['LoginRequest']
}
}
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown
}
content: {
'text/plain': components['schemas']['LoginResponse']
'application/json': components['schemas']['LoginResponse']
'text/json': components['schemas']['LoginResponse']
}
}
}
}
delete?: never
options?: never
head?: never
patch?: never
trace?: never
}
'/auth/signup': {
parameters: {
query?: never
header?: never
path?: never
cookie?: never
}
get?: never
put?: never
post: {
parameters: {
query?: never
header?: never
path?: never
cookie?: never
}
requestBody: {
content: {
'application/json': components['schemas']['SignupRequest']
'text/json': components['schemas']['SignupRequest']
'application/*+json': components['schemas']['SignupRequest']
}
}
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown
}
content: {
'text/plain': components['schemas']['SignupResponse']
'application/json': components['schemas']['SignupResponse']
'text/json': components['schemas']['SignupResponse']
}
}
}
}
delete?: never
options?: never
head?: never
patch?: never
trace?: never
}
'/auth/delete-session': {
parameters: {
query?: never
header?: never
path?: never
cookie?: never
}
get?: never
put?: never
post: {
parameters: {
query?: never
header?: never
path?: never
cookie?: never
}
requestBody: {
content: {
'application/json': components['schemas']['DeleteSessionRequest']
'text/json': components['schemas']['DeleteSessionRequest']
'application/*+json': components['schemas']['DeleteSessionRequest']
}
}
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown
}
content?: never
}
}
}
delete?: never
options?: never
head?: never
patch?: never
trace?: never
}
} }
export type webhooks = Record<string, never> export type webhooks = Record<string, never>
export interface components { export interface components {
@ -172,11 +297,24 @@ export interface components {
/** Format: uuid */ /** Format: uuid */
postId: string postId: string
} }
DeleteSessionRequest: {
sessionToken: string
}
GetAllPublicPostsResponse: { GetAllPublicPostsResponse: {
posts: components['schemas']['PublicPostDto'][] posts: components['schemas']['PublicPostDto'][]
/** Format: uuid */ /** Format: uuid */
next: string | null next: string | null
} }
LoginRequest: {
username: string
password: string
}
LoginResponse: {
/** Format: uuid */
userId: string
username: string
sessionToken: string
}
PublicPostAuthorDto: { PublicPostAuthorDto: {
/** Format: uuid */ /** Format: uuid */
authorId: string authorId: string
@ -199,6 +337,18 @@ export interface components {
/** Format: int32 */ /** Format: int32 */
height: number | null height: number | null
} }
SignupRequest: {
username: string
password: string
signupCode: string
email: string | null
}
SignupResponse: {
/** Format: uuid */
userId: string
username: string
sessionToken: string
}
UploadMediaResponse: { UploadMediaResponse: {
/** Format: uuid */ /** Format: uuid */
mediaId: string mediaId: string

View file

@ -2,7 +2,8 @@ import { useRef, useState, FormEvent } from 'react'
import SingleColumnLayout from '../layouts/SingleColumnLayout.tsx' import SingleColumnLayout from '../layouts/SingleColumnLayout.tsx'
import TextInput from '../components/TextInput.tsx' import TextInput from '../components/TextInput.tsx'
import PrimaryButton from '../components/PrimaryButton.tsx' import PrimaryButton from '../components/PrimaryButton.tsx'
import { AuthService } from '../auth/authService.ts' import { AuthService } from './authService.ts'
import { useNavigate } from 'react-router'
interface LoginPageProps { interface LoginPageProps {
authService: AuthService authService: AuthService
@ -15,6 +16,7 @@ export default function LoginPage({ authService }: LoginPageProps) {
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const usernameInputRef = useRef<HTMLInputElement | null>(null) const usernameInputRef = useRef<HTMLInputElement | null>(null)
const passwordInputRef = useRef<HTMLInputElement | null>(null) const passwordInputRef = useRef<HTMLInputElement | null>(null)
const navigate = useNavigate()
const onSubmit = async (e: FormEvent<HTMLFormElement>) => { const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault() e.preventDefault()
@ -34,7 +36,8 @@ export default function LoginPage({ authService }: LoginPageProps) {
setIsSubmitting(true) setIsSubmitting(true)
try { try {
const loginResult = await authService.login(username, password) await authService.login(username, password)
navigate('/')
} catch (error: unknown) { } catch (error: unknown) {
setError(error instanceof Error ? error.message : 'something went terribly wrong') setError(error instanceof Error ? error.message : 'something went terribly wrong')
} finally { } finally {

22
src/auth/LogoutPage.tsx Normal file
View file

@ -0,0 +1,22 @@
import { useNavigate } from 'react-router'
import { AuthService } from './authService.ts'
import { useEffect } from 'react'
interface LogoutPageProps {
authService: AuthService
}
export default function LogoutPage({ authService }: LogoutPageProps) {
const navigate = useNavigate()
useEffect(() => {
const timeout = setTimeout(async () => {
await authService.logout()
navigate('/')
})
return () => clearTimeout(timeout)
}, [authService, navigate])
return <></>
}

View file

@ -1,14 +1,19 @@
import { useParams } from 'react-router' import { useNavigate, useParams } from 'react-router'
import { useEffect, useRef, useState, FormEvent, useCallback, Ref } from 'react' import { useEffect, useRef, useState, FormEvent, useCallback, Ref } from 'react'
import SingleColumnLayout from '../layouts/SingleColumnLayout.tsx' import SingleColumnLayout from '../layouts/SingleColumnLayout.tsx'
import TextInput from '../components/TextInput.tsx' import TextInput from '../components/TextInput.tsx'
import PrimaryButton from '../components/PrimaryButton.tsx' import PrimaryButton from '../components/PrimaryButton.tsx'
import PrimaryLinkButton from '../components/PrimaryLinkButton.tsx' import PrimaryLinkButton from '../components/PrimaryLinkButton.tsx'
import { invalid, valid, Validation } from '../utils/validation.ts' import { invalid, valid, Validation } from '../utils/validation.ts'
import { AuthService } from './authService.ts'
const SignupCodeKey = 'signupCode' const SignupCodeKey = 'signupCode'
export default function SignupPage() { interface SignupPageProps {
authService: AuthService
}
export default function SignupPage({ authService }: SignupPageProps) {
const { code } = useParams() const { code } = useParams()
const [signupCode, setSignupCode] = useState<string | null>(null) const [signupCode, setSignupCode] = useState<string | null>(null)
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
@ -27,6 +32,8 @@ export default function SignupPage() {
const dialogRef = useRef<HTMLDialogElement | null>(null) const dialogRef = useRef<HTMLDialogElement | null>(null)
const navigate = useNavigate()
useEffect(() => { useEffect(() => {
if (signupCode) return if (signupCode) return
@ -47,6 +54,10 @@ export default function SignupPage() {
const onSubmit = async (e: FormEvent<HTMLFormElement>) => { const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault() e.preventDefault()
if (!signupCode) {
throw new Error("there's no code")
}
const isUsernameValid = validateUsername() const isUsernameValid = validateUsername()
const isEmailValid = validateEmail() const isEmailValid = validateEmail()
const isPasswordValid = validatePassword() const isPasswordValid = validatePassword()
@ -70,7 +81,8 @@ export default function SignupPage() {
setIsSubmitting(true) setIsSubmitting(true)
try { try {
// todo await authService.signup(username, email, password, signupCode)
navigate('/')
} finally { } finally {
setIsSubmitting(false) setIsSubmitting(false)
} }

View file

@ -1,9 +1,40 @@
import { User } from '../store/userStore.ts'
import { dispatchMessage } from '../messageBus/addMessageListener.ts'
import { Api } from '../api/api.ts'
export class AuthService { export class AuthService {
constructor(
private readonly api: Api,
private readonly user: User | null,
) {}
async login(username: string, password: string) { async login(username: string, password: string) {
throw new Error('not implemented') if (this.user != null) {
throw new Error('already logged in')
}
const res = await this.api.auth.login(username, password)
dispatchMessage('logged-in', { ...res })
} }
async signup(username: string, password: string, signupCode: string, email?: string) { async signup(username: string, password: string, signupCode: string, email?: string) {
throw new Error('not implemented') if (this.user != null) {
throw new Error('already logged in')
}
const res = await this.api.auth.signup(username, password, signupCode, email ?? null)
dispatchMessage('signed-up', { ...res })
}
async logout() {
if (this.user == null) {
return
}
await this.api.auth.deleteSession(this.user.sessionToken)
dispatchMessage('logged-out', {})
} }
} }

View file

@ -1,10 +1,31 @@
import { useUser } from '../store/userStore'
import NavLinkButton from './NavLinkButton' import NavLinkButton from './NavLinkButton'
export default function NavBar() { export default function NavBar() {
const [user] = useUser()
const loggedIn = user != null
return ( return (
<nav className={`w-full flex flex-row-reverse gap-4 px-4 md:px-8 py-0.5`}> <nav className={`w-full flex flex-row-reverse gap-4 px-4 md:px-8 py-0.5`}>
<NavLinkButton to="/signup">register</NavLinkButton> {loggedIn ? <LoggedInContent /> : <LoggedOutContent />}
<NavLinkButton to="/login">login</NavLinkButton>
</nav> </nav>
) )
} }
function LoggedInContent() {
return (
<>
<NavLinkButton to="/logout">logout</NavLinkButton>
</>
)
}
function LoggedOutContent() {
return (
<>
<NavLinkButton to="/signup">register</NavLinkButton>
<NavLinkButton to="/login">login</NavLinkButton>{' '}
</>
)
}

View file

@ -1,10 +1,10 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import FeedView from '../feed/FeedView.tsx' import FeedView from './FeedView.tsx'
import { PostsService } from '../feed/models/posts/postsService.ts' import { PostsService } from './models/posts/postsService.ts'
import { useParams } from 'react-router' import { useParams } from 'react-router'
import SingleColumnLayout from '../layouts/SingleColumnLayout.tsx' import SingleColumnLayout from '../layouts/SingleColumnLayout.tsx'
import NavBar from '../components/NavBar.tsx' import NavBar from '../components/NavBar.tsx'
import { useFeedViewModel } from '../feed/FeedView.ts' import { useFeedViewModel } from './FeedView.ts'
interface AuthorPageParams { interface AuthorPageParams {
postsService: PostsService postsService: PostsService

View file

@ -1,11 +1,11 @@
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import FeedView from '../feed/FeedView.tsx' import FeedView from './FeedView.tsx'
import { PostsService } from '../feed/models/posts/postsService.ts' import { PostsService } from './models/posts/postsService.ts'
import { useUserStore } from '../store/userStore.ts' import { useUser } from '../store/userStore.ts'
import { MediaService } from '../model/media/mediaService.ts' import { MediaService } from '../model/media/mediaService.ts'
import NewPostWidget from '../components/NewPostWidget.tsx' import NewPostWidget from '../components/NewPostWidget.tsx'
import { useFeedViewModel } from '../feed/FeedView.ts' import { useFeedViewModel } from './FeedView.ts'
import { Post } from '../feed/models/posts/posts.ts' import { Post } from './models/posts/posts.ts'
import { Temporal } from '@js-temporal/polyfill' import { Temporal } from '@js-temporal/polyfill'
import SingleColumnLayout from '../layouts/SingleColumnLayout.tsx' import SingleColumnLayout from '../layouts/SingleColumnLayout.tsx'
import NavBar from '../components/NavBar.tsx' import NavBar from '../components/NavBar.tsx'
@ -16,7 +16,7 @@ interface HomePageProps {
} }
export default function HomePage({ postsService, mediaService }: HomePageProps) { export default function HomePage({ postsService, mediaService }: HomePageProps) {
const [user] = useUserStore() const [user] = useUser()
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)

View file

@ -1,9 +1,10 @@
import { createPost, loadPublicFeed } from '../../../api/api.ts' import { Api } from '../../../api/api.ts'
import { Post } from './posts.ts' import { Post } from './posts.ts'
export class PostsService { export class PostsService {
constructor(private readonly api: Api) {}
async createNew(authorId: string, content: string, media: CreatePostMedia[]): Promise<string> { async createNew(authorId: string, content: string, media: CreatePostMedia[]): Promise<string> {
const { postId } = await createPost( const { postId } = await this.api.createPost(
authorId, authorId,
content, content,
media.map((m) => { media.map((m) => {
@ -14,7 +15,7 @@ export class PostsService {
} }
async loadPublicFeed(cursor: string | null, amount: number | null): Promise<Post[]> { async loadPublicFeed(cursor: string | null, amount: number | null): Promise<Post[]> {
const result = await loadPublicFeed(cursor, amount, null) const result = await this.api.loadPublicFeed(cursor, amount, null)
return result.posts.map((post) => Post.fromDto(post)) return result.posts.map((post) => Post.fromDto(post))
} }
@ -23,7 +24,7 @@ export class PostsService {
cursor: string | null, cursor: string | null,
amount: number | null, amount: number | null,
): Promise<Post[]> { ): Promise<Post[]> {
const result = await loadPublicFeed(cursor, amount, username) const result = await this.api.loadPublicFeed(cursor, amount, username)
return result.posts.map((post) => Post.fromDto(post)) return result.posts.map((post) => Post.fromDto(post))
} }
} }

View file

@ -0,0 +1,12 @@
import { MessageTypes } from './messageTypes.ts'
export function addMessageListener<E extends keyof MessageTypes>(
e: E,
listener: (message: MessageTypes[E]) => void,
) {
window.addEventListener(e, (event) => listener((event as CustomEvent).detail))
}
export function dispatchMessage<E extends keyof MessageTypes>(e: E, message: MessageTypes[E]) {
window.dispatchEvent(new CustomEvent(e, { detail: message }))
}

View file

@ -0,0 +1,13 @@
export interface MessageTypes {
'logged-in': {
userId: string
username: string
sessionToken: string
}
'signed-up': {
userId: string
username: string
sessionToken: string
}
'logged-out': {}
}

View file

@ -1,8 +1,9 @@
import { uploadMedia } from '../../api/api.ts' import { Api } from '../../api/api.ts'
export class MediaService { export class MediaService {
constructor(private readonly api: Api) {}
async uploadFile(file: File): Promise<{ mediaId: string; url: URL }> { async uploadFile(file: File): Promise<{ mediaId: string; url: URL }> {
const { mediaId, url } = await uploadMedia(file) const { mediaId, url } = await this.api.uploadMedia(file)
return { mediaId, url: new URL(url) } return { mediaId, url: new URL(url) }
} }
} }

View file

@ -1,8 +1,6 @@
// storeFactory.ts
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
interface Store<T> { export interface Store<T> {
useStore: () => [T, (nextState: T | ((prevState: T) => T)) => void]
getState: () => T getState: () => T
setState: (nextState: T | ((prevState: T) => T)) => void setState: (nextState: T | ((prevState: T) => T)) => void
subscribe: (listener: Listener<T>) => () => void subscribe: (listener: Listener<T>) => () => void
@ -33,13 +31,13 @@ export function createStore<T extends object | null>(initialState: T): Store<T>
} }
} }
function useStore(): [typeof state, typeof setState] { return { getState, setState, subscribe }
const [selectedState, setSelectedState] = useState(() => getState()) }
useEffect(() => subscribe((newState: T) => setSelectedState(newState)), []) export function useStore<T>(store: Store<T>) {
const [selectedState, setSelectedState] = useState(() => store.getState())
return [selectedState, setState]
} useEffect(() => store.subscribe((newState) => setSelectedState(newState)), [store])
return { useStore, getState, setState, subscribe } return [selectedState, setSelectedState] as const
} }

View file

@ -1,21 +1,41 @@
import { createStore } from './store.ts' import { createStore, Store, useStore } from './store.ts'
import { useState } from 'react' import { addMessageListener } from '../messageBus/addMessageListener.ts'
interface User { export interface User {
userId: string userId: string
username: string username: string
sessionToken: string
} }
let user: User | null = localStorage.getItem('user') export type UserStore = Store<User | null>
export function useUserStore() { const UserKey = 'user'
const [user, setUser] = useState<User | null>(user)
}
// todo not hardcode export const userStore = createStore<User | null>(loadStoredUser())
export const userStore = createStore<User | null>({
userId: '0196960c-6296-7532-ba66-8fabb38c6ae0', userStore.subscribe((user) => {
username: 'johnbotris', localStorage.setItem(UserKey, JSON.stringify(user))
}) })
export const useUserStore = userStore.useStore addMessageListener('logged-in', (e) => {
userStore.setState({
userId: e.userId,
username: e.username,
sessionToken: e.sessionToken,
})
})
addMessageListener('logged-out', () => {
userStore.setState(null)
})
export const useUser = () => useStore(userStore)
function loadStoredUser(): User | null {
const json = localStorage.getItem(UserKey)
if (json) {
return JSON.parse(json) as User
} else {
return null
}
}