From c6eb4383145a5645474aab6e6559ef34cb78c44a Mon Sep 17 00:00:00 2001 From: Julien Oculi Date: Tue, 16 Jul 2024 16:34:43 +0200 Subject: [PATCH] fix(island): :bug: transpilation cause `Suspense` to broke promise as children --- components/AutoGrid.tsx | 3 +- components/BlogBlocks.tsx | 8 +- components/Picture.tsx | 34 +++--- dev.ts | 10 +- islands/BlogCardList.tsx | 82 +++++++------- islands/CardList.tsx | 147 ++++++++++++------------ islands/MemberCardList.tsx | 88 +++++++-------- islands/Suspens.tsx | 17 +-- routes/api/members/fetchAll.ts | 32 +++--- routes/api/news/fetchAll.ts | 16 +-- routes/api/webauthn/login/[step].ts | 7 +- routes/api/webauthn/register/[step].ts | 7 +- routes/index.tsx | 34 +++--- src/blog/mod.ts | 148 ++++++++++++------------- src/cache/middleware.ts | 28 ++--- src/csp/middleware.ts | 48 ++++---- src/csp/mod.ts | 62 +++++------ src/db/mod.ts | 4 +- src/security_headers/middleware.ts | 42 +++---- src/serviceworker/middleware.ts | 14 +-- src/session/middleware.ts | 82 +++++++------- 21 files changed, 457 insertions(+), 456 deletions(-) diff --git a/components/AutoGrid.tsx b/components/AutoGrid.tsx index 53ccb5e..fa7f789 100644 --- a/components/AutoGrid.tsx +++ b/components/AutoGrid.tsx @@ -13,8 +13,7 @@ export function AutoGrid(
diff --git a/components/BlogBlocks.tsx b/components/BlogBlocks.tsx index 9199c7e..328a574 100644 --- a/components/BlogBlocks.tsx +++ b/components/BlogBlocks.tsx @@ -69,9 +69,7 @@ export function BlogPost(
@@ -90,9 +88,7 @@ export function BlogPost( function NewsTags({ tags }: Pick) { return (
- {tags - ? tags.map((tag) => {tag}) - : Aucun tag} + {tags ? tags.map((tag) => {tag}) : Aucun tag}
) } diff --git a/components/Picture.tsx b/components/Picture.tsx index e4e888b..1e4fa57 100644 --- a/components/Picture.tsx +++ b/components/Picture.tsx @@ -3,26 +3,26 @@ import { Ensure } from ':types' import { JSX } from 'preact' export type PictureProps = - & { formats: string[] } - & Ensure, 'src' | 'alt' | 'loading'> + & { formats: string[] } + & Ensure, 'src' | 'alt' | 'loading'> export function Picture( - { formats, src, ...props }: PictureProps, + { formats, src, ...props }: PictureProps, ) { - const groups = unwrapSignalOrValue(src)?.match(/(?.*)(?\.\w+)/) - ?.groups - if (groups === undefined) { - throw new SyntaxError(`unable to parse path of "${src.valueOf()}"`) - } + const groups = unwrapSignalOrValue(src)?.match(/(?.*)(?\.\w+)/) + ?.groups + if (groups === undefined) { + throw new SyntaxError(`unable to parse path of "${src.valueOf()}"`) + } - const { path } = groups + const { path } = groups - return ( - - {formats.map((format) => ( - - ))} - - - ) + return ( + + {formats.map((format) => ( + + ))} + + + ) } diff --git a/dev.ts b/dev.ts index 3444176..2fbec6a 100644 --- a/dev.ts +++ b/dev.ts @@ -6,9 +6,9 @@ import config from './fresh.config.ts' import '$std/dotenv/load.ts' await dev(import.meta.url, './main.ts', { - ...config, - server: { - cert: await Deno.readTextFile('./cert/localhost.pem'), - key: await Deno.readTextFile('./cert/localhost-key.pem'), - }, + ...config, + server: { + cert: await Deno.readTextFile('./cert/localhost.pem'), + key: await Deno.readTextFile('./cert/localhost-key.pem'), + }, }) diff --git a/islands/BlogCardList.tsx b/islands/BlogCardList.tsx index 9c69f04..d51022d 100644 --- a/islands/BlogCardList.tsx +++ b/islands/BlogCardList.tsx @@ -3,56 +3,56 @@ import CardList from ':islands/CardList.tsx' import type { Ref } from 'preact' export default function BlogCardList( - { limit, usePlaceholder, useObserver }: { - limit?: number - usePlaceholder?: boolean - useObserver?: boolean - }, + { limit, usePlaceholder, useObserver }: { + limit?: number + usePlaceholder?: boolean + useObserver?: boolean + }, ) { - if (usePlaceholder) { - return ( - - ) - } + if (usePlaceholder) { + return ( + + ) + } - return ( - - ) + return ( + + ) } function builder(news: BlogProps) { - return BlogCard({ ...news, lastUpdate: new Date(news.lastUpdate) }) + return BlogCard({ ...news, lastUpdate: new Date(news.lastUpdate) }) } function Placeholder({ ref }: { ref?: Ref | undefined }) { - return ( -
-

Chargement ...

-
- ) + return ( +
+

Chargement ...

+
+ ) } function Fallback() { - return ( -
-

Pas de news disponible

-
- ) + return ( +
+

Pas de news disponible

+
+ ) } diff --git a/islands/CardList.tsx b/islands/CardList.tsx index 815fead..336e86e 100644 --- a/islands/CardList.tsx +++ b/islands/CardList.tsx @@ -7,97 +7,96 @@ import { useEffect, useRef } from 'preact/hooks' export type Builder = (props: T) => JSX.Element export type CardListProps = { - apiRoute: string - builder: Builder - limit?: number + apiRoute: string + builder: Builder + limit?: number } | { - apiRoute: string - builder: Builder - limit?: number - useObserver?: boolean - placeholder: ({ ref }: { ref: Ref | undefined }) => JSX.Element - fallback: Fallback + apiRoute: string + builder: Builder + limit?: number + useObserver?: boolean + placeholder: ({ ref }: { ref: Ref | undefined }) => JSX.Element + fallback: Fallback } export default function CardList( - { limit, builder, apiRoute, ...props }: CardListProps, + { limit, builder, apiRoute, ...props }: CardListProps, ) { - const list: Signal = useSignal([]) - const ac = new AbortController() - const ref = useRef(null) + const list: Signal = useSignal([]) + const ac = new AbortController() + const ref = useRef(null) - const useObserver = 'useObserver' in props ? props.useObserver : false - const placeholder = 'placeholder' in props ? props.placeholder : false - const fallback = 'fallback' in props ? props.fallback : false + const useObserver = 'useObserver' in props ? props.useObserver : false + const placeholder = 'placeholder' in props ? props.placeholder : false + const fallback = 'fallback' in props ? props.fallback : false - useEffect(() => { - if (ref.current && useObserver) { - const observer = new IntersectionObserver(([entry]) => { - if (entry.isIntersecting) { - fillList(list, builder, apiRoute, { limit, ac }) - observer.disconnect() - } - }, { - rootMargin: '300px', - }) - observer.observe(ref.current) - } else { - fillList(list, builder, apiRoute, { limit, ac }) - } - }) + useEffect(() => { + if (ref.current && useObserver) { + const observer = new IntersectionObserver(([entry]) => { + if (entry.isIntersecting) { + fillList(list, builder, apiRoute, { limit, ac }) + observer.disconnect() + } + }, { + rootMargin: '300px', + }) + observer.observe(ref.current) + } else { + fillList(list, builder, apiRoute, { limit, ac }) + } + }) - if (limit && placeholder && fallback) { - const placeholders = Array - .from({ length: limit }) - .map((_, index) => ( - - {updateFromList(list, index)} - - )) - return <>{placeholders} - } + if (limit && placeholder && fallback) { + const placeholders = Array + .from({ length: limit }) + .map((_, index) => ( + + )) + return <>{placeholders} + } - return <>{list} + return <>{list} } function fillList( - list: Signal, - builder: Builder, - apiRoute: string, - { limit, ac }: { limit?: number; ac?: AbortController }, + list: Signal, + builder: Builder, + apiRoute: string, + { limit, ac }: { limit?: number; ac?: AbortController }, ) { - ;(async () => { - const propsList = requestApiStream( - apiRoute, - 'GET', - ) + ;(async () => { + const propsList = requestApiStream( + apiRoute, + 'GET', + ) - for await (const props of propsList) { - list.value = [ - ...list.value, - builder(props), - ] - if (limit && list.value.length >= limit) break - } - ac?.abort() - })() + for await (const props of propsList) { + list.value = [ + ...list.value, + builder(props), + ] + if (limit && list.value.length >= limit) break + } + ac?.abort() + })() } function updateFromList( - list: Signal, - index: number, + list: Signal, + index: number, ): Promise { - const { promise, resolve } = Promise.withResolvers() - list.subscribe((value: JSX.Element[]) => { - const selected = value.at(index) - if (selected) { - resolve(selected) - } - }) + const { promise, resolve } = Promise.withResolvers() + list.subscribe((value: JSX.Element[]) => { + const selected = value.at(index) + if (selected) { + resolve(selected) + } + }) - return promise + return promise } diff --git a/islands/MemberCardList.tsx b/islands/MemberCardList.tsx index 4db3ade..501ea77 100644 --- a/islands/MemberCardList.tsx +++ b/islands/MemberCardList.tsx @@ -3,58 +3,58 @@ import CardList from ':islands/CardList.tsx' import type { Ref } from 'preact' export default function MemberCardList( - { limit, filters, usePlaceholder, useObserver }: { - filters?: [string, string][] - limit?: number - usePlaceholder?: boolean - useObserver?: boolean - }, + { limit, filters, usePlaceholder, useObserver }: { + filters?: [string, string][] + limit?: number + usePlaceholder?: boolean + useObserver?: boolean + }, ) { - const query = new URL('members/fetchAll', 'https://null/') - filters?.forEach((filter) => query.searchParams.set(...filter)) + const query = new URL('members/fetchAll', 'https://null/') + filters?.forEach((filter) => query.searchParams.set(...filter)) - const apiRoute = `${query.pathname}${query.search}` + const apiRoute = `${query.pathname}${query.search}` - if (usePlaceholder) { - return ( - - ) - } + if (usePlaceholder) { + return ( + + ) + } - return ( - - ) + return ( + + ) } function Placeholder({ ref }: { ref?: Ref | undefined }) { - return ( -
-

Chargement ...

-
- ) + return ( +
+

Chargement ...

+
+ ) } function Fallback() { - return ( -
-

Pas d'utilisateur

-
- ) + return ( +
+

Pas d'utilisateur

+
+ ) } diff --git a/islands/Suspens.tsx b/islands/Suspens.tsx index 0054df4..b952709 100644 --- a/islands/Suspens.tsx +++ b/islands/Suspens.tsx @@ -18,13 +18,14 @@ function RenderError( export type Fallback = ({ error }: { error: Error }) => JSX.Element +export type SuspenseProps = { + loader: JSX.Element + fallback?: Fallback + signal?: AbortSignal +} & ({ children: Promise } | { value: Promise }) + export default function Suspense( - { loader, fallback, signal, children }: { - loader: JSX.Element - children: Promise - fallback?: Fallback - signal?: AbortSignal - }, + { loader, fallback, signal, ...props }: SuspenseProps, ) { const displayed = useSignal(loader) let loaded = false @@ -38,7 +39,9 @@ export default function Suspense( } }) - children + //Prevent transpilation error due to children expected to not be a promise + const inner = 'value' in props ? props.value : props.children + inner .then((element) => { if (signal?.aborted) return displayed.value = element diff --git a/routes/api/members/fetchAll.ts b/routes/api/members/fetchAll.ts index c4e1af5..3535d92 100644 --- a/routes/api/members/fetchAll.ts +++ b/routes/api/members/fetchAll.ts @@ -5,24 +5,24 @@ import { SessionHandlers } from ':src/session/mod.ts' import { respondApi, respondApiStream } from ':src/utils.ts' export const handler: SessionHandlers = { - GET(_req, ctx) { - try { - const memberList = dbToMemberCardProps(db) + GET(_req, ctx) { + try { + const memberList = dbToMemberCardProps(db) - const params = ctx.url.searchParams + const params = ctx.url.searchParams - const groupParam = params.get('group') - if (groupParam) { - const list = memberList.filter( - (member) => member.groups.includes(groupParam), - ) as AsyncIterableIterator + const groupParam = params.get('group') + if (groupParam) { + const list = memberList.filter( + (member) => member.groups.includes(groupParam), + ) as AsyncIterableIterator - return respondApiStream(list) - } + return respondApiStream(list) + } - return respondApiStream(memberList) - } catch (error) { - return respondApi('error', error) - } - }, + return respondApiStream(memberList) + } catch (error) { + return respondApi('error', error) + } + }, } diff --git a/routes/api/news/fetchAll.ts b/routes/api/news/fetchAll.ts index ff56fe0..b42f201 100644 --- a/routes/api/news/fetchAll.ts +++ b/routes/api/news/fetchAll.ts @@ -3,12 +3,12 @@ import { SessionHandlers } from ':src/session/mod.ts' import { respondApi, respondApiStream } from ':src/utils.ts' export const handler: SessionHandlers = { - GET() { - try { - const newsList = fetchNewsList('cohabit') - return respondApiStream(newsList) - } catch (error) { - return respondApi('error', error) - } - }, + GET() { + try { + const newsList = fetchNewsList('cohabit') + return respondApiStream(newsList) + } catch (error) { + return respondApi('error', error) + } + }, } diff --git a/routes/api/webauthn/login/[step].ts b/routes/api/webauthn/login/[step].ts index 7a8d760..dca5f5a 100644 --- a/routes/api/webauthn/login/[step].ts +++ b/routes/api/webauthn/login/[step].ts @@ -2,7 +2,12 @@ import { db } from ':src/db/mod.ts' import type { SessionHandlers } from ':src/session/mod.ts' import { respondApi } from ':src/utils.ts' import { getRelyingParty } from ':src/webauthn/mod.ts' -import { Credential, Passkey, Ref, User } from '@cohabit/resources-manager/models' +import { + Credential, + Passkey, + Ref, + User, +} from '@cohabit/resources-manager/models' import { generateAuthenticationOptions, verifyAuthenticationResponse, diff --git a/routes/api/webauthn/register/[step].ts b/routes/api/webauthn/register/[step].ts index ed6b6c4..a16fce9 100644 --- a/routes/api/webauthn/register/[step].ts +++ b/routes/api/webauthn/register/[step].ts @@ -12,7 +12,12 @@ import type { //TODO improve workspace imports import { db } from ':src/db/mod.ts' import { getRelyingParty } from ':src/webauthn/mod.ts' -import { Credential, Passkey, Ref, User } from '@cohabit/resources-manager/models' +import { + Credential, + Passkey, + Ref, + User, +} from '@cohabit/resources-manager/models' import { encodeBase64 } from '@std/encoding' type Params = { step: 'start' | 'finish' } diff --git a/routes/index.tsx b/routes/index.tsx index d6b77e7..5645c53 100644 --- a/routes/index.tsx +++ b/routes/index.tsx @@ -29,9 +29,8 @@ export default function Home() {

Nos machines

- Vous avez besoin d'aide pour concrétiser votre projet ? Le - Fablab vous accompagnes dans vos projets, grâce à son parc - de machine... + Vous avez besoin d'aide pour concrétiser votre projet ? Le Fablab vous + accompagnes dans vos projets, grâce à son parc de machine...

<> @@ -63,29 +62,26 @@ export default function Home() {

Présentation

- Coh@bit est un fablab de l'université de Bordeaux ouvert à - tous les publics depuis 2016. Du collégien à - l'enseignant-chercheur, l'équipe du fablab accompagne les - adhérents dans la réalisation de leurs projets de - fabrication autour du numérique. + Coh@bit est un fablab de l'université de Bordeaux ouvert à tous les + publics depuis 2016. Du collégien à l'enseignant-chercheur, l'équipe + du fablab accompagne les adhérents dans la réalisation de leurs + projets de fabrication autour du numérique.

- Venez découvrir un tout nouvelle univers où vous pouvez - concrétiser vos projet, découvrir des personnes avec les - même affinités que vous, cultiver votre savoir et savoir - faire, dans l'entraide et le partage. + Venez découvrir un tout nouvelle univers où vous pouvez concrétiser + vos projet, découvrir des personnes avec les même affinités que vous, + cultiver votre savoir et savoir faire, dans l'entraide et le partage.

- Créer par Frédéric Bos (Directeur de l'IUT de Bordeaux) en - 2014, Coh@bit (Creative Open House at Bordeaux Institut of - Technology) est une association réunissant deux entités : le - Fablab et le Technoshop. + Créer par Frédéric Bos (Directeur de l'IUT de Bordeaux) en 2014, + Coh@bit (Creative Open House at Bordeaux Institut of Technology) est + une association réunissant deux entités : le Fablab et le Technoshop.

Ouvert à tous les publics depuis 2016, allant de - l'enseignant-chercheur au collégien, l'équipe du fablab - accompagne les adhérents dans la réalisation de leurs - projets de fabrication numérique. + l'enseignant-chercheur au collégien, l'équipe du fablab accompagne les + adhérents dans la réalisation de leurs projets de fabrication + numérique.

diff --git a/src/blog/mod.ts b/src/blog/mod.ts index 08f8c15..4fffdf5 100644 --- a/src/blog/mod.ts +++ b/src/blog/mod.ts @@ -4,101 +4,99 @@ import { base64ToString } from ':src/utils.ts' import { extract } from '@std/front-matter/yaml' export async function fetchNews( - publisher: string, - name: string, + publisher: string, + name: string, ): Promise { - const apiUrl = 'https://git.cohabit.fr/api/v1/' - const baseEndpoint = new URL(`repos/${publisher}/.news/`, apiUrl) - const endpoint = new URL('contents/', baseEndpoint) + const apiUrl = 'https://git.cohabit.fr/api/v1/' + const baseEndpoint = new URL(`repos/${publisher}/.news/`, apiUrl) + const endpoint = new URL('contents/', baseEndpoint) - // Get readme content api url - const readmePath = encodeURIComponent(`${name}/README.md`) - const contentUrl = new URL(readmePath, endpoint) + // Get readme content api url + const readmePath = encodeURIComponent(`${name}/README.md`) + const contentUrl = new URL(readmePath, endpoint) - // Fetch readme content, commit hash and raw url for relative links - const file = await getCommitAndContent(contentUrl) - // Get commit infos (author + date) and get readme content from base64 source - const { raw, url, lastUpdate, author } = await getAuthorAndParseContent( - file, - baseEndpoint, - ) - // Extract frontmatter - const { attrs, body } = extract(raw) + // Fetch readme content, commit hash and raw url for relative links + const file = await getCommitAndContent(contentUrl) + // Get commit infos (author + date) and get readme content from base64 source + const { raw, url, lastUpdate, author } = await getAuthorAndParseContent( + file, + baseEndpoint, + ) + // Extract frontmatter + const { attrs, body } = extract(raw) - // Transform API responses into BlogProps for BlogCard and BlogPost components - return { - author, - publisher, - lastUpdate, - options: attrs['x-cohabit'], - title: attrs.title, - hash: file.sha, - description: attrs.description, - body, - name, - url, - tags: attrs.tags, - } + // Transform API responses into BlogProps for BlogCard and BlogPost components + return { + author, + publisher, + lastUpdate, + options: attrs['x-cohabit'], + title: attrs.title, + hash: file.sha, + description: attrs.description, + body, + name, + url, + tags: attrs.tags, + } } export async function* fetchNewsList( - publisher: string, + publisher: string, ): AsyncGenerator { - const apiUrl = 'https://git.cohabit.fr/api/v1/' - const baseEndpoint = new URL(`repos/${publisher}/.news/`, apiUrl) - const endpoint = new URL('contents/', baseEndpoint) + const apiUrl = 'https://git.cohabit.fr/api/v1/' + const baseEndpoint = new URL(`repos/${publisher}/.news/`, apiUrl) + const endpoint = new URL('contents/', baseEndpoint) - // Fetch repo content - const root = await fetch(endpoint).then((response) => response.json()) as { - name: string - type: string - }[] + // Fetch repo content + const root = await fetch(endpoint).then((response) => response.json()) as { + name: string + type: string + }[] - // Fetch `README.md` in sub directories - const blogPropsList = root - // Remove file and dir starting with "." - .filter(isNewsDirectory) - // Fetch single news and return BlogProps - .map(({ name }) => fetchNews(publisher, name)) + // Fetch `README.md` in sub directories + const blogPropsList = root + // Remove file and dir starting with "." + .filter(isNewsDirectory) + // Fetch single news and return BlogProps + .map(({ name }) => fetchNews(publisher, name)) - // Yield each news - for (const blogProps of blogPropsList) { - yield blogProps - } + // Yield each news + for (const blogProps of blogPropsList) { + yield blogProps + } } async function getAuthorAndParseContent( - file: { download_url: string; content: string; last_commit_sha: string }, - baseEndpoint: URL, + file: { download_url: string; content: string; last_commit_sha: string }, + baseEndpoint: URL, ) { - const commitUrl = new URL( - `git/commits/${file.last_commit_sha}?stat=false&verification=false&files=false`, - baseEndpoint, - ) - const infos = await fetch(commitUrl).then((response) => - response.json() - ) as { - created: string - author: { login: string } - } + const commitUrl = new URL( + `git/commits/${file.last_commit_sha}?stat=false&verification=false&files=false`, + baseEndpoint, + ) + const infos = await fetch(commitUrl).then((response) => response.json()) as { + created: string + author: { login: string } + } - return { - raw: base64ToString(file.content), - url: file.download_url, - lastUpdate: new Date(infos.created), - author: infos.author.login, - } + return { + raw: base64ToString(file.content), + url: file.download_url, + lastUpdate: new Date(infos.created), + author: infos.author.login, + } } async function getCommitAndContent(contentUrl: URL) { - return await fetch(contentUrl).then((response) => response.json()) as { - download_url: string - content: string - sha: string - last_commit_sha: string - } + return await fetch(contentUrl).then((response) => response.json()) as { + download_url: string + content: string + sha: string + last_commit_sha: string + } } function isNewsDirectory(entry: { name: string; type: string }): boolean { - return entry.type === 'dir' && entry.name.startsWith('.') === false + return entry.type === 'dir' && entry.name.startsWith('.') === false } diff --git a/src/cache/middleware.ts b/src/cache/middleware.ts index b714f66..aa94667 100644 --- a/src/cache/middleware.ts +++ b/src/cache/middleware.ts @@ -1,19 +1,19 @@ import { FreshContext } from '$fresh/server.ts' export function useCache( - _request: Request, - response: Response, - ctx: FreshContext, + _request: Request, + response: Response, + ctx: FreshContext, ) { - if (ctx.config.dev) return - if ( - ctx.url.pathname.match( - /(.+\.|_)((css)|(ttf)|(woff2)|(png)|(svg)|(jpe?g)|(avif))/, - ) - ) { - response.headers.set( - 'Cache-Control', - 'public, max-age=31536000, immutable', - ) - } + if (ctx.config.dev) return + if ( + ctx.url.pathname.match( + /(.+\.|_)((css)|(ttf)|(woff2)|(png)|(svg)|(jpe?g)|(avif))/, + ) + ) { + response.headers.set( + 'Cache-Control', + 'public, max-age=31536000, immutable', + ) + } } diff --git a/src/csp/middleware.ts b/src/csp/middleware.ts index cb8b02b..9eab075 100644 --- a/src/csp/middleware.ts +++ b/src/csp/middleware.ts @@ -2,31 +2,31 @@ import { FreshContext } from '$fresh/server.ts' import { applyCspRulesWithNonce, CspRules } from ':src/csp/mod.ts' export function useCsp( - _request: Request, - response: Response, - ctx: FreshContext, + _request: Request, + response: Response, + ctx: FreshContext, ) { //See https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/CSP - - const trustedDomains = ["'self'", 'https://git.cohabit.fr'] - const cspRules: CspRules = { - defaultSrc: ["'none'"], - frameAncestors: ["'none'"], - upgradeInsecureRequests: true, - styleSrc: [...trustedDomains, "'unsafe-inline'"], //set nonce to inline script - manifestSrc: [`${ctx.url.origin.replace('http:', 'https:')}/manifest.json`], - baseUri: ["'none'"], - imgSrc: [ - ...trustedDomains, - 'data:', - 'https:', - ], - fontSrc: [...trustedDomains, 'https://cdn.jsdelivr.net'], - scriptSrc: ["'self'", "'strict-dynamic'"], - connectSrc: ["'self'"], - formAction: ["'none'"], - } - - return applyCspRulesWithNonce(response, cspRules) + const trustedDomains = ["'self'", 'https://git.cohabit.fr'] + + const cspRules: CspRules = { + defaultSrc: ["'none'"], + frameAncestors: ["'none'"], + upgradeInsecureRequests: true, + styleSrc: [...trustedDomains, "'unsafe-inline'"], //set nonce to inline script + manifestSrc: [`${ctx.url.origin.replace('http:', 'https:')}/manifest.json`], + baseUri: ["'none'"], + imgSrc: [ + ...trustedDomains, + 'data:', + 'https:', + ], + fontSrc: [...trustedDomains, 'https://cdn.jsdelivr.net'], + scriptSrc: ["'self'", "'strict-dynamic'"], + connectSrc: ["'self'"], + formAction: ["'none'"], + } + + return applyCspRulesWithNonce(response, cspRules) } diff --git a/src/csp/mod.ts b/src/csp/mod.ts index e9639fd..7d5f61f 100644 --- a/src/csp/mod.ts +++ b/src/csp/mod.ts @@ -2,51 +2,51 @@ import type { ContentSecurityPolicyDirectives } from '$fresh/runtime.ts' import { getFreshNonce, toSnakeCase } from ':src/utils.ts' export type CspRules = ContentSecurityPolicyDirectives & { - upgradeInsecureRequests: true + upgradeInsecureRequests: true } export function applyCspRules( - { headers }: { headers: Headers }, - rules: CspRules, + { headers }: { headers: Headers }, + rules: CspRules, ): void { - const rulesString: string[] = [] + const rulesString: string[] = [] - for (const rule in rules) { - const value = rules[rule as unknown as keyof CspRules] - const ruleName = toSnakeCase(rule) + for (const rule in rules) { + const value = rules[rule as unknown as keyof CspRules] + const ruleName = toSnakeCase(rule) - if (typeof value === 'boolean') { - rulesString.push(`${ruleName}`) - continue - } + if (typeof value === 'boolean') { + rulesString.push(`${ruleName}`) + continue + } - if (Array.isArray(value)) { - rulesString.push(`${ruleName} ${value.join(' ')}`) - continue - } + if (Array.isArray(value)) { + rulesString.push(`${ruleName} ${value.join(' ')}`) + continue + } - if (typeof value === 'string') { - rulesString.push(`${ruleName} ${value}`) - continue - } + if (typeof value === 'string') { + rulesString.push(`${ruleName} ${value}`) + continue + } - throw TypeError(`unsupported csp rule "${rule}" with value (${value})`) - } + throw TypeError(`unsupported csp rule "${rule}" with value (${value})`) + } - headers.set('Content-Security-Policy', rulesString.join('; ')) + headers.set('Content-Security-Policy', rulesString.join('; ')) } export async function applyCspRulesWithNonce( - response: Response, - rules: CspRules, + response: Response, + rules: CspRules, ): Promise { - // Get nonce from any html response - const nonce = await getFreshNonce(response) + // Get nonce from any html response + const nonce = await getFreshNonce(response) - // Add nonce to script src if defined - if (nonce) { - rules.scriptSrc?.push(`'nonce-${nonce}'`) - } + // Add nonce to script src if defined + if (nonce) { + rules.scriptSrc?.push(`'nonce-${nonce}'`) + } - return applyCspRules(response, rules) + return applyCspRules(response, rules) } diff --git a/src/db/mod.ts b/src/db/mod.ts index f40a303..711590f 100644 --- a/src/db/mod.ts +++ b/src/db/mod.ts @@ -5,8 +5,8 @@ import type { MailAddress } from '@cohabit/resources-manager/types' // Import Datas import { exists } from '$std/fs/exists.ts' import { ensureDir } from '$std/fs/mod.ts' -import groups from ":src/db/mock/groups.json" with { type: 'json' } -import users from ":src/db/mock/users.json" with { type: 'json' } +import groups from ':src/db/mock/groups.json' with { type: 'json' } +import users from ':src/db/mock/users.json' with { type: 'json' } await ensureDir('./cache') const dbPath = './cache/db.sqlite' diff --git a/src/security_headers/middleware.ts b/src/security_headers/middleware.ts index d53d61e..d90a6da 100644 --- a/src/security_headers/middleware.ts +++ b/src/security_headers/middleware.ts @@ -1,26 +1,26 @@ import { FreshContext } from '$fresh/server.ts' export function useSecurityHeaders( - _request: Request, - response: Response, - _ctx: FreshContext, + _request: Request, + response: Response, + _ctx: FreshContext, ) { - // See https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/TLS#http_strict_transport_security - response.headers.set( - 'Strict-Transport-Security', - 'max-age=63072000; includeSubDomains; preload', - ) - //See https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/Referrer_policy - response.headers.set( - 'Referrer-Policy', - 'no-referrer, strict-origin-when-cross-origin', - ) - //See https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/MIME_types - response.headers.set('X-Content-Type-Options', 'nosniff') - //See https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/Clickjacking - response.headers.set('X-Frame-Options', 'DENY') - //See https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/CORP - response.headers.set('Cross-Origin-Resource-Policy', 'same-origin') - //See https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity - //? SRI plugin for non local resources only ? + // See https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/TLS#http_strict_transport_security + response.headers.set( + 'Strict-Transport-Security', + 'max-age=63072000; includeSubDomains; preload', + ) + //See https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/Referrer_policy + response.headers.set( + 'Referrer-Policy', + 'no-referrer, strict-origin-when-cross-origin', + ) + //See https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/MIME_types + response.headers.set('X-Content-Type-Options', 'nosniff') + //See https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/Clickjacking + response.headers.set('X-Frame-Options', 'DENY') + //See https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/CORP + response.headers.set('Cross-Origin-Resource-Policy', 'same-origin') + //See https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity + //? SRI plugin for non local resources only ? } diff --git a/src/serviceworker/middleware.ts b/src/serviceworker/middleware.ts index 1a62130..9f60d00 100644 --- a/src/serviceworker/middleware.ts +++ b/src/serviceworker/middleware.ts @@ -1,12 +1,12 @@ import { FreshContext } from '$fresh/server.ts' export function useServiceworker( - _request: Request, - response: Response, - ctx: FreshContext, + _request: Request, + response: Response, + ctx: FreshContext, ) { - // Allow service worker to serve root scope - if (ctx.url.pathname.endsWith('island-startserviceworker.js')) { - response.headers.set('Service-Worker-Allowed', '/') - } + // Allow service worker to serve root scope + if (ctx.url.pathname.endsWith('island-startserviceworker.js')) { + response.headers.set('Service-Worker-Allowed', '/') + } } diff --git a/src/session/middleware.ts b/src/session/middleware.ts index 6b4ec3f..e979d1a 100644 --- a/src/session/middleware.ts +++ b/src/session/middleware.ts @@ -3,51 +3,51 @@ import { SessionStore } from ':src/session/mod.ts' import { getCookies, setCookie } from 'jsr:@std/http@^0.224.4/cookie' export function useSession( - request: Request, - response: Response, - ctx: FreshContext, + request: Request, + response: Response, + ctx: FreshContext, ) { - // Check if session already started - if (SessionStore.getFromRequest(request) !== undefined) { - return - } + // Check if session already started + if (SessionStore.getFromRequest(request) !== undefined) { + return + } - // Clear outdated cookies - for (const cookie in getCookies(request.headers)) { - setCookie(response.headers, { - name: cookie, - value: '', - path: '/', - expires: 0, - }) - } + // Clear outdated cookies + for (const cookie in getCookies(request.headers)) { + setCookie(response.headers, { + name: cookie, + value: '', + path: '/', + expires: 0, + }) + } - // Create new session - const session = SessionStore.createSession() - ctx.state = { ...ctx.state, session } + // Create new session + const session = SessionStore.createSession() + ctx.state = { ...ctx.state, session } - // Set session cookie - setCookie(response.headers, { - name: '__Secure-SESSION', - value: session.uuid, - httpOnly: true, - sameSite: 'Strict', - secure: true, - path: '/', - expires: SessionStore.maxAge, - }) + // Set session cookie + setCookie(response.headers, { + name: '__Secure-SESSION', + value: session.uuid, + httpOnly: true, + sameSite: 'Strict', + secure: true, + path: '/', + expires: SessionStore.maxAge, + }) - // Set csrf - const csrf = crypto.randomUUID() - session.set('_csrf', csrf) + // Set csrf + const csrf = crypto.randomUUID() + session.set('_csrf', csrf) - setCookie(response.headers, { - name: '__Host-CSRF', - value: csrf, - httpOnly: false, - sameSite: 'Strict', - secure: true, - path: '/', - expires: SessionStore.maxAge, - }) + setCookie(response.headers, { + name: '__Host-CSRF', + value: csrf, + httpOnly: false, + sameSite: 'Strict', + secure: true, + path: '/', + expires: SessionStore.maxAge, + }) }