begin work on protected posts

This commit is contained in:
Max Leiter 2022-03-21 03:28:06 -07:00
parent 65b0c8f7f3
commit 3f0212c5c6
No known key found for this signature in database
GPG key ID: A3512F2F2F17EBDA
14 changed files with 136 additions and 50 deletions

View file

@ -2,9 +2,9 @@ import React from 'react'
import MoonIcon from '@geist-ui/icons/moon' import MoonIcon from '@geist-ui/icons/moon'
import SunIcon from '@geist-ui/icons/sun' import SunIcon from '@geist-ui/icons/sun'
import { Select } from '@geist-ui/core' import { Select } from '@geist-ui/core'
import { ThemeProps } from '../../pages/_app'
// import { useAllThemes, useTheme } from '@geist-ui/core' // import { useAllThemes, useTheme } from '@geist-ui/core'
import styles from './header.module.css' import styles from './header.module.css'
import { ThemeProps } from '@lib/types'
const Controls = ({ changeTheme, theme }: ThemeProps) => { const Controls = ({ changeTheme, theme }: ThemeProps) => {
const switchThemes = (type: string | string[]) => { const switchThemes = (type: string | string[]) => {

View file

@ -1,29 +1,56 @@
import { Button, ButtonDropdown, useToasts } from '@geist-ui/core' import { Button, ButtonDropdown, Input, Modal, Note, useModal, useToasts } from '@geist-ui/core'
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import generateUUID from '@lib/generate-uuid'; import generateUUID from '@lib/generate-uuid';
import Document from '../document'; import DocumentComponent from '../document';
import FileDropzone from './drag-and-drop'; import FileDropzone from './drag-and-drop';
import styles from './post.module.css' import styles from './post.module.css'
import Title from './title'; import Title from './title';
import Cookies from 'js-cookie' import Cookies from 'js-cookie'
import type { PostVisibility, Document as DocumentType } from '@lib/types';
export type Document = { import PasswordModal from './password';
title: string
content: string
id: string
}
const Post = () => { const Post = () => {
const { setToast } = useToasts() const { setToast } = useToasts()
const router = useRouter(); const router = useRouter();
const [title, setTitle] = useState<string>() const [title, setTitle] = useState<string>()
const [docs, setDocs] = useState<Document[]>([{ const [docs, setDocs] = useState<DocumentType[]>([{
title: '', title: '',
content: '', content: '',
id: generateUUID() id: generateUUID()
}]) }])
const [passwordModalVisible, setPasswordModalVisible] = useState(false)
const sendRequest = useCallback(async (url: string, data: { visibility?: PostVisibility, title?: string, files?: DocumentType[], password?: string, userId: string }) => {
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${Cookies.get('drift-token')}`
},
body: JSON.stringify({
title,
files: docs,
...data,
})
})
if (res.ok) {
const json = await res.json()
router.push(`/post/${json.id}`)
} else {
const json = await res.json()
setToast({
text: json.message,
type: 'error'
})
}
}, [docs, router, setToast, title])
const closePasswordModel = () => {
setPasswordModalVisible(false)
setSubmitting(false)
}
const [isSubmitting, setSubmitting] = useState(false) const [isSubmitting, setSubmitting] = useState(false)
@ -31,29 +58,30 @@ const Post = () => {
setDocs(docs.filter((doc) => doc.id !== id)) setDocs(docs.filter((doc) => doc.id !== id))
} }
const onSubmit = async (visibility: string) => { const onSubmit = async (visibility: PostVisibility, password?: string) => {
setSubmitting(true) setSubmitting(true)
const response = await fetch('/server-api/posts/create', {
method: 'POST', if (visibility === 'protected' && !password) {
headers: { setPasswordModalVisible(true)
'Content-Type': 'application/json', return
'Authorization': `Bearer ${Cookies.get("drift-token")}` }
},
body: JSON.stringify({ await sendRequest('/server-api/posts/create', {
title, title,
files: docs, files: docs,
visibility, visibility,
userId: Cookies.get("drift-userid"), password,
}) userId: Cookies.get('drift-userid') || ''
}) })
const json = await response.json()
setSubmitting(false) setSubmitting(false)
if (json.id)
router.push(`/post/${json.id}`)
else {
setToast({ text: json.error.message, type: "error" })
} }
const onClosePasswordModal = () => {
setPasswordModalVisible(false)
setSubmitting(false)
} }
const updateTitle = useCallback((title: string, id: string) => { const updateTitle = useCallback((title: string, id: string) => {
@ -64,7 +92,7 @@ const Post = () => {
setDocs(docs.map((doc) => doc.id === id ? { ...doc, content } : doc)) setDocs(docs.map((doc) => doc.id === id ? { ...doc, content } : doc))
}, [docs]) }, [docs])
const uploadDocs = useCallback((files: Document[]) => { const uploadDocs = useCallback((files: DocumentType[]) => {
// if no title is set and the only document is empty, // if no title is set and the only document is empty,
const isFirstDocEmpty = docs.length === 1 && docs[0].title === '' && docs[0].content === '' const isFirstDocEmpty = docs.length === 1 && docs[0].title === '' && docs[0].content === ''
const shouldSetTitle = !title && isFirstDocEmpty const shouldSetTitle = !title && isFirstDocEmpty
@ -87,7 +115,7 @@ const Post = () => {
{ {
docs.map(({ content, id, title }) => { docs.map(({ content, id, title }) => {
return ( return (
<Document <DocumentComponent
remove={() => remove(id)} remove={() => remove(id)}
key={id} key={id}
editable={true} editable={true}
@ -120,7 +148,9 @@ const Post = () => {
<ButtonDropdown.Item main onClick={() => onSubmit('private')}>Create Private</ButtonDropdown.Item> <ButtonDropdown.Item main onClick={() => onSubmit('private')}>Create Private</ButtonDropdown.Item>
<ButtonDropdown.Item onClick={() => onSubmit('public')} >Create Public</ButtonDropdown.Item> <ButtonDropdown.Item onClick={() => onSubmit('public')} >Create Public</ButtonDropdown.Item>
<ButtonDropdown.Item onClick={() => onSubmit('unlisted')} >Create Unlisted</ButtonDropdown.Item> <ButtonDropdown.Item onClick={() => onSubmit('unlisted')} >Create Unlisted</ButtonDropdown.Item>
<ButtonDropdown.Item onClick={() => onSubmit('protected')} >Create with Password</ButtonDropdown.Item>
</ButtonDropdown> </ButtonDropdown>
<PasswordModal isOpen={passwordModalVisible} onClose={onClosePasswordModal} onSubmit={(password) => onSubmit('protected', password)} />
</div> </div>
</div > </div >
) )

View file

@ -0,0 +1,50 @@
import { Input, Modal, Note, Spacer } from "@geist-ui/core"
import { useState } from "react"
type Props = {
isOpen: boolean
onClose: () => void
onSubmit: (password: string) => void
}
const PasswordModal = ({ isOpen, onClose, onSubmit: onSubmitAfterVerify }: Props) => {
const [password, setPassword] = useState<string>()
const [confirmPassword, setConfirmPassword] = useState<string>()
const [error, setError] = useState<string>()
const onSubmit = () => {
if (!password || !confirmPassword) {
setError('Please enter a password')
return
}
if (password !== confirmPassword) {
setError("Passwords do not match")
return
}
onSubmitAfterVerify(password)
}
return (<>
{<Modal visible={isOpen} >
<Modal.Title>Enter a password</Modal.Title>
<Modal.Content>
{!error && <Note type="warning" label='Warning'>
This doesn&apos;t protect your post from the server administrator.
</Note>}
{error && <Note type="error" label='Error'>
{error}
</Note>}
<Spacer />
<Input width={"100%"} label="Password" marginBottom={1} htmlType="password" placeholder="Password" onChange={(e) => setPassword(e.target.value)} />
<Input width={"100%"} label="Confirm" htmlType="password" placeholder="Confirm Password" onChange={(e) => setConfirmPassword(e.target.value)} />
</Modal.Content>
<Modal.Action passive onClick={onClose}>Cancel</Modal.Action>
<Modal.Action onClick={onSubmit}>Submit</Modal.Action>
</Modal>}
</>)
}
export default PasswordModal

View file

@ -1,6 +1,5 @@
import { Badge } from "@geist-ui/core" import { Badge } from "@geist-ui/core"
import { Visibility } from "@lib/types"
type Visibility = "unlisted" | "private" | "public"
type Props = { type Props = {
visibility: Visibility visibility: Visibility

12
client/lib/types.d.ts vendored Normal file
View file

@ -0,0 +1,12 @@
export type PostVisibility = "unlisted" | "private" | "public" | "protected"
export type ThemeProps = {
theme: "light" | "dark" | string,
changeTheme: () => void
}
export type Document = {
title: string
content: string
id: string
}

View file

@ -7,11 +7,7 @@ import useSharedState from '@lib/hooks/use-shared-state';
import 'react-loading-skeleton/dist/skeleton.css' import 'react-loading-skeleton/dist/skeleton.css'
import { SkeletonTheme } from 'react-loading-skeleton'; import { SkeletonTheme } from 'react-loading-skeleton';
import Head from 'next/head'; import Head from 'next/head';
import { ThemeProps } from '@lib/types';
export type ThemeProps = {
theme: "light" | "dark" | string,
changeTheme: () => void
}
type AppProps<P = any> = { type AppProps<P = any> = {
pageProps: P; pageProps: P;

View file

@ -2,11 +2,11 @@ import styles from '@styles/Home.module.css'
import { Page, Spacer, Text } from '@geist-ui/core' import { Page, Spacer, Text } from '@geist-ui/core'
import Header from '@components/header' import Header from '@components/header'
import { ThemeProps } from './_app'
import Document from '@components/document' import Document from '@components/document'
import Image from 'next/image' import Image from 'next/image'
import ShiftBy from '@components/shift-by' import ShiftBy from '@components/shift-by'
import PageSeo from '@components/page-seo' import PageSeo from '@components/page-seo'
import { ThemeProps } from '@lib/types'
export function getStaticProps() { export function getStaticProps() {
const introDoc = process.env.WELCOME_CONTENT const introDoc = process.env.WELCOME_CONTENT

View file

@ -3,12 +3,10 @@ import NewPost from '@components/new-post'
import { Page } from '@geist-ui/core' import { Page } from '@geist-ui/core'
import useSignedIn from '@lib/hooks/use-signed-in' import useSignedIn from '@lib/hooks/use-signed-in'
import Header from '@components/header' import Header from '@components/header'
import { ThemeProps } from './_app'
import { useRouter } from 'next/router'
import PageSeo from '@components/page-seo' import PageSeo from '@components/page-seo'
import { ThemeProps } from '@lib/types'
const New = ({ theme, changeTheme }: ThemeProps) => { const New = ({ theme, changeTheme }: ThemeProps) => {
const router = useRouter()
const isSignedIn = useSignedIn() const isSignedIn = useSignedIn()
return ( return (

View file

@ -6,10 +6,10 @@ import { useEffect, useState } from "react";
import Document from '../../components/document' import Document from '../../components/document'
import Header from "../../components/header"; import Header from "../../components/header";
import VisibilityBadge from "../../components/visibility-badge"; import VisibilityBadge from "../../components/visibility-badge";
import { ThemeProps } from "../_app";
import PageSeo from "components/page-seo"; import PageSeo from "components/page-seo";
import styles from './styles.module.css'; import styles from './styles.module.css';
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { ThemeProps } from "@lib/types";
const Post = ({ theme, changeTheme }: ThemeProps) => { const Post = ({ theme, changeTheme }: ThemeProps) => {
const [post, setPost] = useState<any>() const [post, setPost] = useState<any>()

View file

@ -2,7 +2,7 @@ import { Page } from "@geist-ui/core";
import PageSeo from "@components/page-seo"; import PageSeo from "@components/page-seo";
import Auth from "@components/auth"; import Auth from "@components/auth";
import Header from "@components/header"; import Header from "@components/header";
import { ThemeProps } from "./_app"; import { ThemeProps } from "@lib/types";
const SignIn = ({ theme, changeTheme }: ThemeProps) => ( const SignIn = ({ theme, changeTheme }: ThemeProps) => (
<Page width={"100%"}> <Page width={"100%"}>

View file

@ -2,7 +2,7 @@ import { Page } from "@geist-ui/core";
import Auth from "@components/auth"; import Auth from "@components/auth";
import Header from "@components/header"; import Header from "@components/header";
import PageSeo from '@components/page-seo'; import PageSeo from '@components/page-seo';
import { ThemeProps } from "./_app"; import { ThemeProps } from "@lib/types";
const SignUp = ({ theme, changeTheme }: ThemeProps) => ( const SignUp = ({ theme, changeTheme }: ThemeProps) => (
<Page width="100%"> <Page width="100%">

View file

@ -48,6 +48,9 @@ export class Post extends Model {
@Column @Column
visibility!: string; visibility!: string;
@Column
password?: string;
@UpdatedAt @UpdatedAt
@Column @Column
updatedAt!: Date; updatedAt!: Date;

View file

@ -26,7 +26,6 @@ posts.post('/create', jwt, async (req, res, next) => {
throw new Error("Please provide a visibility.") throw new Error("Please provide a visibility.")
} }
// Create the "post" object
const newPost = new Post({ const newPost = new Post({
title: req.body.title, title: req.body.title,
visibility: req.body.visibility, visibility: req.body.visibility,
@ -35,7 +34,6 @@ posts.post('/create', jwt, async (req, res, next) => {
await newPost.save() await newPost.save()
await newPost.$add('users', req.body.userId); await newPost.$add('users', req.body.userId);
const newFiles = await Promise.all(req.body.files.map(async (file) => { const newFiles = await Promise.all(req.body.files.map(async (file) => {
// Establish a "file" for each file in the request
const newFile = new File({ const newFile = new File({
title: file.title, title: file.title,
content: file.content, content: file.content,

View file

@ -4,7 +4,7 @@ import config from '../lib/config';
import { sequelize } from '../lib/sequelize'; import { sequelize } from '../lib/sequelize';
(async () => { (async () => {
await sequelize.sync(); await sequelize.sync({ force: true });
createServer(app) createServer(app)
.listen( .listen(
config.port, config.port,