diff --git a/api/app/Controllers/Http/SearchesController.ts b/api/app/Controllers/Http/SearchesController.ts index dec9734..bc40d5e 100644 --- a/api/app/Controllers/Http/SearchesController.ts +++ b/api/app/Controllers/Http/SearchesController.ts @@ -15,9 +15,11 @@ export default class SearchesController { } const illustrations = await Illustration.query() - .where('title', search) - .orWhere('content', 'LIKE', `%${search}%`) - .orWhere('author', 'LIKE', `%${search}%`) + .where((query) => { + query.where('title', search) + .orWhere('content', 'LIKE', `%${search}%`) + .orWhere('author', 'LIKE', `%${search}%`) + }) .andWhere('user_id', `${auth.user?.id}`) const tagSanitizedSearch = _.startCase(search).replace(/ /g, '-') const tags = await Tag.query().where('name',tagSanitizedSearch).andWhere('user_id', `${auth.user?.id}`) diff --git a/api/tests/functional/user.spec.ts b/api/tests/functional/user.spec.ts index 203fdcd..d8451b0 100644 --- a/api/tests/functional/user.spec.ts +++ b/api/tests/functional/user.spec.ts @@ -14,8 +14,8 @@ test.group('Users', (group) => { const user = await UserFactory.make() let fixedUser = { email: user.email, - password: user.password, - password_confirmation: user.password + password: user.password+"1A!a", + password_confirmation: user.password+"1A!a" } const response = await client.post('/register').json(fixedUser) @@ -24,7 +24,7 @@ test.group('Users', (group) => { response.assertBodyContains({uid: response.body().uid}) response.assertStatus(200) - const loggedInUser = await client.post('/login').json({email: user.email, password: user.password}) + const loggedInUser = await client.post('/login').json({email: user.email, password: user.password+"1A!a"}) const verify = await client.get(`/users/${response.body().uid}`).bearerToken(loggedInUser.body().token) verify.assertStatus(200) diff --git a/frontend/src/components/TagSelect.tsx b/frontend/src/components/TagSelect.tsx index b9a497a..1792cb2 100644 --- a/frontend/src/components/TagSelect.tsx +++ b/frontend/src/components/TagSelect.tsx @@ -50,7 +50,7 @@ export default function TagSelect({ defaultValue, token }:{ defaultValue: string const handleKeyPress = (event: KeyboardEvent) => { if (event.key === ",") { event.preventDefault() - if (inputRef.current) { + if (inputRef.current && (inputRef.current.value != "")) { handleTagAdd(inputRef.current.value) } } diff --git a/frontend/src/features/ui/reducer.ts b/frontend/src/features/ui/reducer.ts index 067f2f3..102f676 100644 --- a/frontend/src/features/ui/reducer.ts +++ b/frontend/src/features/ui/reducer.ts @@ -5,11 +5,13 @@ import type { AppState, AppThunk } from '@/store' export interface UIState { illustrationEdit: boolean updateUI: boolean + redirect: string } const initialState: UIState = { illustrationEdit: false, - updateUI: false + updateUI: false, + redirect: "/", } export const uiReducer = createSlice({ @@ -23,13 +25,17 @@ export const uiReducer = createSlice({ setUpdateUI: (state, actions) => { state.updateUI = actions.payload }, + setRedirect: (state, actions) => { + state.redirect = actions.payload + }, }, }) export const selectIllustrationEdit = (state: AppState) => state.ui.illustrationEdit export const selectUpdateUI = (state: AppState) => state.ui.updateUI +export const getRedirect = (state: AppState) => state.ui.redirect -export const { setIllustrationEdit, setUpdateUI } = uiReducer.actions +export const { setIllustrationEdit, setUpdateUI, setRedirect } = uiReducer.actions export default uiReducer.reducer diff --git a/frontend/src/library/useUser.ts b/frontend/src/library/useUser.ts index 4d06e5f..c198f54 100644 --- a/frontend/src/library/useUser.ts +++ b/frontend/src/library/useUser.ts @@ -2,17 +2,12 @@ import { useEffect } from 'react' import Router from 'next/router' import useSWR from 'swr' import { User } from '@/pages/api/user' -import { useAppSelector, useAppDispatch } from '@/hooks' - -import { setToken } from '@/features/user/reducer'; export default function useUser({ redirectTo = '', redirectIfFound = false, } = {}) { const { data: user, mutate: mutateUser } = useSWR('/api/user') - const dispatch = useAppDispatch() - // dispatch(setToken(user?.token)) // send token for api calls useEffect(() => { // if no redirect needed, just return (example: already on /dashboard) diff --git a/frontend/src/pages/author/[id].tsx b/frontend/src/pages/author/[id].tsx index d1248e9..b7e0fb1 100644 --- a/frontend/src/pages/author/[id].tsx +++ b/frontend/src/pages/author/[id].tsx @@ -7,7 +7,7 @@ import Link from 'next/link' import useUser from '@/library/useUser'; import Layout from '@/components/Layout'; import Head from 'next/head'; - +import { setRedirect } from '@/features/ui/reducer'; import { useAppSelector, useAppDispatch } from '@/hooks' @@ -24,6 +24,7 @@ export default function Author() { const [isLoading, setLoading] = useState(false) useEffect(() => { + if (!user?.token) dispatch(setRedirect(`/author/${name}`)) if (!name) { setLoading(true) return @@ -38,7 +39,7 @@ export default function Author() { }, [name]); - + if (!user?.token) return if (isLoading) return (
Loading...
diff --git a/frontend/src/pages/authors.tsx b/frontend/src/pages/authors.tsx index b7d9329..6baa910 100644 --- a/frontend/src/pages/authors.tsx +++ b/frontend/src/pages/authors.tsx @@ -1,12 +1,20 @@ import Author from '@/components/AuthorIndex'; import Layout from '@/components/Layout' import useUser from '@/library/useUser'; +import { setRedirect } from '@/features/ui/reducer'; +import { useAppDispatch } from '@/hooks' +import { useEffect } from 'react'; -export default function Home() { +export default function Authors() { const { user } = useUser({ redirectTo: '/login', }) + const dispatch = useAppDispatch() + useEffect(() => { + if (!user?.token) dispatch(setRedirect(`/authors`)) + }, [user]) + if (!user?.token) return return ( { user?.isLoggedIn && () } diff --git a/frontend/src/pages/illustration/[id].tsx b/frontend/src/pages/illustration/[id].tsx index 3794149..d980544 100644 --- a/frontend/src/pages/illustration/[id].tsx +++ b/frontend/src/pages/illustration/[id].tsx @@ -12,7 +12,7 @@ import { selectModal, setModal } from '@/features/modal/reducer' import { setFlashMessage } from '@/features/flash/reducer' import IllustrationForm from '@/components/IllustrationForm'; import { illustrationType } from '@/library/illustrationType'; -import { selectIllustrationEdit, setIllustrationEdit, selectUpdateUI, setUpdateUI } from '@/features/ui/reducer'; +import { selectIllustrationEdit, setIllustrationEdit, selectUpdateUI, setUpdateUI, setRedirect } from '@/features/ui/reducer'; import format from 'date-fns/format'; import PlaceConfirmDialog from '@/components/PlaceConfirmDialog'; import { placeType } from '@/library/placeType'; @@ -37,10 +37,11 @@ export default function IllustrationWrapper() { useEffect(() => { setLoading(true) - if (!router.query.id || !user) { + if (!user?.token) dispatch(setRedirect(`/illustration/${router.query.id}`)) + if (!router.query.id || !user?.token) { return } - dispatch(getThunkSettings(user?.token)) + dispatch(getThunkSettings(user.token)) api.get(`/illustration/${router.query.id}`, '', user.token) .then(data => { @@ -116,8 +117,8 @@ export default function IllustrationWrapper() { used: form.Used.value.trim(), } } - - if (!user?.token || !illustration || !userSettings) return Loading... + if (!user?.token) return + if (!illustration || !userSettings) return Loading... return ( @@ -127,19 +128,19 @@ export default function IllustrationWrapper() { : <> -
-
+
+
Title: {/* Title is required */} {illustration?.title}
{illustration?.author && -
+
Author: {illustration.author}
} - {illustration?.source &&
- Source: + {illustration?.source &&
+ Source: {isValidHttpUrl(illustration.source) ? {illustration.source} : illustration.source}
} {illustration?.tags &&
diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx index 14a50ac..aa2fabd 100644 --- a/frontend/src/pages/login.tsx +++ b/frontend/src/pages/login.tsx @@ -3,12 +3,16 @@ import useUser from '@/library/useUser' import Layout from '@/components/Layout' import Form from '@/components/Form' import fetchJson, { FetchError } from '@/library/fetchJson' - +import { useAppDispatch, useAppSelector } from '@/hooks' +import { getRedirect, setRedirect } from '@/features/ui/reducer'; export default function Login() { - // here we just check if user is already logged in and redirect to profile - // should be last page + + const dispatch = useAppDispatch() + + // retrieve first accessed path as unauthenticated user + const redirectPath = useAppSelector(getRedirect) const { mutateUser } = useUser({ - redirectTo: '/', + redirectTo: redirectPath, redirectIfFound: true, }) diff --git a/frontend/src/pages/new-illustration.tsx b/frontend/src/pages/new-illustration.tsx index 197ed3f..a549c24 100644 --- a/frontend/src/pages/new-illustration.tsx +++ b/frontend/src/pages/new-illustration.tsx @@ -2,15 +2,21 @@ import React, { useState } from 'react' import useUser from '@/library/useUser' import Layout from '@/components/Layout' import IllustrationForm from '@/components/IllustrationForm' +import { setRedirect } from '@/features/ui/reducer'; +import { useAppDispatch } from '@/hooks' +import { useEffect } from 'react'; export default function Login() { - // here we just check if user is already logged in and redirect to profile - // should be last page + const { user } = useUser({ - redirectTo: '/', + redirectTo: '/login', }) - + const dispatch = useAppDispatch() + useEffect(() => { + if (!user?.token) dispatch(setRedirect(`/new-illustration`)) + }, [user]) + if (!user?.token) return return ( diff --git a/frontend/src/pages/search.tsx b/frontend/src/pages/search.tsx index 7c4d39a..0440d76 100644 --- a/frontend/src/pages/search.tsx +++ b/frontend/src/pages/search.tsx @@ -9,6 +9,8 @@ import { tagType } from '@/library/tagtype' import { illustrationType } from '@/library/illustrationType' import { placeType } from '@/library/placeType' import { MagnifyingGlassCircleIcon } from '@heroicons/react/20/solid' +import { setRedirect } from '@/features/ui/reducer'; +import { useEffect } from 'react'; type dataReturn = { illustrations: any @@ -18,16 +20,18 @@ type dataReturn = { } export default function Search() { - // here we just check if user is already logged in and redirect to profile - // should be last page const { user } = useUser({ - redirectTo: '/', + redirectTo: '/login', }) const dispatch = useAppDispatch() const [data, setData] = useState(null) const [searched, setSearched] = useState('') + useEffect(() => { + if (!user?.token) dispatch(setRedirect(`/search`)) + }, [user]) + const onSubmit = (event: FormEvent) => { event.preventDefault(); let form = { @@ -45,7 +49,7 @@ export default function Search() { setSearched(form.search) }); } - + if (!user?.token) return // The UI does not allow the saving of an illustration without tags. // If it does, then we need to have a listing of those illustrations here. return ( diff --git a/frontend/src/pages/settings.tsx b/frontend/src/pages/settings.tsx index f220899..7fc1a9e 100644 --- a/frontend/src/pages/settings.tsx +++ b/frontend/src/pages/settings.tsx @@ -7,18 +7,22 @@ import api from '@/library/api' import { getSettings, getThunkSettings, setSettings } from '@/features/user/reducer' import router from 'next/router' import { ArrowLeftIcon, ClipboardDocumentListIcon } from '@heroicons/react/24/solid' +import { setRedirect } from '@/features/ui/reducer'; export default function Settings() { - // here we just check if user is already logged in and redirect to profile - // should be last page + const { user } = useUser({ - redirectTo: '/', + redirectTo: '/login', }) const dispatch = useAppDispatch() dispatch(getThunkSettings(user?.token)) const settings = useAppSelector(getSettings) + useEffect(() => { + if (!user?.token) dispatch(setRedirect(`/settings`)) + }, [user]) + const onSubmit = (event: FormEvent) => { event.preventDefault(); let form = grabAndReturnObject(event.currentTarget) @@ -41,7 +45,7 @@ export default function Settings() { location: form.location.value.trim(), } } - + if (!user?.token) return return ( {settings && <> diff --git a/frontend/src/pages/tag/[name].tsx b/frontend/src/pages/tag/[name].tsx index 925291b..402fd53 100644 --- a/frontend/src/pages/tag/[name].tsx +++ b/frontend/src/pages/tag/[name].tsx @@ -10,7 +10,7 @@ import ConfirmDialog from '@/components/ConfirmDialog'; import { PencilSquareIcon, CheckCircleIcon, TrashIcon } from '@heroicons/react/24/solid' import { FormEvent } from 'react'; import Head from 'next/head'; - +import { setRedirect } from '@/features/ui/reducer'; import { useAppSelector, useAppDispatch } from '@/hooks' import { selectModal, setModal } from '@/features/modal/reducer' import { setFlashMessage } from '@/features/flash/reducer' @@ -29,12 +29,13 @@ export default function Tag() { const [editTag, setEditTag] = useState(false) useEffect(() => { + if (!user?.token) dispatch(setRedirect(`/tag/${router.query.name}`)) if (!name) { setLoading(true) return } // add - for data fetching - api.get(`/tag/${name}`.replace(/ /g, '-'), '', user?.token) + api.get(`/tag/${router.query.name}`, '', user?.token) .then(data => { setData(data); // illustrations setLoading(false) @@ -73,7 +74,7 @@ export default function Tag() { router.replace('/') // go home }); }; - + if (!user?.token) return if (isLoading) return (
Loading...