client: add downloading and viewing raw files (#21)
This commit is contained in:
parent
606e38e192
commit
f9e9c6fe06
20 changed files with 189 additions and 49 deletions
|
@ -1,6 +1,5 @@
|
||||||
# <img src="client/public/assets/logo.png" height="32px" alt="" /> Drift
|
# <img src="client/public/assets/logo.png" height="32px" alt="" /> Drift
|
||||||
|
|
||||||
|
|
||||||
Drift is a self-hostable Gist clone. It's also a major work-in-progress, but is (almost, no database yet) completely functional.
|
Drift is a self-hostable Gist clone. It's also a major work-in-progress, but is (almost, no database yet) completely functional.
|
||||||
|
|
||||||
You can try a demo at https://drift.maxleiter.com. The demo is built on master but has no database, so files and accounts can be wiped at any time.
|
You can try a demo at https://drift.maxleiter.com. The demo is built on master but has no database, so files and accounts can be wiped at any time.
|
||||||
|
@ -8,6 +7,7 @@ You can try a demo at https://drift.maxleiter.com. The demo is built on master b
|
||||||
If you want to contribute, need support, or want to stay updated, you can join the IRC channel at #drift on irc.libera.chat or [reach me on twitter](https://twitter.com/Max_Leiter). If you don't have an IRC client yet, you can use a webclient [here](https://demo.thelounge.chat/#/connect?join=%23drift&nick=drift-user&realname=Drift%20User).
|
If you want to contribute, need support, or want to stay updated, you can join the IRC channel at #drift on irc.libera.chat or [reach me on twitter](https://twitter.com/Max_Leiter). If you don't have an IRC client yet, you can use a webclient [here](https://demo.thelounge.chat/#/connect?join=%23drift&nick=drift-user&realname=Drift%20User).
|
||||||
|
|
||||||
## Current status
|
## Current status
|
||||||
|
|
||||||
Drit is a major work in progress. Below is a (rough) list of completed and envisioned features. If you want to help address any of them, please let me know regardless of your experience and I'll be happy to assist.
|
Drit is a major work in progress. Below is a (rough) list of completed and envisioned features. If you want to help address any of them, please let me know regardless of your experience and I'll be happy to assist.
|
||||||
|
|
||||||
- [x] creating and sharing private, public, unlisted posts
|
- [x] creating and sharing private, public, unlisted posts
|
||||||
|
@ -17,7 +17,7 @@ Drit is a major work in progress. Below is a (rough) list of completed and envis
|
||||||
- [x] responsive UI
|
- [x] responsive UI
|
||||||
- [x] user auth
|
- [x] user auth
|
||||||
- [ ] SSO via HTTP header (Issue: [#11](https://github.com/MaxLeiter/Drift/issues/11))
|
- [ ] SSO via HTTP header (Issue: [#11](https://github.com/MaxLeiter/Drift/issues/11))
|
||||||
- [ ] downloading files (individually and entire posts)
|
- [x] downloading files (individually and entire posts)
|
||||||
- [ ] password protected posts
|
- [ ] password protected posts
|
||||||
- [ ] sqlite database (should be very easy to set-up; the ORM is just currently set to memory for ease of development)
|
- [ ] sqlite database (should be very easy to set-up; the ORM is just currently set to memory for ease of development)
|
||||||
- [ ] non-node backend
|
- [ ] non-node backend
|
||||||
|
|
|
@ -3,9 +3,8 @@ import { useRouter } from "next/router";
|
||||||
|
|
||||||
const Link = (props: LinkProps) => {
|
const Link = (props: LinkProps) => {
|
||||||
const { basePath } = useRouter();
|
const { basePath } = useRouter();
|
||||||
const propHrefWithoutLeadingSlash = props.href && props.href.startsWith("/") ? props.href.substr(1) : props.href;
|
const propHrefWithoutLeadingSlash = props.href && props.href.startsWith("/") ? props.href.substring(1) : props.href;
|
||||||
const href = basePath ? `${basePath}/${propHrefWithoutLeadingSlash}` : props.href;
|
const href = basePath ? `${basePath}/${propHrefWithoutLeadingSlash}` : props.href;
|
||||||
(href)
|
|
||||||
return <GeistLink {...props} href={href} />
|
return <GeistLink {...props} href={href} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ const Auth = ({ page }: { page: "signup" | "signin" }) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (signingIn) {
|
if (signingIn) {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/auth/signin', reqOpts)
|
const resp = await fetch('/server-api/auth/signin', reqOpts)
|
||||||
const json = await resp.json()
|
const json = await resp.json()
|
||||||
handleJson(json)
|
handleJson(json)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
@ -43,7 +43,7 @@ const Auth = ({ page }: { page: "signup" | "signin" }) => {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/auth/signup', reqOpts)
|
const resp = await fetch('/server-api/auth/signup', reqOpts)
|
||||||
const json = await resp.json()
|
const json = await resp.json()
|
||||||
handleJson(json)
|
handleJson(json)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|
|
@ -29,3 +29,13 @@
|
||||||
.textarea {
|
.textarea {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actionWrapper {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionWrapper .actions {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { ButtonGroup, Button } from "@geist-ui/core"
|
import { ButtonGroup, Button } from "@geist-ui/core"
|
||||||
import { Bold, Italic, Link, Image as ImageIcon } from '@geist-ui/icons'
|
import { Bold, Italic, Link, Image as ImageIcon } from '@geist-ui/icons'
|
||||||
import { RefObject, useCallback, useMemo } from "react"
|
import { RefObject, useCallback, useMemo } from "react"
|
||||||
|
import styles from '../document.module.css'
|
||||||
|
|
||||||
// TODO: clean up
|
// TODO: clean up
|
||||||
|
|
||||||
|
@ -122,11 +123,8 @@ const FormattingIcons = ({ textareaRef, setText }: { textareaRef?: RefObject<HTM
|
||||||
], [handleBoldClick, handleImageClick, handleItalicClick, handleLinkClick])
|
], [handleBoldClick, handleImageClick, handleItalicClick, handleLinkClick])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'relative', zIndex: 1 }}>
|
<div className={styles.actionWrapper}>
|
||||||
<ButtonGroup style={{
|
<ButtonGroup className={styles.actions}>
|
||||||
position: 'absolute',
|
|
||||||
right: 0,
|
|
||||||
}}>
|
|
||||||
{formattingActions.map(({ icon, name, action }) => (
|
{formattingActions.map(({ icon, name, action }) => (
|
||||||
<Button auto scale={2 / 3} px={0.6} aria-label={name} key={name} icon={icon} onMouseDown={(e) => e.preventDefault()} onClick={action} />
|
<Button auto scale={2 / 3} px={0.6} aria-label={name} key={name} icon={icon} onMouseDown={(e) => e.preventDefault()} onClick={action} />
|
||||||
))}
|
))}
|
|
@ -1,10 +1,11 @@
|
||||||
import { Button, Card, Input, Spacer, Tabs, Textarea } from "@geist-ui/core"
|
import { Button, ButtonGroup, Card, Input, Spacer, Tabs, Textarea, Tooltip } from "@geist-ui/core"
|
||||||
import { ChangeEvent, memo, useMemo, useRef, useState } from "react"
|
import { ChangeEvent, memo, useCallback, useMemo, useRef, useState } from "react"
|
||||||
import styles from './document.module.css'
|
import styles from './document.module.css'
|
||||||
import MarkdownPreview from '../preview'
|
import MarkdownPreview from '../preview'
|
||||||
import { Trash } from '@geist-ui/icons'
|
import { Trash, Download, ExternalLink } from '@geist-ui/icons'
|
||||||
import FormattingIcons from "../formatting-icons"
|
import FormattingIcons from "./formatting-icons"
|
||||||
import Skeleton from "react-loading-skeleton"
|
import Skeleton from "react-loading-skeleton"
|
||||||
|
// import Link from "next/link"
|
||||||
type Props = {
|
type Props = {
|
||||||
editable?: boolean
|
editable?: boolean
|
||||||
remove?: () => void
|
remove?: () => void
|
||||||
|
@ -14,9 +15,38 @@ type Props = {
|
||||||
setContent?: (content: string) => void
|
setContent?: (content: string) => void
|
||||||
initialTab?: "edit" | "preview"
|
initialTab?: "edit" | "preview"
|
||||||
skeleton?: boolean
|
skeleton?: boolean
|
||||||
|
id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const Document = ({ remove, editable, title, content, setTitle, setContent, initialTab = 'edit', skeleton }: Props) => {
|
const DownloadButton = ({ rawLink }: { rawLink?: string }) => {
|
||||||
|
return (<div className={styles.actionWrapper}>
|
||||||
|
<ButtonGroup className={styles.actions}>
|
||||||
|
<Tooltip text="Download">
|
||||||
|
<a href={`${rawLink}?download=true`} target="_blank" rel="noopener noreferrer">
|
||||||
|
<Button
|
||||||
|
scale={2 / 3} px={0.6}
|
||||||
|
icon={<Download />}
|
||||||
|
auto
|
||||||
|
aria-label="Download"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip text="Open raw in new tab">
|
||||||
|
<a href={rawLink} target="_blank" rel="noopener noreferrer">
|
||||||
|
<Button
|
||||||
|
scale={2 / 3} px={0.6}
|
||||||
|
icon={<ExternalLink />}
|
||||||
|
auto
|
||||||
|
aria-label="Open raw file in new tab"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const Document = ({ remove, editable, title, content, setTitle, setContent, initialTab = 'edit', skeleton, id }: Props) => {
|
||||||
const codeEditorRef = useRef<HTMLTextAreaElement>(null)
|
const codeEditorRef = useRef<HTMLTextAreaElement>(null)
|
||||||
const [tab, setTab] = useState(initialTab)
|
const [tab, setTab] = useState(initialTab)
|
||||||
const height = editable ? "500px" : '100%'
|
const height = editable ? "500px" : '100%'
|
||||||
|
@ -47,6 +77,13 @@ const Document = ({ remove, editable, title, content, setTitle, setContent, init
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rawLink = useMemo(() => {
|
||||||
|
if (id) {
|
||||||
|
return `/file/raw/${id}`
|
||||||
|
}
|
||||||
|
}, [id])
|
||||||
|
|
||||||
if (skeleton) {
|
if (skeleton) {
|
||||||
return <>
|
return <>
|
||||||
<Spacer height={1} />
|
<Spacer height={1} />
|
||||||
|
@ -82,6 +119,7 @@ const Document = ({ remove, editable, title, content, setTitle, setContent, init
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.descriptionContainer}>
|
<div className={styles.descriptionContainer}>
|
||||||
{tab === 'edit' && editable && <FormattingIcons setText={setContent} textareaRef={codeEditorRef} />}
|
{tab === 'edit' && editable && <FormattingIcons setText={setContent} textareaRef={codeEditorRef} />}
|
||||||
|
{rawLink && <DownloadButton rawLink={rawLink} />}
|
||||||
<Tabs onChange={handleTabChange} initialValue={initialTab} hideDivider leftSpace={0}>
|
<Tabs onChange={handleTabChange} initialValue={initialTab} hideDivider leftSpace={0}>
|
||||||
<Tabs.Item label={editable ? "Edit" : "Raw"} value="edit">
|
<Tabs.Item label={editable ? "Edit" : "Raw"} value="edit">
|
||||||
{/* <textarea className={styles.lineCounter} wrap='off' readOnly ref={lineNumberRef}>1.</textarea> */}
|
{/* <textarea className={styles.lineCounter} wrap='off' readOnly ref={lineNumberRef}>1.</textarea> */}
|
||||||
|
|
|
@ -175,6 +175,7 @@ const Header = ({ changeTheme, theme }: DriftProps) => {
|
||||||
auto
|
auto
|
||||||
type="abort"
|
type="abort"
|
||||||
onClick={() => setExpanded(!expanded)}
|
onClick={() => setExpanded(!expanded)}
|
||||||
|
aria-label="Menu"
|
||||||
>
|
>
|
||||||
<Spacer height={5 / 6} width={0} />
|
<Spacer height={5 / 6} width={0} />
|
||||||
<MenuIcon />
|
<MenuIcon />
|
||||||
|
|
|
@ -9,7 +9,7 @@ const fetcher = (url: string) => fetch(url, {
|
||||||
}).then(r => r.json())
|
}).then(r => r.json())
|
||||||
|
|
||||||
const MyPosts = () => {
|
const MyPosts = () => {
|
||||||
const { data, error } = useSWR('/api/users/mine', fetcher)
|
const { data, error } = useSWR('/server-api/users/mine', fetcher)
|
||||||
return <PostList posts={data} error={error} />
|
return <PostList posts={data} error={error} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -94,35 +94,37 @@ const allowedFileExtensions = [
|
||||||
'webmanifest',
|
'webmanifest',
|
||||||
]
|
]
|
||||||
|
|
||||||
// TODO: this shouldn't need to know about docs
|
function FileDropzone({ setDocs, docs }: { setDocs: React.Dispatch<React.SetStateAction<Document[]>>, docs: Document[] }) {
|
||||||
function FileDropzone({ setDocs, docs }: { setDocs: (docs: Document[]) => void, docs: Document[] }) {
|
|
||||||
const { palette } = useTheme()
|
const { palette } = useTheme()
|
||||||
const onDrop = useCallback((acceptedFiles) => {
|
const { setToast } = useToasts()
|
||||||
acceptedFiles.forEach((file: File) => {
|
const onDrop = useCallback(async (acceptedFiles) => {
|
||||||
|
const newDocs = await Promise.all(acceptedFiles.map((file: File) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
|
|
||||||
reader.onabort = () => console.log('file reading was aborted')
|
reader.onabort = () => setToast({ text: 'File reading was aborted', type: 'error' })
|
||||||
reader.onerror = () => console.log('file reading has failed')
|
reader.onerror = () => setToast({ text: 'File reading failed', type: 'error' })
|
||||||
reader.onload = () => {
|
reader.onload = () => {
|
||||||
const content = reader.result as string
|
const content = reader.result as string
|
||||||
if (docs.length === 1 && docs[0].content === '') {
|
resolve({
|
||||||
setDocs([{
|
|
||||||
title: file.name,
|
title: file.name,
|
||||||
content,
|
content,
|
||||||
id: generateUUID()
|
id: generateUUID()
|
||||||
}])
|
})
|
||||||
} else {
|
|
||||||
setDocs([...docs, {
|
|
||||||
title: file.name,
|
|
||||||
content,
|
|
||||||
id: generateUUID()
|
|
||||||
}])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
reader.readAsText(file)
|
reader.readAsText(file)
|
||||||
})
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
}, [docs, setDocs])
|
if (docs.length === 1) {
|
||||||
|
if (docs[0].content === '') {
|
||||||
|
setDocs(newDocs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setDocs((oldDocs) => [...oldDocs, ...newDocs])
|
||||||
|
}, [setDocs, setToast, docs])
|
||||||
|
|
||||||
const validator = (file: File) => {
|
const validator = (file: File) => {
|
||||||
// TODO: make this configurable
|
// TODO: make this configurable
|
||||||
|
|
|
@ -31,7 +31,7 @@ const Post = () => {
|
||||||
|
|
||||||
const onSubmit = async (visibility: string) => {
|
const onSubmit = async (visibility: string) => {
|
||||||
setSubmitting(true)
|
setSubmitting(true)
|
||||||
const response = await fetch('/api/posts/create', {
|
const response = await fetch('/server-api/posts/create', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|
|
@ -16,7 +16,7 @@ const useSignedIn = ({ redirectIfNotAuthed = false }: { redirectIfNotAuthed?: bo
|
||||||
async function checkToken() {
|
async function checkToken() {
|
||||||
const token = localStorage.getItem('drift-token')
|
const token = localStorage.getItem('drift-token')
|
||||||
if (token) {
|
if (token) {
|
||||||
const response = await fetch('/api/auth/verify-token', {
|
const response = await fetch('/server-api/auth/verify-token', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${token}`
|
'Authorization': `Bearer ${token}`
|
||||||
|
|
|
@ -10,9 +10,13 @@ const nextConfig = {
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
source: "/api/:path*",
|
source: "/server-api/:path*",
|
||||||
destination: `${process.env.API_URL}/:path*`,
|
destination: `${process.env.API_URL}/:path*`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
source: "/file/raw/:id",
|
||||||
|
destination: `/api/raw/:id`,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
"@fec/remark-a11y-emoji": "^3.1.0",
|
"@fec/remark-a11y-emoji": "^3.1.0",
|
||||||
"@geist-ui/core": "^2.3.5",
|
"@geist-ui/core": "^2.3.5",
|
||||||
"@geist-ui/icons": "^1.0.1",
|
"@geist-ui/icons": "^1.0.1",
|
||||||
|
"client-zip": "^2.0.0",
|
||||||
"comlink": "^4.3.1",
|
"comlink": "^4.3.1",
|
||||||
"dotenv": "^16.0.0",
|
"dotenv": "^16.0.0",
|
||||||
"next": "12.1.0",
|
"next": "12.1.0",
|
||||||
|
|
24
client/pages/api/raw/[id].ts
Normal file
24
client/pages/api/raw/[id].ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { NextApiRequest, NextApiResponse } from "next"
|
||||||
|
|
||||||
|
const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
const { id, download } = req.query
|
||||||
|
const file = await fetch(`${process.env.API_URL}/files/raw/${id}`)
|
||||||
|
if (file.ok) {
|
||||||
|
const data = await file.json()
|
||||||
|
const { title, content } = data
|
||||||
|
// serve the file raw as plain text
|
||||||
|
res.setHeader("Content-Type", "text/plain")
|
||||||
|
res.setHeader('Cache-Control', 's-maxage=86400');
|
||||||
|
if (download) {
|
||||||
|
res.setHeader("Content-Disposition", `attachment; filename="${title}"`)
|
||||||
|
} else {
|
||||||
|
res.setHeader("Content-Disposition", `inline; filename="${title}"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).send(content)
|
||||||
|
} else {
|
||||||
|
res.status(404).send("File not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getRawFile
|
|
@ -1,4 +1,4 @@
|
||||||
import { Page, Text } from "@geist-ui/core";
|
import { Button, Page, Text } from "@geist-ui/core";
|
||||||
import Skeleton from 'react-loading-skeleton';
|
import Skeleton from 'react-loading-skeleton';
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
@ -19,7 +19,7 @@ const Post = ({ theme, changeTheme }: ThemeProps) => {
|
||||||
async function fetchPost() {
|
async function fetchPost() {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
if (router.query.id) {
|
if (router.query.id) {
|
||||||
const post = await fetch(`/api/posts/${router.query.id}`, {
|
const post = await fetch(`/server-api/posts/${router.query.id}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
@ -46,6 +46,23 @@ const Post = ({ theme, changeTheme }: ThemeProps) => {
|
||||||
fetchPost()
|
fetchPost()
|
||||||
}, [router, router.query.id])
|
}, [router, router.query.id])
|
||||||
|
|
||||||
|
const download = async () => {
|
||||||
|
const clientZip = require("client-zip")
|
||||||
|
|
||||||
|
const blob = await clientZip.downloadZip(post.files.map((file: any) => {
|
||||||
|
return {
|
||||||
|
name: file.title,
|
||||||
|
input: file.content,
|
||||||
|
lastModified: new Date(file.updatedAt)
|
||||||
|
}
|
||||||
|
})).blob()
|
||||||
|
const link = document.createElement("a")
|
||||||
|
link.href = URL.createObjectURL(blob)
|
||||||
|
link.download = `${post.title}.zip`
|
||||||
|
link.click()
|
||||||
|
link.remove()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page width={"100%"}>
|
<Page width={"100%"}>
|
||||||
<Head>
|
<Head>
|
||||||
|
@ -62,10 +79,17 @@ const Post = ({ theme, changeTheme }: ThemeProps) => {
|
||||||
{!error && isLoading && <><Text h2><Skeleton width={400} /></Text>
|
{!error && isLoading && <><Text h2><Skeleton width={400} /></Text>
|
||||||
<Document skeleton={true} />
|
<Document skeleton={true} />
|
||||||
</>}
|
</>}
|
||||||
{!isLoading && post && <><Text h2>{post.title} <VisibilityBadge visibility={post.visibility} /></Text>
|
{!isLoading && post && <>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Text h2>{post.title} <VisibilityBadge visibility={post.visibility} /></Text>
|
||||||
|
<Button auto onClick={download}>
|
||||||
|
Download as Zip
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
{post.files.map(({ id, content, title }: { id: any, content: string, title: string }) => (
|
{post.files.map(({ id, content, title }: { id: any, content: string, title: string }) => (
|
||||||
<Document
|
<Document
|
||||||
key={id}
|
key={id}
|
||||||
|
id={id}
|
||||||
content={content}
|
content={content}
|
||||||
title={title}
|
title={title}
|
||||||
editable={false}
|
editable={false}
|
||||||
|
|
|
@ -480,6 +480,11 @@ character-reference-invalid@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560"
|
resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560"
|
||||||
integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==
|
integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==
|
||||||
|
|
||||||
|
client-zip@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/client-zip/-/client-zip-2.0.0.tgz#c93676c92ddb40c858da83517c27297a53874f8d"
|
||||||
|
integrity sha512-JFd4zdhxk5F01NmNnBq3+iMgJkkh0ku9NsI1wZlUjZ52inPJX92vR5TlSkjxRhmHJBPI7YqanD71wDEiKhdWtw==
|
||||||
|
|
||||||
clsx@^1.0.4:
|
clsx@^1.0.4:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
|
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as express from 'express';
|
||||||
import * as bodyParser from 'body-parser';
|
import * as bodyParser from 'body-parser';
|
||||||
import * as errorhandler from 'strong-error-handler';
|
import * as errorhandler from 'strong-error-handler';
|
||||||
import * as cors from 'cors';
|
import * as cors from 'cors';
|
||||||
import { posts, users, auth } from './routes';
|
import { posts, users, auth, files } from './routes';
|
||||||
|
|
||||||
export const app = express();
|
export const app = express();
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ app.use(cors(corsOptions));
|
||||||
app.use("/auth", auth)
|
app.use("/auth", auth)
|
||||||
app.use("/posts", posts)
|
app.use("/posts", posts)
|
||||||
app.use("/users", users)
|
app.use("/users", users)
|
||||||
|
app.use("/files", files)
|
||||||
|
|
||||||
app.use(errorhandler({
|
app.use(errorhandler({
|
||||||
debug: process.env.ENV !== 'production',
|
debug: process.env.ENV !== 'production',
|
||||||
|
|
29
server/src/routes/files.ts
Normal file
29
server/src/routes/files.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { Router } from 'express'
|
||||||
|
// import { Movie } from '../models/Post'
|
||||||
|
import { File } from '../../lib/models/File'
|
||||||
|
|
||||||
|
export const files = Router()
|
||||||
|
|
||||||
|
files.get("/raw/:id", async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const file = await File.findOne({
|
||||||
|
where: {
|
||||||
|
id: req.params.id
|
||||||
|
},
|
||||||
|
attributes: ["title", "content"],
|
||||||
|
})
|
||||||
|
// TODO: fix post inclusion
|
||||||
|
// if (file?.post.visibility === 'public' || file?.post.visibility === 'unlisted') {
|
||||||
|
res.setHeader("Cache-Control", "public, max-age=86400");
|
||||||
|
res.json(file);
|
||||||
|
// } else {
|
||||||
|
// TODO: should this be `private, `?
|
||||||
|
// res.setHeader("Cache-Control", "max-age=86400");
|
||||||
|
// res.json(file);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
next(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
export { auth } from './auth';
|
export { auth } from './auth';
|
||||||
export { posts } from './posts';
|
export { posts } from './posts';
|
||||||
export { users } from './users';
|
export { users } from './users';
|
||||||
|
export { files } from './files';
|
||||||
|
|
|
@ -69,7 +69,7 @@ posts.get("/:id", async (req: UserJwtRequest, res, next) => {
|
||||||
{
|
{
|
||||||
model: File,
|
model: File,
|
||||||
as: "files",
|
as: "files",
|
||||||
attributes: ["id", "title", "content", "sha"],
|
attributes: ["id", "title", "content", "sha", "createdAt", "updatedAt"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
model: User,
|
model: User,
|
||||||
|
@ -80,8 +80,11 @@ posts.get("/:id", async (req: UserJwtRequest, res, next) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (post?.visibility === 'public' || post?.visibility === 'unlisted') {
|
if (post?.visibility === 'public' || post?.visibility === 'unlisted') {
|
||||||
|
res.setHeader("Cache-Control", "public, max-age=86400");
|
||||||
res.json(post);
|
res.json(post);
|
||||||
} else {
|
} else {
|
||||||
|
// TODO: should this be `private, `?
|
||||||
|
res.setHeader("Cache-Control", "max-age=86400");
|
||||||
jwt(req, res, () => {
|
jwt(req, res, () => {
|
||||||
res.json(post);
|
res.json(post);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue