Merge pull request #4770 from freesewing/joost
Feat: Backend strict token verification and blog creation
This commit is contained in:
commit
412a2933ea
13 changed files with 575 additions and 35 deletions
|
@ -5,6 +5,7 @@ export const roles = {
|
|||
readOnly: 2,
|
||||
writeSome: 3,
|
||||
user: 4,
|
||||
tester: 4,
|
||||
curator: 5,
|
||||
bughunter: 6,
|
||||
support: 8,
|
||||
|
|
|
@ -10,6 +10,7 @@ datasource db {
|
|||
|
||||
model Apikey {
|
||||
id String @id @default(uuid())
|
||||
aud String @default("")
|
||||
createdAt DateTime @default(now())
|
||||
expiresAt DateTime
|
||||
name String @default("")
|
||||
|
|
|
@ -50,6 +50,8 @@ const baseConfig = {
|
|||
env: process.env.NODE_ENV || 'development',
|
||||
// Maintainer contact
|
||||
maintainer: 'joost@freesewing.org',
|
||||
// Instance
|
||||
instance: process.env.BACKEND_INSTANCE || Date.now(),
|
||||
// Feature flags
|
||||
use: {
|
||||
github: envToBool(process.env.BACKEND_ENABLE_GITHUB),
|
||||
|
@ -110,8 +112,7 @@ const baseConfig = {
|
|||
},
|
||||
jwt: {
|
||||
secretOrKey: encryptionKey,
|
||||
issuer: process.env.BACKEND_JWT_ISSUER || 'freesewing.org',
|
||||
audience: process.env.BACKEND_JWT_ISSUER || 'freesewing.org',
|
||||
issuer: api,
|
||||
expiresIn: process.env.BACKEND_JWT_EXPIRY || '7d',
|
||||
},
|
||||
languages,
|
||||
|
@ -232,6 +233,7 @@ export const cloudflareImages = config.cloudflareImages || {}
|
|||
export const forwardmx = config.forwardmx || {}
|
||||
export const website = config.website
|
||||
export const githubToken = config.github.token
|
||||
export const instance = config.instance
|
||||
|
||||
const vars = {
|
||||
BACKEND_DB_URL: ['required', 'db.url'],
|
||||
|
|
|
@ -50,9 +50,9 @@ FlowsController.prototype.removeImage = async (req, res, tools) => {
|
|||
* Creates a pull request for a new showcase post
|
||||
* See: https://freesewing.dev/reference/backend/api
|
||||
*/
|
||||
FlowsController.prototype.createShowcasePr = async (req, res, tools) => {
|
||||
FlowsController.prototype.createPostPr = async (req, res, tools, type) => {
|
||||
const Flow = new FlowModel(tools)
|
||||
await Flow.createShowcasePr(req)
|
||||
await Flow.createPostPr(req, type)
|
||||
|
||||
return Flow.sendResponse(res)
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import http from 'passport-http'
|
|||
import jwt from 'passport-jwt'
|
||||
import { ApikeyModel } from './models/apikey.mjs'
|
||||
import { UserModel } from './models/user.mjs'
|
||||
import { api, instance } from './config.mjs'
|
||||
|
||||
/*
|
||||
* In v2 we ended up with a bug where we did not properly track the last login
|
||||
|
@ -10,8 +11,14 @@ import { UserModel } from './models/user.mjs'
|
|||
* this field. It's a bit of a perf hit to write to the database on ever API call
|
||||
* but it's worth it to actually know which accounts are used and which are not.
|
||||
*/
|
||||
async function checkAccess(uid, tools, type) {
|
||||
async function checkAccess(payload, tools, type) {
|
||||
/*
|
||||
* Don't allow tokens/keys to be used on different instances,
|
||||
* even with the same encryption key
|
||||
*/
|
||||
if (payload.aud !== `${api}/${instance}`) return false
|
||||
const User = new UserModel(tools)
|
||||
const uid = payload.userId || payload._id
|
||||
const ok = await User.papersPlease(uid, type)
|
||||
|
||||
return ok
|
||||
|
@ -29,7 +36,7 @@ function loadPassportMiddleware(passport, tools) {
|
|||
/*
|
||||
* We check more than merely the API key
|
||||
*/
|
||||
const ok = Apikey.verified ? await checkAccess(Apikey.record.userId, tools, 'key') : false
|
||||
const ok = Apikey.verified ? await checkAccess(Apikey.record, tools, 'key') : false
|
||||
|
||||
return ok
|
||||
? done(null, {
|
||||
|
@ -50,7 +57,7 @@ function loadPassportMiddleware(passport, tools) {
|
|||
/*
|
||||
* We check more than merely the token
|
||||
*/
|
||||
const ok = await checkAccess(jwt_payload._id, tools, 'jwt')
|
||||
const ok = await checkAccess(jwt_payload, tools, 'jwt')
|
||||
|
||||
return ok
|
||||
? done(null, {
|
||||
|
|
|
@ -261,6 +261,7 @@ ApikeyModel.prototype.create = async function ({ body, user }) {
|
|||
try {
|
||||
this.record = await this.prisma.apikey.create({
|
||||
data: this.cloak({
|
||||
aud: `${this.config.api}/${this.config.instance}`,
|
||||
expiresAt,
|
||||
name: body.name,
|
||||
level: body.level,
|
||||
|
|
|
@ -261,13 +261,14 @@ to English prior to merging.
|
|||
`
|
||||
|
||||
/*
|
||||
* Create a (GitHub) pull request for a new showcase post
|
||||
* Create a (GitHub) pull request for a new blog or showcase post
|
||||
*
|
||||
* @param {body} object - The request body
|
||||
* @param {user} object - The user as loaded by auth middleware
|
||||
* @param {type} string - One of blog or showcase
|
||||
* @returns {FlowModel} object - The FlowModel
|
||||
*/
|
||||
FlowModel.prototype.createShowcasePr = async function ({ body, user }) {
|
||||
FlowModel.prototype.createPostPr = async function ({ body, user }, type) {
|
||||
/*
|
||||
* Is markdown set?
|
||||
*/
|
||||
|
@ -283,16 +284,16 @@ FlowModel.prototype.createShowcasePr = async function ({ body, user }) {
|
|||
/*
|
||||
* Create a new feature branch for this
|
||||
*/
|
||||
const branchName = `showcase-${body.slug}`
|
||||
const branchName = `${type}-${body.slug}`
|
||||
const branch = await createBranch({ name: branchName })
|
||||
|
||||
/*
|
||||
* Create the file
|
||||
*/
|
||||
const file = await createFile({
|
||||
path: `markdown/org/showcase/${body.slug}/en.md`,
|
||||
path: `markdown/org/${type}/${body.slug}/en.md`,
|
||||
body: {
|
||||
message: `feat: New showcase post ${body.slug} by ${this.User.record.username}${
|
||||
message: `feat: New ${type} post ${body.slug} by ${this.User.record.username}${
|
||||
body.language !== 'en' ? nonEnWarning : ''
|
||||
}`,
|
||||
content: new Buffer.from(body.markdown).toString('base64'),
|
||||
|
@ -308,8 +309,8 @@ FlowModel.prototype.createShowcasePr = async function ({ body, user }) {
|
|||
* New create the pull request
|
||||
*/
|
||||
const pr = await createPullRequest({
|
||||
title: `feat: New showcase post ${body.slug} by ${this.User.record.username}`,
|
||||
body: `Hey @joostdecock you should check out this awesome showcase post.${
|
||||
title: `feat: New ${type} post ${body.slug} by ${this.User.record.username}`,
|
||||
body: `Paging @joostdecock to check out this proposed ${type} post.${
|
||||
body.language !== 'en' ? nonEnWarning : ''
|
||||
}`,
|
||||
from: branchName,
|
||||
|
|
|
@ -1281,7 +1281,7 @@ UserModel.prototype.getToken = function () {
|
|||
username: this.record.username,
|
||||
role: this.record.role,
|
||||
status: this.record.status,
|
||||
aud: this.config.jwt.audience,
|
||||
aud: `${this.config.api}/${this.config.instance}`,
|
||||
iss: this.config.jwt.issuer,
|
||||
},
|
||||
this.config.jwt.secretOrKey,
|
||||
|
|
|
@ -39,13 +39,15 @@ export function flowsRoutes(tools) {
|
|||
Flow.removeImage(req, res, tools)
|
||||
)
|
||||
|
||||
// Submit a pull request for a new showcase
|
||||
app.post('/flows/pr/showcase/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||
Flow.createShowcasePr(req, res, tools)
|
||||
// Submit a pull request for a new showcase or blog post
|
||||
for (const type of ['blog', 'showcase']) {
|
||||
app.post(`/flows/pr/${type}/jwt`, passport.authenticate(...jwt), (req, res) =>
|
||||
Flow.createPostPr(req, res, tools, type)
|
||||
)
|
||||
app.post('/flows/pr/showcase/key', passport.authenticate(...bsc), (req, res) =>
|
||||
Flow.createShowcasePr(req, res, tools)
|
||||
app.post(`/flows/pr/${type}/key`, passport.authenticate(...bsc), (req, res) =>
|
||||
Flow.createPostPr(req, res, tools, type)
|
||||
)
|
||||
}
|
||||
|
||||
// Create Issue - No auth needed
|
||||
app.post('/issues', (req, res) => Flow.createIssue(req, res, tools))
|
||||
|
|
527
sites/org/components/github/create-post.mjs
Normal file
527
sites/org/components/github/create-post.mjs
Normal file
|
@ -0,0 +1,527 @@
|
|||
// Dependencies
|
||||
import { nsMerge, capitalize, cloudflareImageUrl, yyyymmdd } from 'shared/utils.mjs'
|
||||
// Hooks
|
||||
import { useState, Fragment } from 'react'
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
// Components
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
import { AuthWrapper, ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
|
||||
import { DesignPicker } from './design-picker.mjs'
|
||||
import {
|
||||
TitleInput,
|
||||
SlugInput,
|
||||
ImageInput,
|
||||
CaptionInput,
|
||||
IntroInput,
|
||||
BodyInput,
|
||||
} from './inputs.mjs'
|
||||
import { Tab } from 'shared/components/account/bio.mjs'
|
||||
import { CodeBox } from 'shared/components/code-box.mjs'
|
||||
import { PostArticle, ns as mdxNs } from 'site/components/mdx/posts/article.mjs'
|
||||
import { PageLink } from 'shared/components/page-link.mjs'
|
||||
import { OkIcon, WarningIcon as KoIcon } from 'shared/components/icons.mjs'
|
||||
import { useLoadingStatus } from 'shared/hooks/use-loading-status.mjs'
|
||||
import { WebLink } from 'shared/components/web-link.mjs'
|
||||
|
||||
export const ns = nsMerge('account', 'posts', authNs, mdxNs)
|
||||
|
||||
const Tip = ({ children }) => <p className="italic opacity-70">{children}</p>
|
||||
|
||||
const Item = ({ title, children }) => (
|
||||
<div className="collapse collapse-arrow bg-base-100 hover:bg-secondary hover:bg-opacity-20">
|
||||
<input type="radio" name="my-accordion-2" />
|
||||
<div className="collapse-title text-xl font-medium">{title}</div>
|
||||
<div className="collapse-content bg-base-100 hover:bg-base-100">{children}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const dataAsMd = ({ title, user, caption, intro, designs, body }, type) => {
|
||||
let md = `---
|
||||
title: "${title}"
|
||||
caption: "${caption}"
|
||||
date: ${yyyymmdd()}
|
||||
intro: "${intro}"`
|
||||
if (type === 'showcase')
|
||||
md += `
|
||||
designs: [${designs.map((design) => `"${design}"`).join(', ')}]
|
||||
maker: ${user}`
|
||||
else
|
||||
md += `
|
||||
author: ${user}`
|
||||
md += `
|
||||
---
|
||||
|
||||
${body}
|
||||
|
||||
`
|
||||
|
||||
return md
|
||||
}
|
||||
|
||||
export const CreatePost = ({ type = 'showcase' }) => {
|
||||
// Hooks
|
||||
const backend = useBackend()
|
||||
const { account } = useAccount()
|
||||
const { t, i18n } = useTranslation(ns)
|
||||
const { loading, setLoadingStatus, LoadingStatus } = useLoadingStatus()
|
||||
|
||||
// State
|
||||
const [designs, setDesigns] = useState([])
|
||||
const [title, setTitle] = useState('')
|
||||
const [slug, setSlug] = useState(false)
|
||||
const [slugAvailable, setSlugAvailable] = useState(true)
|
||||
const [img, setImg] = useState(false)
|
||||
const [caption, setCaption] = useState('')
|
||||
const [intro, setIntro] = useState('')
|
||||
const [body, setBody] = useState('')
|
||||
const [extraImages, setExtraImages] = useState({})
|
||||
const [activeTab, setActiveTab] = useState('create')
|
||||
const [pr, setPr] = useState(false)
|
||||
|
||||
// Method that submits the post to the backend
|
||||
const submitPost = async () => {
|
||||
setLoadingStatus([true, `Creating ${type} post & pull request`])
|
||||
const result = await backend.createPost(type, {
|
||||
markdown: dataAsMd(
|
||||
{
|
||||
title,
|
||||
user: account.username,
|
||||
caption,
|
||||
intro,
|
||||
designs,
|
||||
body,
|
||||
},
|
||||
type
|
||||
),
|
||||
slug,
|
||||
language: i18n.language,
|
||||
})
|
||||
if (result.success) setPr(result.data)
|
||||
setLoadingStatus([false])
|
||||
}
|
||||
|
||||
// Shared props for tabs
|
||||
const tabProps = { activeTab, setActiveTab, t }
|
||||
|
||||
const addImage = () => {
|
||||
const id = Object.keys(extraImages).length + 1
|
||||
const newImages = { ...extraImages }
|
||||
newImages[id] = null
|
||||
setExtraImages(newImages)
|
||||
}
|
||||
|
||||
const verifySlug = async (newSlug) => {
|
||||
setSlug(newSlug)
|
||||
const result = await backend.isSlugAvailable({ slug: newSlug, type })
|
||||
console.log(result)
|
||||
setSlugAvailable(result.available === true ? true : false)
|
||||
}
|
||||
|
||||
const setExtraImg = (key, img) => {
|
||||
const newImages = { ...extraImages }
|
||||
newImages[key] = img
|
||||
setExtraImages(newImages)
|
||||
}
|
||||
|
||||
const childProps = {
|
||||
type,
|
||||
designs,
|
||||
setDesigns,
|
||||
title,
|
||||
setTitle,
|
||||
slug,
|
||||
img,
|
||||
setImg,
|
||||
caption,
|
||||
setCaption,
|
||||
intro,
|
||||
setIntro,
|
||||
body,
|
||||
setBody,
|
||||
extraImages,
|
||||
setExtraImages,
|
||||
addImage,
|
||||
setExtraImg,
|
||||
account,
|
||||
t,
|
||||
verifySlug,
|
||||
slugAvailable,
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthWrapper>
|
||||
<LoadingStatus />
|
||||
{pr ? (
|
||||
<div className="w-full max-w-3xl m-auto p-4">
|
||||
<h1>Thank you for submitting this {type} post</h1>
|
||||
<p>Here is what happened while you were waiting:</p>
|
||||
<ul className="list list-inside list-disc ml-4">
|
||||
<li>
|
||||
We created a new branch:{' '}
|
||||
<b>
|
||||
<WebLink
|
||||
href={`https://github.com/freesewing/freesewing/tree/${type}-${slug}`}
|
||||
txt={`${type}-${slug}`}
|
||||
/>
|
||||
</b>
|
||||
</li>
|
||||
<li>
|
||||
We committed your work:{' '}
|
||||
<b>
|
||||
<WebLink href={pr.file.commit.html_url} txt={pr.file.commit.sha} />
|
||||
</b>
|
||||
</li>
|
||||
<li>
|
||||
We created a pull request:{' '}
|
||||
<b>
|
||||
<WebLink
|
||||
href={pr.pr.html_url}
|
||||
txt={`github.com/freesewing/freesewing/pull/${pr.pr.number}`}
|
||||
/>
|
||||
</b>
|
||||
</li>
|
||||
</ul>
|
||||
<p>Next steps:</p>
|
||||
<ul className="list list-inside list-disc ml-4">
|
||||
<li>
|
||||
<b>Joost will review</b> the pull request to make sure everything is ok
|
||||
</li>
|
||||
<li>
|
||||
If everything looks fine, <b>they will merge it</b>.
|
||||
</li>
|
||||
<li>
|
||||
This will trigger a <b>preview build of the website</b>
|
||||
</li>
|
||||
<li>
|
||||
If that goes without any hiccups, this preview build will be{' '}
|
||||
<b>deployed in production</b>.
|
||||
</li>
|
||||
<li>
|
||||
When that happens, <b>your post will go live</b> at:{' '}
|
||||
<WebLink
|
||||
href={`https://freesewing.org/${type}/${slug}/`}
|
||||
txt={`freesewing.org/${type}/${slug}`}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<p className="text-xl font-bold mt-6">
|
||||
To summarize: You did great <span role="img">💜</span> and we'll take it from here{' '}
|
||||
<span role="img">🙌</span>
|
||||
</p>
|
||||
<button className="btn btn-primary btn-outline mt-4" onClick={() => setPr(false)}>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="hidden md:grid md:grid-cols-2 md:gap-4">
|
||||
<div className="p-4 shadow rounded-lg my-8">
|
||||
<PostEditor {...childProps} />
|
||||
</div>
|
||||
<div className="p-4 shadow rounded-lg my-8">
|
||||
<PostPreview {...childProps} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="block md:hidden px-4">
|
||||
<div className="tabs w-full">
|
||||
<Tab id="create" {...tabProps} />
|
||||
<Tab id="preview" {...tabProps} />
|
||||
</div>
|
||||
{activeTab === 'create' ? (
|
||||
<PostEditor {...childProps} />
|
||||
) : (
|
||||
<PostPreview {...childProps} />
|
||||
)}
|
||||
</div>
|
||||
<div className="px-4 max-w-lg m-auto my-8 text-center">
|
||||
{!(title && slug && img && (type === 'blog' || designs.length > 0)) && (
|
||||
<Popout note>
|
||||
<h5 className="text-left">You are missing the following:</h5>
|
||||
<ul className="text-left list list-inside list-disc ml-4">
|
||||
{type === 'showcase' && designs.length < 1 && <li>Design</li>}
|
||||
{!title && <li>Title</li>}
|
||||
{!slug && <li>Slug</li>}
|
||||
{!img && <li>Main Image</li>}
|
||||
</ul>
|
||||
</Popout>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-lg btn-primary capitalize"
|
||||
disabled={
|
||||
loading || !(title && slug && img && (type === 'blog' || designs.length > 0))
|
||||
}
|
||||
onClick={submitPost}
|
||||
>
|
||||
Submit {type} Post
|
||||
</button>
|
||||
{!account.data?.githubUser && !account.data?.githubEmail && (
|
||||
<Popout tip>
|
||||
<h5 className="text-left">
|
||||
<small>Optional:</small> Are you on GitHub?
|
||||
</h5>
|
||||
<p className="text-left">
|
||||
If you configure your GitHub username{' '}
|
||||
<PageLink href="/account/github" txt="in your account" />, we will credit these
|
||||
changes to you.
|
||||
</p>
|
||||
</Popout>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</AuthWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const PostPreview = ({ designs, title, img, caption, intro, body, account }) => (
|
||||
<>
|
||||
<h1>{title}</h1>
|
||||
<PostArticle
|
||||
frontmatter={{
|
||||
title,
|
||||
designs,
|
||||
maker: account.username,
|
||||
date: yyyymmdd(),
|
||||
caption,
|
||||
intro,
|
||||
}}
|
||||
imgId={img}
|
||||
body={body}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
const PostEditor = ({
|
||||
type,
|
||||
designs,
|
||||
setDesigns,
|
||||
title,
|
||||
setTitle,
|
||||
slug,
|
||||
verifySlug,
|
||||
slugAvailable,
|
||||
img,
|
||||
setImg,
|
||||
caption,
|
||||
setCaption,
|
||||
intro,
|
||||
setIntro,
|
||||
body,
|
||||
setBody,
|
||||
extraImages,
|
||||
addImage,
|
||||
setExtraImg,
|
||||
t,
|
||||
}) => (
|
||||
<>
|
||||
<h2>Create a new {type} post</h2>
|
||||
<Tip>{t(`${type}NewInfo`)}</Tip>
|
||||
{type === 'showcase' && (
|
||||
<Item
|
||||
title={
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
{designs.length > 0 ? (
|
||||
<OkIcon stroke={4} className="w-5 h-5 text-success" />
|
||||
) : (
|
||||
<KoIcon stroke={3} className="w-5 h-5 text-error" />
|
||||
)}
|
||||
<b>Design:</b>
|
||||
{designs.length > 0 ? (
|
||||
<span className="text-base">{designs.map((d) => capitalize(d)).join(', ')}</span>
|
||||
) : (
|
||||
<span className="text-error text-base">Please select at least 1 design</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Tip>Pick one or more designs that are featured in this post.</Tip>
|
||||
<DesignPicker {...{ designs, setDesigns }} />
|
||||
</Item>
|
||||
)}
|
||||
<Item
|
||||
title={
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
{title.length > 10 ? (
|
||||
<OkIcon stroke={4} className="w-5 h-5 text-success" />
|
||||
) : (
|
||||
<KoIcon stroke={3} className="w-5 h-5 text-error" />
|
||||
)}
|
||||
<b>Title:</b>
|
||||
{title.length > 10 ? (
|
||||
<span className="text-base">{title}</span>
|
||||
) : (
|
||||
<span className="text-error text-base">Please enter a post title</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Tip>Give your post a title. A good title is more than just a few words.</Tip>
|
||||
<TitleInput {...{ title, setTitle }} />
|
||||
</Item>
|
||||
<Item
|
||||
title={
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
{slugAvailable && slug.length > 3 ? (
|
||||
<OkIcon stroke={4} className="w-5 h-5 text-success" />
|
||||
) : (
|
||||
<KoIcon stroke={3} className="w-5 h-5 text-error" />
|
||||
)}
|
||||
<b>Slug:</b>
|
||||
{slug.length > 3 ? (
|
||||
<span className="text-base">{slug}</span>
|
||||
) : (
|
||||
<span className="text-error text-base">Please enter a slug (or post title)</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Tip>
|
||||
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.
|
||||
</Tip>
|
||||
<SlugInput {...{ title, slug, setSlug: verifySlug, slugAvailable }} />
|
||||
</Item>
|
||||
<Item
|
||||
title={
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
{img.length > 3 ? (
|
||||
<OkIcon stroke={4} className="w-5 h-5 text-success" />
|
||||
) : (
|
||||
<KoIcon stroke={3} className="w-5 h-5 text-error" />
|
||||
)}
|
||||
<b>Main Image:</b>
|
||||
{img.length > 3 ? (
|
||||
<span className="text-base">{img}</span>
|
||||
) : (
|
||||
<span className="text-error text-base">Please provide a main image for the post</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Tip>
|
||||
The main image will be shown at the top of the post, and as the only image on the {type}
|
||||
index page.
|
||||
</Tip>
|
||||
<ImageInput {...{ img, setImg, slug }} type={type} subId="main" />
|
||||
</Item>
|
||||
<Item
|
||||
title={
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
{caption.length > 3 ? (
|
||||
<OkIcon stroke={4} className="w-5 h-5 text-success" />
|
||||
) : (
|
||||
<KoIcon stroke={3} className="w-5 h-5 text-error" />
|
||||
)}
|
||||
<b>Main Image Caption:</b>
|
||||
{caption.length > 3 ? (
|
||||
<span className="text-base">{caption}</span>
|
||||
) : (
|
||||
<span className="text-error text-base">
|
||||
Please provide a caption for the main image
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Tip>
|
||||
The caption is the text that goes under the main image. Can include copyrights/credits.
|
||||
Markdown is allowed.
|
||||
</Tip>
|
||||
<CaptionInput {...{ caption, setCaption }} />
|
||||
</Item>
|
||||
<Item
|
||||
title={
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
{intro.length > 3 ? (
|
||||
<OkIcon stroke={4} className="w-5 h-5 text-success" />
|
||||
) : (
|
||||
<KoIcon stroke={3} className="w-5 h-5 text-error" />
|
||||
)}
|
||||
<b>Intro:</b>
|
||||
{intro.length > 3 ? (
|
||||
<span className="text-base">{intro}</span>
|
||||
) : (
|
||||
<span className="text-error text-base">Please provide an intro for link proviews</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Tip>A brief paragraph that will be shown on post previews on social media and so on.</Tip>
|
||||
<IntroInput {...{ intro, setIntro }} />
|
||||
</Item>
|
||||
<Item
|
||||
title={
|
||||
<span>
|
||||
<b>Additional Images</b>: {Object.keys(extraImages).length}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{img ? (
|
||||
<>
|
||||
<Tip>Here you can add any images you want to include in the post body.</Tip>
|
||||
{Object.keys(extraImages).map((key) => {
|
||||
const markup =
|
||||
' +
|
||||
' "The image caption/title goes here")'
|
||||
return (
|
||||
<Fragment key={key}>
|
||||
<ImageInput
|
||||
key={key}
|
||||
setImg={(img) => setExtraImg(key, img)}
|
||||
type={type}
|
||||
subId={key}
|
||||
img={extraImages[key]}
|
||||
slug={slug}
|
||||
/>
|
||||
{extraImages[key] && (
|
||||
<>
|
||||
<p>To include this image in your post, use this markdown snippet:</p>
|
||||
<CodeBox code={markup} title="MarkDown" />
|
||||
<p className="text-right -mt-5">
|
||||
<button
|
||||
className="btn btn-sm btn-secondary btn-outline"
|
||||
onClick={() => setBody(body + '\n\n' + markup)}
|
||||
>
|
||||
Add to post body
|
||||
</button>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
<button className="btn btn-secondary mt-2" onClick={addImage}>
|
||||
Add Image
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<Popout note compact>
|
||||
Please add a main image first
|
||||
</Popout>
|
||||
)}
|
||||
</Item>
|
||||
<Item
|
||||
title={
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
{body.length > 3 ? (
|
||||
<OkIcon stroke={4} className="w-5 h-5 text-success" />
|
||||
) : (
|
||||
<KoIcon stroke={3} className="w-5 h-5 text-error" />
|
||||
)}
|
||||
<b>Post body:</b>
|
||||
{body.length > 3 ? (
|
||||
<span className="text-base">{body.slice(0, 30) + '...'}</span>
|
||||
) : (
|
||||
<span className="text-error text-base">Please provide a post body</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Tip>The actual post body. Supports Markdown.</Tip>
|
||||
<BodyInput {...{ body, setBody }} />
|
||||
</Item>
|
||||
</>
|
||||
)
|
|
@ -6,11 +6,11 @@ import { useTranslation } from 'next-i18next'
|
|||
// Components
|
||||
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
|
||||
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
|
||||
//import { BareLayout } from 'site/components/layouts/bare.mjs'
|
||||
import { Popout } from 'shared/components/popout/index.mjs'
|
||||
import { CreatePost, ns as createNs } from 'site/components/github/create-post.mjs'
|
||||
import { BareLayout } from 'site/components/layouts/bare.mjs'
|
||||
|
||||
// Translation namespaces used on this page
|
||||
const namespaces = nsMerge(authNs, pageNs)
|
||||
const namespaces = nsMerge(createNs, authNs, pageNs)
|
||||
|
||||
/*
|
||||
* Each page MUST be wrapped in the PageWrapper component.
|
||||
|
@ -22,11 +22,9 @@ const NewBlogPage = ({ page }) => {
|
|||
const { t } = useTranslation(namespaces)
|
||||
|
||||
return (
|
||||
<PageWrapper {...page} title={t('blogNew')}>
|
||||
<PageWrapper {...page} title={t('blogNew')} layout={BareLayout}>
|
||||
<div className="w-full px-4 mt-8">
|
||||
<Popout fixme compact>
|
||||
This is not (yet) implemented
|
||||
</Popout>
|
||||
<CreatePost type="blog" />
|
||||
</div>
|
||||
</PageWrapper>
|
||||
)
|
||||
|
@ -40,7 +38,7 @@ export async function getStaticProps({ locale }) {
|
|||
...(await serverSideTranslations(locale, namespaces)),
|
||||
page: {
|
||||
locale,
|
||||
path: ['new', 'showcase'],
|
||||
path: ['new', 'blog'],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { useTranslation } from 'next-i18next'
|
|||
// Components
|
||||
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
|
||||
import { ns as authNs } from 'shared/components/wrappers/auth/index.mjs'
|
||||
import { CreateShowcasePost, ns as createNs } from 'site/components/github/create-showcase.mjs'
|
||||
import { CreatePost, ns as createNs } from 'site/components/github/create-post.mjs'
|
||||
import { BareLayout } from 'site/components/layouts/bare.mjs'
|
||||
|
||||
// Translation namespaces used on this page
|
||||
|
@ -24,7 +24,7 @@ const NewShowcasePage = ({ page }) => {
|
|||
return (
|
||||
<PageWrapper {...page} title={t('showcaseNew')} layout={BareLayout}>
|
||||
<div className="w-full px-4 mt-8">
|
||||
<CreateShowcasePost noTitle />
|
||||
<CreatePost type="showcase" />
|
||||
</div>
|
||||
</PageWrapper>
|
||||
)
|
||||
|
|
|
@ -311,10 +311,10 @@ Backend.prototype.isSlugAvailable = async function ({ slug, type }) {
|
|||
}
|
||||
|
||||
/*
|
||||
* Create showcase Pull Request
|
||||
* Create showcase/blog post (pull request)
|
||||
*/
|
||||
Backend.prototype.createShowcasePr = async function (data) {
|
||||
return responseHandler(await api.post(`/flows/pr/showcase/jwt`, data, this.auth), 201)
|
||||
Backend.prototype.createPost = async function (type, data) {
|
||||
return responseHandler(await api.post(`/flows/pr/${type}/jwt`, data, this.auth), 201)
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue