1
0
Fork 0

Merge pull request #4763 from freesewing/joost

fix(backend/shared): Catch JSON errors in backend and add border to measurements input
This commit is contained in:
Joost De Cock 2023-08-18 16:07:07 +02:00 committed by GitHub
commit e8e6705504
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 193 additions and 56 deletions

View file

@ -69,3 +69,24 @@ FlowsController.prototype.createIssue = async (req, res, tools) => {
return Flow.sendResponse(res) return Flow.sendResponse(res)
} }
/*
* Is a blog or showcase slug available?
*
* This is the endpoint that verifies whether a blog or showcase slug is available
* See: https://freesewing.dev/reference/backend/api/apikey
*/
FlowsController.prototype.isSlugAvailable = async (req, res, tools, type) => {
const Flow = new FlowModel(tools)
const available = await Flow.isSlugAvailable(req, type)
if (!available)
Flow.setResponse(200, false, {
result: 'success',
slug: req.params.slug,
available: false,
})
else Flow.setResponse(404)
return Flow.sendResponse(res)
}

View file

@ -1,7 +1,13 @@
import { i18nUrl } from '../utils/index.mjs' import { i18nUrl } from '../utils/index.mjs'
import { decorateModel } from '../utils/model-decorator.mjs' import { decorateModel } from '../utils/model-decorator.mjs'
import { ensureImage, replaceImage, removeImage } from '../utils/cloudflare-images.mjs' import { ensureImage, replaceImage, removeImage } from '../utils/cloudflare-images.mjs'
import { createIssue, createFile, createBranch, createPullRequest } from '../utils/github.mjs' import {
createIssue,
createFile,
createBranch,
createPullRequest,
getFileList,
} from '../utils/github.mjs'
/* /*
* This model handles all flows (typically that involves sending out emails) * This model handles all flows (typically that involves sending out emails)
@ -315,3 +321,28 @@ FlowModel.prototype.createShowcasePr = async function ({ body, user }) {
*/ */
return pr ? this.setResponse201({ branch, file, pr }) : this.setResponse(400) return pr ? this.setResponse201({ branch, file, pr }) : this.setResponse(400)
} }
/*
* Checks to see whether a slug is available
*
* @param {params} object - The request (URL) parameters
* @param {user} object - The user as loaded by auth middleware
* @param {type} object - The type to check (either 'blog' or 'showcase')
* @returns {FlowModel} object - The FlowModel
*/
FlowModel.prototype.isSlugAvailable = async function ({ params }, type) {
/*
* Is slug set?
*/
if (!params.slug) return this.setResponse(400, `slugMissing`)
/*
* Load the list of folders from github
*/
const folders = (await getFileList(`markdown/org/${type}`)).map((folder) => folder.name)
/*
* Return whether or not params.slug is already included in the list of slugs
*/
return !folders.includes(params.slug)
}

View file

@ -49,4 +49,14 @@ export function flowsRoutes(tools) {
// Create Issue - No auth needed // Create Issue - No auth needed
app.post('/issues', (req, res) => Flow.createIssue(req, res, tools)) app.post('/issues', (req, res) => Flow.createIssue(req, res, tools))
// See if a showcase or blog slug is available
for (const type of ['showcase', 'blog']) {
app.get(`/slugs/${type}/:slug/jwt`, passport.authenticate(...jwt), (req, res) =>
Flow.isSlugAvailable(req, res, tools, type)
)
app.get(`/slugs/${type}/:slug/key`, passport.authenticate(...bsc), (req, res) =>
Flow.isSlugAvailable(req, res, tools, type)
)
}
} }

View file

@ -3,6 +3,40 @@ import { githubToken as token } from '../config.mjs'
const api = 'https://api.github.com/repos/freesewing/freesewing' const api = 'https://api.github.com/repos/freesewing/freesewing'
/*
* Sometimes we'd like to cache responses.
* This is a poor man's cache
*/
const cache = {}
const fresh = 1800000 // 30 minutes
/*
* Helper method to run a requests against the GitHub API
* with built-in cachine
*/
const cachedApiRequest = async (method, url, body = false, success = 200) => {
const id = JSON.stringify({ method, url, body, success })
const now = Date.now()
/*
* Is this reponse cached?
*/
if (cache[id]) {
/*
* It is. But Is it fresh?
*/
if (cache[id].timestamp && now - cache[id].timestamp < fresh) return cache[id].data
/*
* It is in the cache, but stale. Remove cache entry
*/ else delete cache[id]
} else {
const data = apiRequest(method, url, body, success)
cache[id] = { timestamp: now, data }
return data
}
}
/* /*
* Helper method to run requests against the GitHub API * Helper method to run requests against the GitHub API
*/ */
@ -79,3 +113,8 @@ export const createPullRequest = async ({ title, body, from, to = 'develop' }) =
}, },
201 201
) )
/*
* Retrieves a list of files for a folder
*/
export const getFileList = async (path) => await cachedApiRequest('GET', `${api}/contents/${path}`)

View file

@ -107,7 +107,11 @@ export function decorateModel(Model, tools, modelConfig) {
if (this.record) { if (this.record) {
for (const field of this.jsonFields) { for (const field of this.jsonFields) {
if (this.encryptedFields && this.encryptedFields.includes(field)) { if (this.encryptedFields && this.encryptedFields.includes(field)) {
this.clear[field] = JSON.parse(this.clear[field]) try {
this.clear[field] = JSON.parse(this.clear[field])
} catch (err) {
console.log(err, typeof this.clear[field])
}
} else { } else {
this.record[field] = JSON.parse(this.record[field]) this.record[field] = JSON.parse(this.record[field])
} }

View file

@ -59,6 +59,7 @@ export const CreateShowcasePost = () => {
const [designs, setDesigns] = useState([]) const [designs, setDesigns] = useState([])
const [title, setTitle] = useState('') const [title, setTitle] = useState('')
const [slug, setSlug] = useState(false) const [slug, setSlug] = useState(false)
const [slugAvailable, setSlugAvailable] = useState(true)
const [img, setImg] = useState(false) const [img, setImg] = useState(false)
const [caption, setCaption] = useState('') const [caption, setCaption] = useState('')
const [intro, setIntro] = useState('') const [intro, setIntro] = useState('')
@ -96,6 +97,13 @@ export const CreateShowcasePost = () => {
setExtraImages(newImages) setExtraImages(newImages)
} }
const verifySlug = async (newSlug) => {
setSlug(newSlug)
const result = await backend.isSlugAvailable({ slug: newSlug, type: 'showcase' })
console.log(result)
setSlugAvailable(result.available === true ? true : false)
}
const setExtraImg = (key, img) => { const setExtraImg = (key, img) => {
const newImages = { ...extraImages } const newImages = { ...extraImages }
newImages[key] = img newImages[key] = img
@ -108,7 +116,6 @@ export const CreateShowcasePost = () => {
title, title,
setTitle, setTitle,
slug, slug,
setSlug,
img, img,
setImg, setImg,
caption, caption,
@ -123,6 +130,8 @@ export const CreateShowcasePost = () => {
setExtraImg, setExtraImg,
account, account,
t, t,
verifySlug,
slugAvailable,
} }
return ( return (
@ -272,7 +281,8 @@ const ShowcaseEditor = ({
title, title,
setTitle, setTitle,
slug, slug,
setSlug, verifySlug,
slugAvailable,
img, img,
setImg, setImg,
caption, caption,
@ -332,7 +342,7 @@ const ShowcaseEditor = ({
<Item <Item
title={ title={
<div className="flex flex-row gap-2 items-center"> <div className="flex flex-row gap-2 items-center">
{slug.length > 3 ? ( {slugAvailable && slug.length > 3 ? (
<OkIcon stroke={4} className="w-5 h-5 text-success" /> <OkIcon stroke={4} className="w-5 h-5 text-success" />
) : ( ) : (
<KoIcon stroke={3} className="w-5 h-5 text-error" /> <KoIcon stroke={3} className="w-5 h-5 text-error" />
@ -350,7 +360,7 @@ const ShowcaseEditor = ({
The slug is the part of the URL that uniquely identifies the post. We can generate one based The slug is the part of the URL that uniquely identifies the post. We can generate one based
on the title, but you can also customize it. on the title, but you can also customize it.
</Tip> </Tip>
<SlugInput {...{ title, slug, setSlug }} /> <SlugInput {...{ title, slug, setSlug: verifySlug, slugAvailable }} />
</Item> </Item>
<Item <Item
title={ title={

View file

@ -13,7 +13,9 @@ export const ns = ['account']
export const CaptionInput = ({ caption, setCaption }) => ( export const CaptionInput = ({ caption, setCaption }) => (
<input <input
className="input input-text input-bordered input-lg w-full" className={`input input-text input-bordered input-lg w-full ${
caption.length < 4 ? 'input-error' : 'input-success'
}`}
value={caption} value={caption}
placeholder="Type your caption here" placeholder="Type your caption here"
onChange={(evt) => setCaption(evt.target.value)} onChange={(evt) => setCaption(evt.target.value)}
@ -22,7 +24,9 @@ export const CaptionInput = ({ caption, setCaption }) => (
export const IntroInput = ({ intro, setIntro }) => ( export const IntroInput = ({ intro, setIntro }) => (
<input <input
className="input input-text input-bordered input-lg w-full" className={`input input-text input-bordered input-lg w-full ${
intro.length < 4 ? 'input-error' : 'input-success'
}`}
value={intro} value={intro}
placeholder="Type your intro here" placeholder="Type your intro here"
onChange={(evt) => setIntro(evt.target.value)} onChange={(evt) => setIntro(evt.target.value)}
@ -31,7 +35,9 @@ export const IntroInput = ({ intro, setIntro }) => (
export const BodyInput = ({ body, setBody }) => ( export const BodyInput = ({ body, setBody }) => (
<textarea <textarea
className="input input-text input-bordered input-lg w-full h-96" className={`input input-text input-bordered input-lg w-full h-96 ${
body.length < 4 ? 'input-error' : 'input-success'
}`}
placeholder="Type your post body here" placeholder="Type your post body here"
onChange={(evt) => setBody(evt.target.value)} onChange={(evt) => setBody(evt.target.value)}
rows={16} rows={16}
@ -41,21 +47,26 @@ export const BodyInput = ({ body, setBody }) => (
export const TitleInput = ({ title, setTitle }) => ( export const TitleInput = ({ title, setTitle }) => (
<input <input
className="input input-text input-bordered input-lg w-full" className={`input input-text input-bordered input-lg w-full ${
title.length < 11 ? 'input-error' : 'input-success'
}`}
value={title} value={title}
placeholder="Type your title here" placeholder="Type your title here"
onChange={(evt) => setTitle(evt.target.value)} onChange={(evt) => setTitle(evt.target.value)}
/> />
) )
export const SlugInput = ({ slug, setSlug, title }) => { export const SlugInput = ({ slug, setSlug, title, slugAvailable }) => {
useEffect(() => { useEffect(() => {
if (title !== slug) setSlug(slugify(title)) if (title !== slug) setSlug(slugify(title))
}, [title]) }, [title])
console.log(slugAvailable)
return ( return (
<input <input
className="input input-text input-bordered input-lg w-full mb-2" className={`input input-text input-bordered input-lg w-full mb-2 ${
!slugAvailable || slug.length < 4 ? 'input-error' : 'input-success'
}`}
value={slug} value={slug}
placeholder="Type your title here" placeholder="Type your title here"
onChange={(evt) => setSlug(slugifyNoTrim(evt.target.value))} onChange={(evt) => setSlug(slugifyNoTrim(evt.target.value))}

View file

@ -144,60 +144,58 @@ export const MeasieInput = ({
return ( return (
<div className="form-control mb-2 "> <div className="form-control mb-2 ">
<div className="flex items-center gap-4 flex-wrap mx-auto"> <label className="shrink-0 grow max-w-full">
<label className="shrink-0 grow max-w-full"> {children}
{children} <span className="input-group">
<span className="input-group"> <NumberInput
<NumberInput className={`border-r-0 w-full ${valid === null && ''}`}
className={`border-r-0 w-full ${valid === null && 'border-base-200'}`} value={val}
value={val} onUpdate={update}
onUpdate={update} onMount={setValid}
onMount={setValid} />
/> <span
<span className={`bg-transparent border-y w-20
className={`bg-transparent border-y w-20
${valid === false && 'border-error text-neutral-content'} ${valid === false && 'border-error text-neutral-content'}
${valid && 'border-success text-neutral-content'} ${valid && 'border-success text-neutral-content'}
${valid === null && 'border-base-200 text-base-content'} ${valid === null && 'border-secondary text-base-content'}
`} `}
> >
<Mval <Mval
imperial={mset.imperial} imperial={mset.imperial}
val={isDegree ? val : measurementAsMm(val, units)} val={isDegree ? val : measurementAsMm(val, units)}
m={m} m={m}
className="text-base-content bg-transparent text-success text-xs font-bold p-0" className="text-base-content bg-transparent text-success text-xs font-bold p-0"
/> />
</span> </span>
<span <span
role="img" role="img"
className={`bg-transparent border-y className={`bg-transparent border-y
${valid === false && 'border-error text-neutral-content'} ${valid === false && 'border-error text-neutral-content'}
${valid && 'border-success text-neutral-content'} ${valid && 'border-success text-neutral-content'}
${valid === null && 'border-base-200 text-base-content'} ${valid === null && 'border-secondary text-base-content'}
`} `}
> >
{valid && '👍'} {valid && '👍'}
{valid === false && '🤔'} {valid === false && '🤔'}
</span> </span>
<span <span
className={`w-14 text-center className={`w-14 text-center border-secondary border-solid border border-l-0
${valid === false && 'bg-error text-neutral-content'} ${valid === false && 'bg-error text-neutral-content'}
${valid && 'bg-success text-neutral-content'} ${valid && 'bg-success text-neutral-content'}
${valid === null && 'bg-base-200 text-base-content'} ${valid === null && 'bg-base-200 text-base-content border-secondary'}
`} `}
> >
{isDegree ? '°' : mset.imperial ? 'in' : 'cm'} {isDegree ? '°' : mset.imperial ? 'in' : 'cm'}
</span>
</span> </span>
</label> </span>
{mset.imperial && ( </label>
<div className="grow my-2 sm:min-w-[22rem]"> {mset.imperial && (
{!isDegree && <FractionButtons {...{ t, fraction }} />} <div className="grow my-2 sm:min-w-[22rem]">
</div> {!isDegree && <FractionButtons {...{ t, fraction }} />}
)} </div>
</div> )}
{backend && ( {backend && (
<button className="btn btn-secondary w-24 mt-4" onClick={save} disabled={!valid}> <button className="btn btn-secondary mt-4" onClick={save} disabled={!valid}>
{t('save')} {t('save')}
</button> </button>
)} )}

View file

@ -119,7 +119,7 @@ export const NumberInput = ({
<input <input
type="text" type="text"
inputMode="number" inputMode="number"
className={`input input-bordered ${className || 'input-sm grow text-base-content'} className={`input input-secondary ${className || 'input-sm grow text-base-content'}
${valid.current === false && 'input-error'} ${valid.current === false && 'input-error'}
${valid.current && 'input-success'} ${valid.current && 'input-success'}
`} `}

View file

@ -298,12 +298,25 @@ Backend.prototype.createIssue = async function (data) {
return responseHandler(await api.post(`/issues`, data), 201) return responseHandler(await api.post(`/issues`, data), 201)
} }
/*
* Check whether a slug is available
*/
Backend.prototype.isSlugAvailable = async function ({ slug, type }) {
const response = await api.get(`/slugs/${type}/${slug}/jwt`, this.auth)
// 404 means username is available, which is success in this case
return response.status === 200
? { success: false, available: false, response }
: { success: true, data: false, available: true, response }
}
/* /*
* Create showcase Pull Request * Create showcase Pull Request
*/ */
Backend.prototype.createShowcasePr = async function (data) { Backend.prototype.createShowcasePr = async function (data) {
return responseHandler(await api.post(`/flows/pr/showcase/jwt`, data, this.auth), 201) return responseHandler(await api.post(`/flows/pr/showcase/jwt`, data, this.auth), 201)
} }
/* /*
* Send translation invite * Send translation invite
*/ */