auth
This commit is contained in:
parent
4573048a47
commit
d4a1492d56
16 changed files with 463 additions and 98 deletions
22
src/App.tsx
22
src/App.tsx
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
159
src/api/api.ts
159
src/api/api.ts
|
@ -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> {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
22
src/auth/LogoutPage.tsx
Normal 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 <></>
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
|
@ -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', {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>{' '}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
|
@ -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)
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
12
src/messageBus/addMessageListener.ts
Normal file
12
src/messageBus/addMessageListener.ts
Normal 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 }))
|
||||||
|
}
|
13
src/messageBus/messageTypes.ts
Normal file
13
src/messageBus/messageTypes.ts
Normal 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': {}
|
||||||
|
}
|
|
@ -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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue