refactor: middlewares (#40)

This commit is contained in:
cinwell.li 2021-05-08 14:13:55 +08:00 committed by GitHub
parent 399b6a8ac4
commit c4ff336c6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 516 additions and 415 deletions

View File

@ -135,9 +135,6 @@ PASSWORD=
# bucketnamenamespace and region “ap-chuncheon-1” need check your profile and https://docs.oracle.com/en-us/iaas/api/#/en/s3objectstorage/20160918/
```
### Exoscale
`.env`
@ -152,24 +149,23 @@ STORE_FORCE_PATH_STYLE=true
PASSWORD=
```
Other services that support the s3 protocol can also be used.
Contribution examples are welcome.
## Environment variables
| Name | Description | Default | Optional | Required |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------- | --------- | -------- | -------- |
| PASSWORD | Password to login to the app | | | true |
| STORE_ACCESS_KEY | AccessKey | | | true |
| STORE_SECRET_KEY | SecretKey | | | true |
| STORE_BUCKET | Bucket | | | true |
| STORE_END_POINT | Host name or an IP address. | | | |
| STORE_REGION | region | us-east-1 | | |
| STORE_FORCE_PATH_STYLE | Whether to force path style URLs for S3 objects | false | | |
| COOKIE_SECURE | Only works under https: scheme **If the website is not https, you may not be able to log in, you need to set it to false** | true | | |
| BASE_URL | The domain of the website, used for SEO | | | |
| DISABLE_PASSWORD | Disable password protection | false | | |
| Name | Description | Default | Optional | Required |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | -------- | -------- |
| PASSWORD | Password to login to the app | | | true |
| STORE_ACCESS_KEY | AccessKey | | | true |
| STORE_SECRET_KEY | SecretKey | | | true |
| STORE_BUCKET | Bucket | | | true |
| STORE_END_POINT | Host name or an IP address. | | | |
| STORE_REGION | region | us-east-1 | | |
| STORE_FORCE_PATH_STYLE | Whether to force path style URLs for S3 objects | false | | |
| COOKIE_SECURE | Only works under https: scheme **If the website is not https, you may not be able to log in, and you need to set it to false** | true | | |
| BASE_URL | The domain of the website, used for SEO | | | |
| DISABLE_PASSWORD | Disable password protection. This means that you need to implement authentication on the server yourself, but the route `/share/:id` needs to be accessible anonymously, if you need share page. | false | | |
## Development
@ -182,16 +178,16 @@ yarn dev
### What is S3? And what is MinIO
- Amazon Simple Storage Service (AKA Amazon S3) . TLDR: Read and write stored files or pictures through RESTful API.
- MinIO: a self-hosted S3. Install by docker: docker run -p 9000:9000 minio/minio server /data
- Amazon Simple Storage Service (AKA Amazon S3). TLDR: Read and write stored files or pictures through RESTful API.
- MinIO: a self-hosted S3. Install by docker: `docker run -p 9000:9000 minio/minio server /data`
### Why not use Database?
My understanding: The data stored in Notea is mainly files (such as text or pictures), which is not what the database is good at; Database is more expensive than S3 to read and write small data; Use S3 to generate a signed URL to access files, but the database cannot do it.
Personally speaking, the data stored in Notea is mainly files (such as text or pictures) but the database is not good at reading and writing these type of files; S3 can generate a signed URL to access the remote files, but the database cannot do it.
### Why not use filesystem storage?
Too many excellent offline note-taking apps on the web. Because I couldn't find a product that supports both self-hosted and data synchronization, I create it.
There are many excellent offline note-taking apps supporting filesystem storage available. However, I couldn't find a APP that supports both self-hosted and easy to manage the synchronized data. The purpose of this project is to mitigate the above pain-point.
## LICENSE

View File

@ -6,16 +6,26 @@ import { NextSeo } from 'next-seo'
import { renderMarkdown } from 'libs/web/render-markdown'
// TODO: Maybe can custom
import 'highlight.js/styles/zenburn.css'
import { PageMode } from 'libs/shared/page'
import Error from 'next/error'
import useI18n from 'libs/web/hooks/use-i18n'
export const PostContainer: FC<{ baseURL: string }> = ({ baseURL }) => {
export const PostContainer: FC<{ baseURL: string; pageMode: PageMode }> = ({
baseURL,
pageMode,
}) => {
const { t } = useI18n()
const { note } = NoteState.useContainer()
const content = useMemo(() => renderMarkdown(note?.content ?? ''), [note])
const description = useMemo(
() => removeMarkdown(note?.content).slice(0, 100),
[note]
)
if (pageMode !== PageMode.PUBLIC) {
return <Error statusCode={404} title={t('Not a public page')}></Error>
}
return (
<AriticleStyle className="prose mx-auto prose-sm lg:prose-xl px-4 md:px-0">
<NextSeo

View File

@ -1,6 +1,6 @@
import { TextareaAutosize } from '@material-ui/core'
import useI18n from 'libs/web/hooks/use-i18n'
import { NoteModel } from 'libs/web/state/note'
import { NoteModel } from 'libs/shared/note'
import { has } from 'lodash'
import { useRouter } from 'next/router'
import {

View File

@ -1,7 +1,7 @@
import { FC, RefObject, useCallback } from 'react'
import { use100vh } from 'react-div-100vh'
import useEditState from './edit-state'
import { NoteModel } from 'libs/web/state/note'
import { NoteModel } from 'libs/shared/note'
import MarkdownEditor from 'rich-markdown-editor'
import { DebouncedState } from 'use-debounce/lib/useDebouncedCallback'
import { useTheme } from 'next-themes'

View File

@ -1,5 +1,5 @@
import MarkdownEditor from 'rich-markdown-editor'
import NoteState, { NoteModel } from 'libs/web/state/note'
import NoteState from 'libs/web/state/note'
import { useRef } from 'react'
import router from 'next/router'
import { has } from 'lodash'
@ -7,6 +7,7 @@ import { useDebouncedCallback } from 'use-debounce'
import EditTitle from './edit-title'
import Editor from './editor'
import styled from 'styled-components'
import { NoteModel } from 'libs/shared/note'
const Article = styled.article`
ul {

View File

@ -1,6 +1,6 @@
import NoteTreeState from 'libs/web/state/tree'
import { FC, useEffect } from 'react'
import NoteState, { NoteModel } from 'libs/web/state/note'
import NoteState from 'libs/web/state/note'
import { useResizeDetector } from 'react-resize-detector'
import Sidebar from 'components/sidebar/sidebar'
import UIState from 'libs/web/state/ui'
@ -14,6 +14,7 @@ import SearchModal from 'components/portal/search-modal/search-modal'
import ShareModal from 'components/portal/share-modal'
import { SwipeableDrawer } from '@material-ui/core'
import SidebarMenu from 'components/portal/sidebar-menu'
import { NoteModel } from 'libs/shared/note'
const StyledWrapper = styled.div`
.gutter {
@ -67,7 +68,7 @@ const MobileMainWrapper: FC = ({ children }) => {
}
const LayoutMain: FC<{
tree: TreeModel
tree?: TreeModel
note?: NoteModel
}> = ({ children, tree, note }) => {
const { ua } = UIState.useContainer()

View File

@ -1,10 +1,11 @@
import { NoteModel } from 'libs/shared/note'
import { TreeModel } from 'libs/shared/tree'
import NoteState, { NoteModel } from 'libs/web/state/note'
import NoteState from 'libs/web/state/note'
import NoteTreeState from 'libs/web/state/tree'
import { FC } from 'react'
const LayoutPublic: FC<{
tree: TreeModel
tree?: TreeModel
note?: NoteModel
}> = ({ children, note, tree }) => {
return (

View File

@ -4,7 +4,7 @@ import FilterModal from 'components/portal/filter-modal/filter-modal'
import FilterModalInput from 'components/portal/filter-modal/filter-modal-input'
import FilterModalList from 'components/portal/filter-modal/filter-modal-list'
import SearchItem from './search-item'
import { NoteModel } from 'libs/web/state/note'
import { NoteModel } from 'libs/shared/note'
import PortalState from 'libs/web/state/portal'
import useI18n from 'libs/web/hooks/use-i18n'

View File

@ -7,6 +7,7 @@ import NoteState from 'libs/web/state/note'
import { NOTE_SHARED } from 'libs/shared/meta'
import { useRouter } from 'next/router'
import useI18n from 'libs/web/hooks/use-i18n'
import UIState from 'libs/web/state/ui'
const ShareModal: FC = () => {
const { t } = useI18n()
@ -15,6 +16,7 @@ const ShareModal: FC = () => {
const [copied, setCopied] = useState(false)
const { note, updateNote } = NoteState.useContainer()
const router = useRouter()
const { disablePassword } = UIState.useContainer()
const handleCopy = useCallback(() => {
url && navigator.clipboard.writeText(url)
@ -31,8 +33,12 @@ const ShareModal: FC = () => {
)
useEffect(() => {
setUrl(location.href)
}, [router.query])
if (disablePassword) {
setUrl(`${location.origin}/share/${router.query.id}`)
} else {
setUrl(location.href)
}
}, [disablePassword, router.query])
return (
<Popover

View File

@ -3,7 +3,7 @@ import FilterModal from 'components/portal/filter-modal/filter-modal'
import FilterModalInput from 'components/portal/filter-modal/filter-modal-input'
import FilterModalList from 'components/portal/filter-modal/filter-modal-list'
import TrashItem from './trash-item'
import { NoteModel } from 'libs/web/state/note'
import { NoteModel } from 'libs/shared/note'
import TrashState from 'libs/web/state/trash'
import PortalState from 'libs/web/state/portal'

View File

@ -1,4 +1,4 @@
import { NoteModel } from 'libs/web/state/note'
import { NoteModel } from 'libs/shared/note'
import Link from 'next/link'
import { FC, ReactText, MouseEvent, useCallback } from 'react'
import classNames from 'classnames'

View File

@ -1,29 +0,0 @@
import nc from 'next-connect'
import { onError, useError } from './middlewares/error'
import { NextApiRequest, NextApiResponse } from 'next'
import { API } from './middlewares/error'
import { StoreProvider } from 'libs/server/store'
import { useSession } from './middlewares/session'
import { Session } from 'next-iron-session'
import TreeStore from './tree'
import { useCsrf } from 'libs/server/middlewares/csrf'
export type ApiRequest = NextApiRequest & {
store: StoreProvider
treeStore: TreeStore
session: Session
}
export type ApiResponse = NextApiResponse & {
APIError: typeof API
}
export type ApiNext = () => void
export const api = () =>
nc<ApiRequest, ApiResponse>({
onError,
})
.use(useError)
.use(useSession)
.use(useCsrf)

80
libs/server/connect.ts Normal file
View File

@ -0,0 +1,80 @@
import nc, { Middleware } from 'next-connect'
import { onError, useError } from './middlewares/error'
import {
GetServerSidePropsContext,
NextApiRequest,
NextApiResponse,
Redirect,
} from 'next'
import { API } from './middlewares/error'
import { StoreProvider } from 'libs/server/store'
import { useSession } from './middlewares/session'
import { Session } from 'next-iron-session'
import TreeStore from './tree'
import { useCsrf } from 'libs/server/middlewares/csrf'
import { PageMode } from 'libs/shared/page'
import { NoteModel } from 'libs/shared/note'
import { Settings } from 'libs/shared/settings'
import { TreeModel } from 'libs/shared/tree'
import { UserAgentType } from 'libs/shared/ua'
import { useStore } from './middlewares/store'
export interface ServerState {
store: StoreProvider
treeStore: TreeStore
}
export interface ServerProps {
isLoggedIn: boolean
csrfToken?: string
pageMode: PageMode
note?: NoteModel
baseURL: string
settings?: Settings
lngDict: JSON
tree?: TreeModel
ua?: UserAgentType
disablePassword: boolean
}
export type ApiRequest = NextApiRequest & {
session: Session
state: ServerState
props: ServerProps
redirect: Redirect
}
export type ApiResponse = NextApiResponse & {
APIError: typeof API
}
export type ApiNext = () => void
export const api = () =>
nc<ApiRequest, ApiResponse>({
onError,
})
.use(useError)
.use(useSession)
.use(useCsrf)
// used by getServerSideProps
export const ssr = () =>
nc<ApiRequest, ApiResponse>({
onError,
})
// init props
.use((req, _res, next) => {
req.props = {} as ServerProps
req.state = {} as ServerState
next()
})
.use(useError)
.use(useStore)
export type SSRContext = GetServerSidePropsContext & {
req: ApiRequest
res: ApiResponse
}
export type SSRMiddeware = Middleware<ApiRequest, ApiResponse>

View File

@ -1,7 +1,6 @@
import { getEnv } from 'libs/shared/env'
import { PageMode } from 'libs/shared/page'
import { GetServerSidePropsContext } from 'next'
import { ApiRequest, ApiResponse, ApiNext } from '../api'
import { ApiRequest, ApiResponse, ApiNext, SSRMiddeware } from '../connect'
export async function useAuth(
req: ApiRequest,
@ -15,37 +14,41 @@ export async function useAuth(
return next()
}
export function withAuth(wrapperHandler: any) {
return async function handler(
ctx: GetServerSidePropsContext & {
req: ApiRequest
}
) {
const redirectLogin = {
redirect: {
destination: `/login?redirect=${ctx.resolvedUrl}`,
permanent: false,
},
}
const res = await wrapperHandler(ctx)
if (res.props?.pageMode !== PageMode.PUBLIC && !isLoggedIn(ctx.req)) {
return redirectLogin
}
res.props = {
...res.props,
}
return res
}
}
export function isLoggedIn(req: ApiRequest) {
if (getEnv('IS_DEMO') || getEnv('DISABLE_PASSWORD', false)) {
return true
}
return req.session.get('user')?.isLoggedIn
return !!req.session.get('user')?.isLoggedIn
}
export const applyAuth: SSRMiddeware = async (req, _res, next) => {
req.props = {
...req.props,
isLoggedIn: isLoggedIn(req),
disablePassword: getEnv('IS_DEMO') || getEnv('DISABLE_PASSWORD', false),
}
next()
}
export const applyRedirectLogin: (resolvedUrl: string) => SSRMiddeware = (
resolvedUrl: string
) => async (req, _res, next) => {
const redirect = {
destination: `/login?redirect=${resolvedUrl}`,
permanent: false,
}
// note 存在的情况
if (req.props.pageMode) {
if (req.props.pageMode !== PageMode.PUBLIC && !req.props.isLoggedIn) {
req.redirect = redirect
}
// 访问首页没有 note则判断是否登录
} else if (!req.props.isLoggedIn) {
req.redirect = redirect
}
next()
}

View File

@ -1,10 +1,8 @@
import Tokens from 'csrf'
import { CRSF_HEADER_KEY } from 'libs/shared/crsf'
import { CRSF_HEADER_KEY } from 'libs/shared/const'
import { getEnv } from 'libs/shared/env'
import { PageMode } from 'libs/shared/page'
import md5 from 'md5'
import { GetServerSidePropsContext } from 'next'
import { ApiNext, ApiRequest, ApiResponse } from '../api'
import { ApiNext, ApiRequest, ApiResponse, SSRMiddeware } from '../connect'
const tokens = new Tokens()
@ -16,42 +14,32 @@ export const getCsrfToken = () => tokens.create(csrfSecret)
export const verifyCsrfToken = (token: string) =>
tokens.verify(csrfSecret, token)
export function withCsrf(wrapperHandler: any) {
return async function handler(
ctx: GetServerSidePropsContext & {
req: ApiRequest
}
) {
const res = await wrapperHandler(ctx)
let csrfToken
if (res.redirect) {
return res
}
if (res.pageMode !== PageMode.PUBLIC) {
csrfToken = getCsrfToken()
}
res.props = {
...res.props,
csrfToken,
}
return res
export const applyCsrf: SSRMiddeware = async (req, _res, next) => {
req.props = {
...req.props,
csrfToken: getCsrfToken(),
}
req.session.set(CRSF_HEADER_KEY, req.props.csrfToken)
await req.session.save()
next()
}
const ignoredMethods = ['GET', 'HEAD', 'OPTIONS']
export function useCsrf(req: ApiRequest, res: ApiResponse, next: ApiNext) {
const token = req.headers[CRSF_HEADER_KEY] as string
const sessionToken = req.session.get(CRSF_HEADER_KEY)
if (ignoredMethods.includes(req.method?.toLocaleUpperCase() as string)) {
return next()
}
if (token && verifyCsrfToken(token)) {
if (
token &&
sessionToken &&
token === sessionToken &&
verifyCsrfToken(token)
) {
next()
} else {
return res.APIError.INVALID_CSRF_TOKEN.throw()

View File

@ -1,5 +1,5 @@
import { mapValues } from 'lodash'
import { ApiRequest, ApiResponse, ApiNext } from '../api'
import { ApiRequest, ApiResponse, ApiNext } from '../connect'
export const API_ERROR: {
[key: string]: {
@ -74,8 +74,7 @@ export const API = mapValues(
export async function onError(
err: Error & APIError,
_req: ApiRequest,
res: ApiResponse,
_next: ApiNext
res: ApiResponse
) {
const e = {
name: err.name || 'UNKNOWN_ERR',
@ -88,7 +87,7 @@ export async function onError(
stack: err.stack,
})
res.status(e.status).json(e)
res.status?.(e.status).json?.(e)
}
export async function useError(

View File

@ -1,45 +1,42 @@
import { PageMode } from 'libs/shared/page'
import { NOTE_SHARED } from 'libs/shared/meta'
import { GetServerSidePropsContext } from 'next'
import { getNote } from 'pages/api/notes/[id]'
import { ApiRequest } from '../api'
import { NoteModel } from 'libs/web/state/note'
import { SSRMiddeware } from '../connect'
import { NoteModel } from 'libs/shared/note'
import { getEnv } from 'libs/shared/env'
import { isLoggedIn } from './auth'
const RESERVED_ROUTES = ['new', 'settings', 'login']
export function withNote(wrapperHandler: any) {
return async function handler(
ctx: GetServerSidePropsContext & {
req: ApiRequest
}
) {
const res = await wrapperHandler(ctx)
const id = ctx.query.id as string
const props: { note?: NoteModel; pageMode: PageMode } = {
pageMode: PageMode.NOTE,
}
// todo 页面不存在时应该跳转到新建页
if (!RESERVED_ROUTES.includes(id)) {
try {
props.note = await getNote(ctx.req.store, id)
} catch (e) {
// do nothing
}
}
if (props.note?.shared === NOTE_SHARED.PUBLIC && !isLoggedIn(ctx.req)) {
props.pageMode = PageMode.PUBLIC
}
res.props = {
...res.props,
...props,
baseURL: getEnv('BASE_URL', '//' + ctx.req.headers.host),
}
return res
export const applyNote: (id: string) => SSRMiddeware = (id: string) => async (
req,
_res,
next
) => {
const props: {
note?: NoteModel
pageMode: PageMode
} = {
pageMode: PageMode.NOTE,
}
// todo 页面不存在时应该跳转到新建页
if (!RESERVED_ROUTES.includes(id)) {
try {
props.note = await getNote(req.state.store, id)
} catch (e) {
// do nothing
}
}
if (props.note?.shared === NOTE_SHARED.PUBLIC) {
props.pageMode = PageMode.PUBLIC
}
req.props = {
...req.props,
...props,
baseURL: getEnv('BASE_URL', '//' + req.headers.host),
}
next()
}

View File

@ -1,4 +1,4 @@
import { ironSession, withIronSession, Handler } from 'next-iron-session'
import { ironSession} from 'next-iron-session'
import md5 from 'md5'
import { getEnv } from 'libs/shared/env'
@ -15,5 +15,3 @@ const sessionOptions = {
}
export const useSession = ironSession(sessionOptions)
export const withSession = (handler: Handler) =>
withIronSession(handler, sessionOptions)

View File

@ -1,36 +1,18 @@
import { PageMode } from 'libs/shared/page'
import { GetServerSidePropsContext } from 'next'
import { DEFAULT_SETTINGS } from 'libs/shared/settings'
import { getSettings } from 'pages/api/settings'
import { ApiRequest } from '../api'
import { SSRMiddeware } from '../connect'
export function withSettings(wrapperHandler: any) {
return async function handler(
ctx: GetServerSidePropsContext & {
req: ApiRequest
}
) {
const res = await wrapperHandler(ctx)
let settings
export const applySettings: SSRMiddeware = async (req, _res, next) => {
const settings = await getSettings(req.state.store)
if (res.redirect) {
return res
}
// import language dict
const { default: lngDict = {} } = await import(
`locales/${settings?.locale || DEFAULT_SETTINGS.locale}.json`
)
if (res.pageMode !== PageMode.PUBLIC) {
settings = await getSettings(ctx.req.store)
}
// import language dict
const { default: lngDict = {} } = await import(
`locales/${settings?.locale}.json`
)
res.props = {
...res.props,
settings,
lngDict,
}
return res
req.props = {
...req.props,
...{ settings, lngDict },
}
next()
}

View File

@ -1,27 +1,19 @@
import { createStore } from 'libs/server/store'
import { ApiRequest, ApiResponse, ApiNext } from '../api'
import { GetServerSidePropsContext } from 'next'
import { ApiRequest, SSRMiddeware } from '../connect'
import TreeStore from 'libs/server/tree'
export function useStore(req: ApiRequest, _res: ApiResponse, next: ApiNext) {
export const useStore: SSRMiddeware = async (req, _res, next) => {
applyStore(req)
return next()
}
function applyStore(req: ApiRequest) {
req.store = createStore()
req.treeStore = new TreeStore(req.store)
}
export function applyStore(req: ApiRequest) {
const store = createStore()
export function withStore(wrapperHandler: any) {
return async function handler(
ctx: GetServerSidePropsContext & {
req: ApiRequest
}
) {
applyStore(ctx.req)
return wrapperHandler(ctx)
req.state = {
...req.state,
store,
treeStore: new TreeStore(store),
}
}

View File

@ -1,41 +1,27 @@
import { PageMode } from 'libs/shared/page'
import { GetServerSidePropsContext } from 'next'
import { ApiRequest } from '../api'
import { SSRMiddeware } from '../connect'
import { API } from './error'
// @atlaskit/tree 的依赖
const { resetServerContext } = require('react-beautiful-dnd-next')
export function withTree(wrapperHandler: any) {
return async function handler(
ctx: GetServerSidePropsContext & {
req: ApiRequest
export const applyTree: SSRMiddeware = async (req, _res, next) => {
resetServerContext()
let tree
// todo 分享页面获取指定树结构
if (req.props.isLoggedIn) {
try {
tree = await req.state.treeStore.get()
} catch (error) {
return API.NOT_FOUND.throw(error.message)
}
) {
const res = await wrapperHandler(ctx)
resetServerContext()
let tree
if (res.redirect) {
return res
}
// todo 分享页面获取指定树结构
if (res.pageMode !== PageMode.PUBLIC) {
try {
tree = await ctx.req.treeStore.get()
} catch (error) {
return API.NOT_FOUND.throw(error.message)
}
}
res.props = {
...res.props,
tree,
}
return res
}
req.props = {
...req.props,
...(tree && { tree }),
}
next()
}

View File

@ -1,23 +1,19 @@
import { UserAgentType } from 'libs/web/state/ui/ua'
import { GetServerSidePropsContext } from 'next'
import { UserAgentType } from 'libs/shared/ua'
import UAParser from 'ua-parser-js'
import { SSRMiddeware } from '../connect'
export function withUA(wrapperHandler: any) {
return async function handler(ctx: GetServerSidePropsContext) {
const res = await wrapperHandler(ctx)
const ua = new UAParser(ctx.req.headers['user-agent']).getResult()
export const applyUA: SSRMiddeware = (req, _res, next) => {
const ua = new UAParser(req.headers['user-agent']).getResult()
res.props = {
...res.props,
ua: {
isMobile: ['mobile', 'tablet'].includes(ua.device.type || ''),
isMobileOnly: ua.device.type === 'mobile',
isTablet: ua.device.type === 'tablet',
isBrowser: !ua.device.type,
isWechat: ua.browser.name?.toLocaleLowerCase() === 'wechat',
} as UserAgentType,
}
return res
req.props = {
...req.props,
ua: {
isMobile: ['mobile', 'tablet'].includes(ua.device.type || ''),
isMobileOnly: ua.device.type === 'mobile',
isTablet: ua.device.type === 'tablet',
isBrowser: !ua.device.type,
isWechat: ua.browser.name?.toLocaleLowerCase() === 'wechat',
} as UserAgentType,
}
next()
}

12
libs/shared/note.ts Normal file
View File

@ -0,0 +1,12 @@
import { NOTE_DELETED, NOTE_SHARED } from './meta'
export interface NoteModel {
id: string
title: string
pid?: string
content?: string
pic?: string
date?: string
deleted: NOTE_DELETED
shared: NOTE_SHARED
}

View File

@ -1,5 +1,5 @@
import { moveItemOnTree, mutateTree, TreeData, TreeItem } from '@atlaskit/tree'
import { NoteModel } from 'libs/web/state/note'
import { NoteModel } from 'libs/shared/note'
import { cloneDeep, forEach, pull, reduce, union } from 'lodash'
export interface TreeItemModel extends TreeItem {

7
libs/shared/ua.ts Normal file
View File

@ -0,0 +1,7 @@
export interface UserAgentType {
isMobile: boolean
isMobileOnly: boolean
isTablet: boolean
isBrowser: boolean
isWechat: boolean
}

View File

@ -1,4 +1,4 @@
import { CRSF_HEADER_KEY } from 'libs/shared/crsf'
import { CRSF_HEADER_KEY } from 'libs/shared/const'
import { useCallback, useRef, useState } from 'react'
import CsrfTokenState from '../state/csrf-token'

View File

@ -1,4 +1,4 @@
import { NoteModel } from 'libs/web/state/note'
import { NoteModel } from 'libs/shared/note'
import { useCallback } from 'react'
import noteCache from '../cache/note'
import useFetcher from './fetcher'

View File

@ -1,4 +1,4 @@
import { NoteModel } from 'libs/web/state/note'
import { NoteModel } from 'libs/shared/note'
import localforage from 'localforage'
export const uiCache = localforage.createInstance({

View File

@ -1,6 +1,6 @@
import { TreeModel } from 'libs/shared/tree'
import { noteCacheInstance, NoteCacheItem } from 'libs/web/cache'
import { NoteModel } from 'libs/web/state/note'
import { NoteModel } from 'libs/shared/note'
import { keys, pull } from 'lodash'
import { removeMarkdown } from '../utils/markdown'

View File

@ -4,17 +4,7 @@ import NoteTreeState from 'libs/web/state/tree'
import { NOTE_DELETED, NOTE_SHARED } from 'libs/shared/meta'
import useNoteAPI from '../api/note'
import noteCache from '../cache/note'
export interface NoteModel {
id: string
title: string
pid?: string
content?: string
pic?: string
date?: string
deleted: NOTE_DELETED
shared: NOTE_SHARED
}
import { NoteModel } from 'libs/shared/note'
const useNote = (initData?: NoteModel) => {
const [note, setNote] = useState<NoteModel | undefined>(initData)

View File

@ -1,6 +1,6 @@
import { NoteModel } from 'libs/shared/note'
import { useState, useCallback, MouseEvent } from 'react'
import { createContainer } from 'unstated-next'
import { NoteModel } from './note'
const useModalIntance = () => {
const [visible, setVisible] = useState(false)

View File

@ -1,12 +1,12 @@
import { useState, useCallback } from 'react'
import { createContainer } from 'unstated-next'
import NoteTreeState from './tree'
import { NoteModel } from './note'
import useTrashAPI from '../api/trash'
import useTrashAPI from '../api/trash'
import noteCache from '../cache/note'
import { NOTE_DELETED } from 'libs/shared/meta'
import { NoteCacheItem } from '../cache'
import { searchNote } from '../utils/search'
import { NoteModel } from 'libs/shared/note'
function useTrash() {
const [keyword, setKeyword] = useState<string>()

View File

@ -2,7 +2,6 @@ import { cloneDeep, isEmpty, map } from 'lodash'
import { genId } from 'libs/shared/id'
import { useCallback, useEffect, useRef, useState } from 'react'
import { createContainer } from 'unstated-next'
import { NoteModel } from './note'
import TreeActions, {
DEFAULT_TREE,
movePosition,
@ -13,6 +12,7 @@ import useNoteAPI from '../api/note'
import noteCache from '../cache/note'
import useTreeAPI from '../api/tree'
import { NOTE_DELETED } from 'libs/shared/meta'
import { NoteModel } from 'libs/shared/note'
const useNoteTree = (initData: TreeModel = DEFAULT_TREE) => {
const { mutate, loading } = useTreeAPI()

View File

@ -1,22 +1,32 @@
import { Settings } from 'libs/shared/settings'
import { UserAgentType } from 'libs/shared/ua'
import { createContainer } from 'unstated-next'
import useSettings from './settings'
import useSidebar from './sidebar'
import useSplit from './split'
import useTitle from './title'
import useUA, { UserAgentType } from './ua'
const DEFAULT_UA: UserAgentType = {
isMobile: false,
isMobileOnly: false,
isTablet: false,
isBrowser: true,
isWechat: false,
}
interface Props {
ua?: UserAgentType
settings?: Settings
disablePassword?: boolean
}
function useUI({ ua, settings }: Props = {}) {
function useUI({ ua = DEFAULT_UA, settings, disablePassword }: Props = {}) {
return {
ua: useUA(ua),
ua,
sidebar: useSidebar(ua?.isMobileOnly ? false : settings?.sidebar_is_fold),
split: useSplit(settings?.split_sizes),
title: useTitle(),
settings: useSettings(settings),
disablePassword,
}
}

View File

@ -1,23 +0,0 @@
import { useState } from 'react'
export interface UserAgentType {
isMobile: boolean
isMobileOnly: boolean
isTablet: boolean
isBrowser: boolean
isWechat: boolean
}
export default function useUA(
initState: UserAgentType = {
isMobile: false,
isMobileOnly: false,
isTablet: false,
isBrowser: true,
isWechat: false,
}
) {
const [ua] = useState(initState)
return ua
}

View File

@ -30,5 +30,7 @@
"Sync with system": "mit System synchronisieren",
"Dark": "Dunkel",
"Light": "Hell",
"Current version": "aktuelle Version"
"Current version": "aktuelle Version",
"Not a public page": "Not a public page"
}

View File

@ -30,5 +30,6 @@
"Sync with system": "Sync with system",
"Dark": "Dark",
"Light": "Light",
"Current version": "Current version"
"Current version": "Current version",
"Not a public page": "Not a public page"
}

View File

@ -30,5 +30,6 @@
"Sync with system": "Как в системе",
"Dark": "Темная",
"Light": "Светлая",
"Current version": "Текущая версия"
"Current version": "Текущая версия",
"Not a public page": "Not a public page"
}

View File

@ -30,5 +30,6 @@
"Sync with system": "自动",
"Dark": "深色",
"Light": "浅色",
"Current version": "当前版本"
"Current version": "当前版本",
"Not a public page": "非公开页面"
}

View File

@ -1,44 +1,61 @@
import { NoteModel } from 'libs/web/state/note'
import LayoutMain from 'components/layout/layout-main'
import { GetServerSideProps, NextPage } from 'next'
import { withTree } from 'libs/server/middlewares/tree'
import { withUA } from 'libs/server/middlewares/ua'
import { TreeModel } from 'libs/shared/tree'
import { withSession } from 'libs/server/middlewares/session'
import { withStore } from 'libs/server/middlewares/store'
import { withSettings } from 'libs/server/middlewares/settings'
import { withAuth } from 'libs/server/middlewares/auth'
import { withNote } from 'libs/server/middlewares/note'
import { applyTree } from 'libs/server/middlewares/tree'
import { useSession } from 'libs/server/middlewares/session'
import { applySettings } from 'libs/server/middlewares/settings'
import { applyAuth, applyRedirectLogin } from 'libs/server/middlewares/auth'
import { applyNote } from 'libs/server/middlewares/note'
import LayoutPublic from 'components/layout/layout-public'
import { PageMode } from 'libs/shared/page'
import { EditContainer } from 'components/container/edit-container'
import { PostContainer } from 'components/container/post-container'
import { withCsrf } from 'libs/server/middlewares/csrf'
import { applyCsrf } from 'libs/server/middlewares/csrf'
import { ssr, SSRContext, ServerProps } from 'libs/server/connect'
import { applyUA } from 'libs/server/middlewares/ua'
const EditNotePage: NextPage<{
tree: TreeModel
note?: NoteModel
pageMode: PageMode
baseURL: string
}> = ({ tree, note, pageMode, baseURL }) => {
if (PageMode.PUBLIC === pageMode) {
export default function EditNotePage({
tree,
note,
pageMode,
baseURL,
isLoggedIn,
}: ServerProps) {
if (isLoggedIn) {
return (
<LayoutPublic tree={tree} note={note}>
<PostContainer baseURL={baseURL} />
</LayoutPublic>
<LayoutMain tree={tree} note={note}>
<EditContainer />
</LayoutMain>
)
}
return (
<LayoutMain tree={tree} note={note}>
<EditContainer />
</LayoutMain>
<LayoutPublic tree={tree} note={note}>
<PostContainer pageMode={pageMode} baseURL={baseURL} />
</LayoutPublic>
)
}
export default EditNotePage
export const getServerSideProps = async (
ctx: SSRContext & {
query: {
id: string
}
}
) => {
if (!/^[A-Za-z0-9_-]+$/.test(ctx.query.id)) {
return { props: {} }
}
await ssr()
.use(useSession)
.use(applyAuth)
.use(applyNote(ctx.query.id))
.use(applyRedirectLogin(ctx.resolvedUrl))
.use(applyTree)
.use(applySettings)
.use(applyCsrf)
.use(applyUA)
.run(ctx.req, ctx.res)
export const getServerSideProps: GetServerSideProps = withUA(
withSession(
withStore(withAuth(withNote(withTree(withSettings(withCsrf(() => ({})))))))
)
)
return {
props: ctx.req.props,
redirect: ctx.req.redirect,
}
}

View File

@ -15,6 +15,7 @@ import { Settings } from 'libs/shared/settings'
import I18nProvider from 'libs/web/utils/i18n-provider'
import CsrfTokenState from 'libs/web/state/csrf-token'
import { muiLocale } from 'locales'
import { ServerProps } from 'libs/server/connect'
const handleRejection = (event: any) => {
// react-beautiful-dnd 会捕获到 `ResizeObserver loop limit exceeded`
@ -49,7 +50,13 @@ function DocumentHead() {
)
}
const AppInner = ({ Component, pageProps }: AppProps) => {
const AppInner = ({
Component,
pageProps,
}: {
pageProps: ServerProps
Component: any
}) => {
const { resolvedTheme } = useTheme()
const settings = pageProps?.settings as Settings
const muiTheme = useMemo(
@ -94,6 +101,7 @@ const AppInner = ({ Component, pageProps }: AppProps) => {
initialState={{
ua: pageProps?.ua,
settings,
disablePassword: pageProps?.disablePassword,
}}
>
<PortalState.Provider>

View File

@ -1,4 +1,4 @@
import { api } from 'libs/server/api'
import { api } from 'libs/server/connect'
import { getEnv } from 'libs/shared/env'
export default api().post(async (req, res) => {

View File

@ -1,4 +1,4 @@
import { api } from 'libs/server/api'
import { api } from 'libs/server/connect'
import { useAuth } from 'libs/server/middlewares/auth'
export default api()

View File

@ -1,4 +1,4 @@
import { api } from 'libs/server/api'
import { api } from 'libs/server/connect'
import { useStore } from 'libs/server/middlewares/store'
import { getPathFileByName } from 'libs/server/note-path'
@ -15,7 +15,7 @@ export default api()
.use(useStore)
.get(async (req, res) => {
if (req.query.file) {
const signUrl = await req.store.getSignUrl(
const signUrl = await req.state.store.getSignUrl(
getPathFileByName((req.query.file as string[]).join('/')),
expires
)

View File

@ -1,9 +1,9 @@
import { api } from 'libs/server/api'
import { api } from 'libs/server/connect'
import { metaToJson } from 'libs/server/meta'
import { useAuth } from 'libs/server/middlewares/auth'
import { useStore } from 'libs/server/middlewares/store'
import { getPathNoteById } from 'libs/server/note-path'
import { NoteModel } from 'libs/web/state/note'
import { NoteModel } from 'libs/shared/note'
import { StoreProvider } from 'libs/server/store'
import { API } from 'libs/server/middlewares/error'
import { strCompress } from 'libs/shared/str'
@ -35,8 +35,8 @@ export default api()
const notePath = getPathNoteById(id)
await Promise.all([
req.store.deleteObject(notePath),
req.treeStore.removeItem(id),
req.state.store.deleteObject(notePath),
req.state.treeStore.removeItem(id),
])
res.end()
@ -50,7 +50,7 @@ export default api()
})
}
const note = await getNote(req.store, id)
const note = await getNote(req.state.store, id)
res.json(note)
})
@ -58,13 +58,13 @@ export default api()
const id = req.query.id as string
const { content } = req.body
const notePath = getPathNoteById(id)
const oldMeta = await req.store.getObjectMeta(notePath)
const oldMeta = await req.state.store.getObjectMeta(notePath)
if (oldMeta) {
oldMeta['date'] = strCompress(new Date().toISOString())
}
await req.store.putObject(notePath, content, {
await req.state.store.putObject(notePath, content, {
contentType: 'text/markdown',
meta: oldMeta,
})

View File

@ -1,4 +1,4 @@
import { api } from 'libs/server/api'
import { api } from 'libs/server/connect'
import { jsonToMeta, metaToJson } from 'libs/server/meta'
import { useAuth } from 'libs/server/middlewares/auth'
import { useStore } from 'libs/server/middlewares/store'
@ -11,7 +11,7 @@ export default api()
.post(async (req, res) => {
const id = req.body.id || req.query.id
const notePath = getPathNoteById(id)
const oldMeta = await req.store.getObjectMeta(notePath)
const oldMeta = await req.state.store.getObjectMeta(notePath)
const oldMetaJson = metaToJson(oldMeta)
let meta = jsonToMeta({
...req.body,
@ -24,11 +24,11 @@ export default api()
// 处理删除情况
const { deleted } = req.body
if (oldMetaJson.deleted !== deleted && deleted === NOTE_DELETED.DELETED) {
await req.treeStore.removeItem(id)
await req.state.treeStore.removeItem(id)
}
}
await req.store.copyObject(notePath, notePath, {
await req.state.store.copyObject(notePath, notePath, {
meta,
contentType: 'text/markdown',
})
@ -38,7 +38,7 @@ export default api()
.get(async (req, res) => {
const id = req.body.id || req.query.id
const notePath = getPathNoteById(id)
const meta = await req.store.getObjectMeta(notePath)
const meta = await req.state.store.getObjectMeta(notePath)
res.json(metaToJson(meta))
})

View File

@ -1,5 +1,5 @@
import { genId } from 'libs/shared/id'
import { api } from 'libs/server/api'
import { api } from 'libs/server/connect'
import { jsonToMeta } from 'libs/server/meta'
import { useAuth } from 'libs/server/middlewares/auth'
import { useStore } from 'libs/server/middlewares/store'
@ -14,7 +14,7 @@ export default api()
if (!id) {
id = genId()
while (await req.store.hasObject(getPathNoteById(id))) {
while (await req.state.store.hasObject(getPathNoteById(id))) {
id = genId()
}
}
@ -26,11 +26,11 @@ export default api()
}
const metaData = jsonToMeta(metaWithModel)
await req.store.putObject(getPathNoteById(id), content, {
await req.state.store.putObject(getPathNoteById(id), content, {
contentType: 'text/markdown',
meta: metaData,
})
await req.treeStore.addItem(id, meta.pid)
await req.state.treeStore.addItem(id, meta.pid)
res.json(metaWithModel)
})

View File

@ -1,4 +1,4 @@
import { api } from 'libs/server/api'
import { api } from 'libs/server/connect'
import { useAuth } from 'libs/server/middlewares/auth'
import { useStore } from 'libs/server/middlewares/store'
import { getPathSettings } from 'libs/server/note-path'
@ -23,18 +23,18 @@ export default api()
.use(useStore)
.post(async (req, res) => {
const { body } = req
const prev = await getSettings(req.store)
const prev = await getSettings(req.state.store)
const settings = formatSettings({
...prev,
...body,
})
await req.store.putObject(getPathSettings(), JSON.stringify(settings))
await req.state.store.putObject(getPathSettings(), JSON.stringify(settings))
res.status(204).end()
})
.get(
async (req, res): Promise<void> => {
const settings = await getSettings(req.store)
const settings = await getSettings(req.state.store)
res.json(settings)
}

View File

@ -1,4 +1,4 @@
import { api, ApiRequest } from 'libs/server/api'
import { api, ApiRequest } from 'libs/server/connect'
import { jsonToMeta } from 'libs/server/meta'
import { useAuth } from 'libs/server/middlewares/auth'
import { useStore } from 'libs/server/middlewares/store'
@ -36,13 +36,13 @@ export default api()
async function deleteNote(req: ApiRequest, id: string) {
const notePath = getPathNoteById(id)
await req.store.deleteObject(notePath)
await req.treeStore.deleteItem(id)
await req.state.store.deleteObject(notePath)
await req.state.treeStore.deleteItem(id)
}
async function restoreNote(req: ApiRequest, id: string, parentId = 'root') {
const notePath = getPathNoteById(id)
const oldMeta = await req.store.getObjectMeta(notePath)
const oldMeta = await req.state.store.getObjectMeta(notePath)
let meta = jsonToMeta({
date: new Date().toISOString(),
deleted: NOTE_DELETED.NORMAL.toString(),
@ -51,9 +51,9 @@ async function restoreNote(req: ApiRequest, id: string, parentId = 'root') {
meta = { ...oldMeta, ...meta }
}
await req.store.copyObject(notePath, notePath, {
await req.state.store.copyObject(notePath, notePath, {
meta,
contentType: 'text/markdown',
})
await req.treeStore.restoreItem(id, parentId)
await req.state.treeStore.restoreItem(id, parentId)
}

View File

@ -1,4 +1,4 @@
import { api } from 'libs/server/api'
import { api } from 'libs/server/connect'
import { useAuth } from 'libs/server/middlewares/auth'
import { useStore } from 'libs/server/middlewares/store'
@ -6,7 +6,7 @@ export default api()
.use(useAuth)
.use(useStore)
.get(async (req, res) => {
res.json(await req.treeStore.get())
res.json(await req.state.treeStore.get())
})
.post(async (req, res) => {
const { action, data } = req.body as {
@ -16,11 +16,11 @@ export default api()
switch (action) {
case 'move':
await req.treeStore.moveItem(data.source, data.destination)
await req.state.treeStore.moveItem(data.source, data.destination)
break
case 'mutate':
await req.treeStore.mutateItem(data.id, data)
await req.state.treeStore.mutateItem(data.id, data)
break
default:

View File

@ -1,4 +1,4 @@
import { api } from 'libs/server/api'
import { api } from 'libs/server/connect'
import { useAuth } from 'libs/server/middlewares/auth'
import { useStore } from 'libs/server/middlewares/store'
import { IncomingForm } from 'formidable'
@ -35,7 +35,7 @@ export default api()
)}${extname(file.name)}`
const filePath = getPathFileByName(fileName)
await req.store.putObject(filePath, buffer, {
await req.state.store.putObject(filePath, buffer, {
contentType: file.type,
headers: {
cacheControl:

View File

@ -1,17 +1,17 @@
import LayoutMain from 'components/layout/layout-main'
import { GetServerSideProps, GetServerSidePropsContext, NextPage } from 'next'
import { withTree } from 'libs/server/middlewares/tree'
import { withUA } from 'libs/server/middlewares/ua'
import { NextPage } from 'next'
import { applyTree } from 'libs/server/middlewares/tree'
import { applyUA } from 'libs/server/middlewares/ua'
import { TreeModel } from 'libs/shared/tree'
import { withSession } from 'libs/server/middlewares/session'
import { withStore } from 'libs/server/middlewares/store'
import { withSettings } from 'libs/server/middlewares/settings'
import { withAuth } from 'libs/server/middlewares/auth'
import { useSession } from 'libs/server/middlewares/session'
import { applySettings } from 'libs/server/middlewares/settings'
import { applyAuth, applyRedirectLogin } from 'libs/server/middlewares/auth'
import Link from 'next/link'
import UIState from 'libs/web/state/ui'
import Router from 'next/router'
import { useEffect } from 'react'
import { withCsrf } from 'libs/server/middlewares/csrf'
import { applyCsrf } from 'libs/server/middlewares/csrf'
import { SSRContext, ssr } from 'libs/server/connect'
const EditNotePage: NextPage<{ tree: TreeModel }> = ({ tree }) => {
const { ua } = UIState.useContainer()
@ -39,24 +39,30 @@ const EditNotePage: NextPage<{ tree: TreeModel }> = ({ tree }) => {
export default EditNotePage
function withIndex(wrapperHandler: any) {
return async function handler(ctx: GetServerSidePropsContext) {
const res = await wrapperHandler(ctx)
const lastVisit = res.props?.settings?.last_visit
export const getServerSideProps = async (ctx: SSRContext) => {
await ssr()
.use(useSession)
.use(applyAuth)
.use(applyRedirectLogin(ctx.resolvedUrl))
.use(applyTree)
.use(applySettings)
.use(applyCsrf)
.use(applyUA)
.run(ctx.req, ctx.res)
if (lastVisit && !res.redirect) {
res.redirect = {
const lastVisit = ctx.req.props?.settings?.last_visit
if (lastVisit) {
return {
redirect: {
destination: lastVisit,
permanent: false,
}
},
}
}
return res
return {
props: ctx.req.props,
redirect: ctx.req.redirect,
}
}
export const getServerSideProps: GetServerSideProps = withUA(
withSession(
withStore(withAuth(withTree(withIndex(withSettings(withCsrf(() => ({})))))))
)
)

View File

@ -1,6 +1,8 @@
import { TextField, Button, Snackbar } from '@material-ui/core'
import { Alert } from '@material-ui/lab'
import { withCsrf } from 'libs/server/middlewares/csrf'
import { SSRContext, ssr } from 'libs/server/connect'
import { applyCsrf } from 'libs/server/middlewares/csrf'
import { useSession } from 'libs/server/middlewares/session'
import useFetcher from 'libs/web/api/fetcher'
import router from 'next/router'
import { FormEvent, useCallback } from 'react'
@ -66,4 +68,10 @@ const LoginPage = () => {
export default LoginPage
export const getServerSideProps = withCsrf(() => ({}))
export const getServerSideProps = async (ctx: SSRContext) => {
await ssr().use(useSession).use(applyCsrf).run(ctx.req, ctx.res)
return {
props: ctx.req.props,
}
}

View File

@ -1,16 +1,16 @@
import LayoutMain from 'components/layout/layout-main'
import { GetServerSideProps, NextPage } from 'next'
import { withTree } from 'libs/server/middlewares/tree'
import { withUA } from 'libs/server/middlewares/ua'
import { NextPage } from 'next'
import { applyTree } from 'libs/server/middlewares/tree'
import { applyUA } from 'libs/server/middlewares/ua'
import { TreeModel } from 'libs/shared/tree'
import { withSession } from 'libs/server/middlewares/session'
import { withStore } from 'libs/server/middlewares/store'
import { withSettings } from 'libs/server/middlewares/settings'
import { withAuth } from 'libs/server/middlewares/auth'
import { useSession } from 'libs/server/middlewares/session'
import { applySettings } from 'libs/server/middlewares/settings'
import { applyAuth } from 'libs/server/middlewares/auth'
import { SettingsForm } from 'components/settings/settings-form'
import useI18n from 'libs/web/hooks/use-i18n'
import { withCsrf } from 'libs/server/middlewares/csrf'
import { applyCsrf } from 'libs/server/middlewares/csrf'
import { SettingFooter } from 'components/settings/setting-footer'
import { SSRContext, ssr } from 'libs/server/connect'
const SettingsPage: NextPage<{ tree: TreeModel }> = ({ tree }) => {
const { t } = useI18n()
@ -33,6 +33,18 @@ const SettingsPage: NextPage<{ tree: TreeModel }> = ({ tree }) => {
export default SettingsPage
export const getServerSideProps: GetServerSideProps = withUA(
withSession(withStore(withAuth(withTree(withSettings(withCsrf(() => ({})))))))
)
export const getServerSideProps = async (ctx: SSRContext) => {
await ssr()
.use(useSession)
.use(applyAuth)
.use(applyTree)
.use(applySettings)
.use(applyCsrf)
.use(applyUA)
.run(ctx.req, ctx.res)
return {
props: ctx.req.props,
redirect: ctx.req.redirect,
}
}

42
pages/share/[id].tsx Normal file
View File

@ -0,0 +1,42 @@
import { applyTree } from 'libs/server/middlewares/tree'
import { applyUA } from 'libs/server/middlewares/ua'
import { applySettings } from 'libs/server/middlewares/settings'
import { applyNote } from 'libs/server/middlewares/note'
import LayoutPublic from 'components/layout/layout-public'
import { PostContainer } from 'components/container/post-container'
import { ServerProps, ssr, SSRContext } from 'libs/server/connect'
import { useSession } from 'libs/server/middlewares/session'
export default function SharePage({
tree,
note,
pageMode,
baseURL,
}: ServerProps) {
return (
<LayoutPublic tree={tree} note={note}>
<PostContainer pageMode={pageMode} baseURL={baseURL} />
</LayoutPublic>
)
}
export const getServerSideProps = async (
ctx: SSRContext & {
query: {
id: string
}
}
) => {
await ssr()
.use(useSession)
.use(applyNote(ctx.query.id))
.use(applyTree)
.use(applySettings)
.use(applyUA)
.run(ctx.req, ctx.res)
return {
props: ctx.req.props,
redirect: ctx.req.redirect,
}
}