Compare commits
5 Commits
b24a083d2e
...
031a51c2fe
| Author | SHA1 | Date |
|---|---|---|
|
|
031a51c2fe | |
|
|
7bdf6ccd13 | |
|
|
cb2c19fada | |
|
|
0abf0b11e6 | |
|
|
ec0afa9686 |
|
|
@ -1,26 +0,0 @@
|
||||||
<template>
|
|
||||||
<template v-if="content && content.length > 0">
|
|
||||||
<Suspense :timeout="0">
|
|
||||||
<MarkdownRenderer #default :key="key" v-if="node" :node="node" :proses="proses"></MarkdownRenderer>
|
|
||||||
<template #fallback><Loading /></template>
|
|
||||||
</Suspense>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { hash } from 'ohash'
|
|
||||||
|
|
||||||
const { content } = defineProps({
|
|
||||||
content: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
proses: {
|
|
||||||
type: Object
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const parser = useMarkdown();
|
|
||||||
const key = computed(() => hash(content));
|
|
||||||
const node = computed(() => content ? parser(content) : undefined);
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,115 +1,49 @@
|
||||||
<script lang="ts">
|
<template>
|
||||||
import type { RootContent, Root } from 'hast';
|
<div v-if="content && content.length > 0">
|
||||||
import { Text, Comment } from 'vue';
|
<ProsesRenderer #default v-if="data" :node="data" :proses="proses" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
import ProseP from '~/components/prose/ProseP.vue';
|
<script setup lang="ts">
|
||||||
import ProseA from '~/components/prose/ProseA.vue';
|
import type { Component } from 'vue';
|
||||||
import ProseBlockquote from '~/components/prose/ProseBlockquote.vue';
|
import { heading } from 'hast-util-heading';
|
||||||
import ProseCallout from './prose/ProseCallout.vue';
|
import { headingRank } from 'hast-util-heading-rank';
|
||||||
import ProseCode from '~/components/prose/ProseCode.vue';
|
import { parseId } from '~/shared/general.utils';
|
||||||
import ProsePre from '~/components/prose/ProsePre.vue';
|
import type { Root } from 'hast';
|
||||||
import ProseEm from '~/components/prose/ProseEm.vue';
|
|
||||||
import ProseH1 from '~/components/prose/ProseH1.vue';
|
|
||||||
import ProseH2 from '~/components/prose/ProseH2.vue';
|
|
||||||
import ProseH3 from '~/components/prose/ProseH3.vue';
|
|
||||||
import ProseH4 from '~/components/prose/ProseH4.vue';
|
|
||||||
import ProseH5 from '~/components/prose/ProseH5.vue';
|
|
||||||
import ProseH6 from '~/components/prose/ProseH6.vue';
|
|
||||||
import ProseHr from '~/components/prose/ProseHr.vue';
|
|
||||||
import ProseImg from '~/components/prose/ProseImg.vue';
|
|
||||||
import ProseUl from '~/components/prose/ProseUl.vue';
|
|
||||||
import ProseOl from '~/components/prose/ProseOl.vue';
|
|
||||||
import ProseLi from '~/components/prose/ProseLi.vue';
|
|
||||||
import ProseSmall from './prose/ProseSmall.vue';
|
|
||||||
import ProseStrong from '~/components/prose/ProseStrong.vue';
|
|
||||||
import ProseTable from '~/components/prose/ProseTable.vue';
|
|
||||||
import ProseTag from '~/components/prose/ProseTag.vue';
|
|
||||||
import ProseThead from '~/components/prose/ProseThead.vue';
|
|
||||||
import ProseTbody from '~/components/prose/ProseTbody.vue';
|
|
||||||
import ProseTd from '~/components/prose/ProseTd.vue';
|
|
||||||
import ProseTh from '~/components/prose/ProseTh.vue';
|
|
||||||
import ProseTr from '~/components/prose/ProseTr.vue';
|
|
||||||
import ProseScript from '~/components/prose/ProseScript.vue';
|
|
||||||
|
|
||||||
const proseList = {
|
const { content, proses, filter } = defineProps<{
|
||||||
"p": ProseP,
|
content: string
|
||||||
"a": ProseA,
|
proses?: Array<string | Component>
|
||||||
"blockquote": ProseBlockquote,
|
filter?: string
|
||||||
"callout": ProseCallout,
|
}>();
|
||||||
"code": ProseCode,
|
|
||||||
"pre": ProsePre,
|
|
||||||
"em": ProseEm,
|
|
||||||
"h1": ProseH1,
|
|
||||||
"h2": ProseH2,
|
|
||||||
"h3": ProseH3,
|
|
||||||
"h4": ProseH4,
|
|
||||||
"h5": ProseH5,
|
|
||||||
"h6": ProseH6,
|
|
||||||
"hr": ProseHr,
|
|
||||||
"img": ProseImg,
|
|
||||||
"ul": ProseUl,
|
|
||||||
"ol": ProseOl,
|
|
||||||
"li": ProseLi,
|
|
||||||
"small": ProseSmall,
|
|
||||||
"strong": ProseStrong,
|
|
||||||
"table": ProseTable,
|
|
||||||
"tag": ProseTag,
|
|
||||||
"thead": ProseThead,
|
|
||||||
"tbody": ProseTbody,
|
|
||||||
"td": ProseTd,
|
|
||||||
"th": ProseTh,
|
|
||||||
"tr": ProseTr,
|
|
||||||
"script": ProseScript
|
|
||||||
};
|
|
||||||
|
|
||||||
export default defineComponent({
|
const parser = useMarkdown(), data = ref<Root>();
|
||||||
name: 'MarkdownRenderer',
|
const node = computed(() => content ? parser(content) : undefined);
|
||||||
props: {
|
watch([node], () => {
|
||||||
node: {
|
if(!node.value)
|
||||||
type: Object,
|
data.value = undefined;
|
||||||
required: true
|
else if(!filter)
|
||||||
},
|
|
||||||
proses: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async setup(props) {
|
|
||||||
if(props.proses)
|
|
||||||
{
|
{
|
||||||
for(const prose of Object.keys(props.proses))
|
data.value = node.value;
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
if(typeof props.proses[prose] === 'string')
|
const start = node.value?.children.findIndex(e => heading(e) && parseId(e.properties.id as string | undefined) === filter) ?? -1;
|
||||||
props.proses[prose] = await resolveComponent(props.proses[prose]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { tags: Object.assign({}, proseList, props.proses) };
|
|
||||||
},
|
|
||||||
render(ctx: any) {
|
|
||||||
const { node, tags } = ctx;
|
|
||||||
|
|
||||||
if(!node)
|
if(start === -1)
|
||||||
return null;
|
data.value = node.value;
|
||||||
|
else
|
||||||
return h('div', null, {default: () => (node as Root).children.map(e => renderNode(e, tags)).filter(e => !!e)});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function renderNode(node: RootContent, tags: Record<string, any>): VNode | undefined
|
|
||||||
{
|
|
||||||
if(node.type === 'text' && node.value.length > 0 && node.value !== '\n')
|
|
||||||
{
|
{
|
||||||
return h(Text, node.value);
|
let end = start;
|
||||||
}
|
const rank = headingRank(node.value.children[start])!;
|
||||||
else if(node.type === 'comment' && node.value.length > 0 && node.value !== '\n')
|
while(end < node.value.children.length)
|
||||||
{
|
{
|
||||||
return h(Comment, node.value);
|
end++;
|
||||||
|
if(heading(node.value.children[end]) && headingRank(node.value.children[end])! >= rank)
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
else if(node.type === 'element')
|
data.value = { ...node.value, children: node.value.children.slice(start, end) };
|
||||||
{
|
|
||||||
return h(tags[node.tagName] ?? node.tagName, { ...node.properties, class: node.properties.className }, { default: () => node.children.map(e => renderNode(e, tags)).filter(e => !!e) });
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return undefined;
|
}, { immediate: true, });
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { RootContent, Root } from 'hast';
|
||||||
|
import { Text, Comment } from 'vue';
|
||||||
|
|
||||||
|
import ProseP from '~/components/prose/ProseP.vue';
|
||||||
|
import ProseA from '~/components/prose/ProseA.vue';
|
||||||
|
import ProseBlockquote from '~/components/prose/ProseBlockquote.vue';
|
||||||
|
import ProseCallout from './prose/ProseCallout.vue';
|
||||||
|
import ProseCode from '~/components/prose/ProseCode.vue';
|
||||||
|
import ProsePre from '~/components/prose/ProsePre.vue';
|
||||||
|
import ProseEm from '~/components/prose/ProseEm.vue';
|
||||||
|
import ProseH1 from '~/components/prose/ProseH1.vue';
|
||||||
|
import ProseH2 from '~/components/prose/ProseH2.vue';
|
||||||
|
import ProseH3 from '~/components/prose/ProseH3.vue';
|
||||||
|
import ProseH4 from '~/components/prose/ProseH4.vue';
|
||||||
|
import ProseH5 from '~/components/prose/ProseH5.vue';
|
||||||
|
import ProseH6 from '~/components/prose/ProseH6.vue';
|
||||||
|
import ProseHr from '~/components/prose/ProseHr.vue';
|
||||||
|
import ProseImg from '~/components/prose/ProseImg.vue';
|
||||||
|
import ProseUl from '~/components/prose/ProseUl.vue';
|
||||||
|
import ProseOl from '~/components/prose/ProseOl.vue';
|
||||||
|
import ProseLi from '~/components/prose/ProseLi.vue';
|
||||||
|
import ProseSmall from './prose/ProseSmall.vue';
|
||||||
|
import ProseStrong from '~/components/prose/ProseStrong.vue';
|
||||||
|
import ProseTable from '~/components/prose/ProseTable.vue';
|
||||||
|
import ProseTag from '~/components/prose/ProseTag.vue';
|
||||||
|
import ProseThead from '~/components/prose/ProseThead.vue';
|
||||||
|
import ProseTbody from '~/components/prose/ProseTbody.vue';
|
||||||
|
import ProseTd from '~/components/prose/ProseTd.vue';
|
||||||
|
import ProseTh from '~/components/prose/ProseTh.vue';
|
||||||
|
import ProseTr from '~/components/prose/ProseTr.vue';
|
||||||
|
import ProseScript from '~/components/prose/ProseScript.vue';
|
||||||
|
|
||||||
|
const proseList = {
|
||||||
|
"p": ProseP,
|
||||||
|
"a": ProseA,
|
||||||
|
"blockquote": ProseBlockquote,
|
||||||
|
"callout": ProseCallout,
|
||||||
|
"code": ProseCode,
|
||||||
|
"pre": ProsePre,
|
||||||
|
"em": ProseEm,
|
||||||
|
"h1": ProseH1,
|
||||||
|
"h2": ProseH2,
|
||||||
|
"h3": ProseH3,
|
||||||
|
"h4": ProseH4,
|
||||||
|
"h5": ProseH5,
|
||||||
|
"h6": ProseH6,
|
||||||
|
"hr": ProseHr,
|
||||||
|
"img": ProseImg,
|
||||||
|
"ul": ProseUl,
|
||||||
|
"ol": ProseOl,
|
||||||
|
"li": ProseLi,
|
||||||
|
"small": ProseSmall,
|
||||||
|
"strong": ProseStrong,
|
||||||
|
"table": ProseTable,
|
||||||
|
"tag": ProseTag,
|
||||||
|
"thead": ProseThead,
|
||||||
|
"tbody": ProseTbody,
|
||||||
|
"td": ProseTd,
|
||||||
|
"th": ProseTh,
|
||||||
|
"tr": ProseTr,
|
||||||
|
"script": ProseScript
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'MarkdownRenderer',
|
||||||
|
props: {
|
||||||
|
node: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
proses: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async setup(props) {
|
||||||
|
if(props.proses)
|
||||||
|
{
|
||||||
|
for(const prose of Object.keys(props.proses))
|
||||||
|
{
|
||||||
|
if(typeof props.proses[prose] === 'string')
|
||||||
|
props.proses[prose] = await resolveComponent(props.proses[prose]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { tags: Object.assign({}, proseList, props.proses) };
|
||||||
|
},
|
||||||
|
render(ctx: any) {
|
||||||
|
const { node, tags } = ctx;
|
||||||
|
|
||||||
|
if(!node)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return h('div', null, {default: () => (node as Root).children.map(e => renderNode(e, tags)).filter(e => !!e)});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderNode(node: RootContent, tags: Record<string, any>): VNode | undefined
|
||||||
|
{
|
||||||
|
if(node.type === 'text' && node.value.length > 0 && node.value !== '\n')
|
||||||
|
{
|
||||||
|
return h(Text, node.value);
|
||||||
|
}
|
||||||
|
else if(node.type === 'comment' && node.value.length > 0 && node.value !== '\n')
|
||||||
|
{
|
||||||
|
return h(Comment, node.value);
|
||||||
|
}
|
||||||
|
else if(node.type === 'element')
|
||||||
|
{
|
||||||
|
return h(tags[node.tagName] ?? node.tagName, { ...node.properties, class: node.properties.className }, { default: () => node.children.map(e => renderNode(e, tags)).filter(e => !!e) });
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -1,224 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { useDrag, usePinch, useWheel } from '@vueuse/gesture';
|
|
||||||
import type { CanvasContent, CanvasNode } from '~/types/canvas';
|
|
||||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
|
||||||
import { clamp } from '#shared/general.utils';
|
|
||||||
|
|
||||||
interface Props
|
|
||||||
{
|
|
||||||
canvas: CanvasContent;
|
|
||||||
}
|
|
||||||
const props = defineProps<Props>();
|
|
||||||
|
|
||||||
const dispX = ref(0), dispY = ref(0), minZoom = ref(0.1), zoom = ref(0.5);
|
|
||||||
const canvas = useTemplateRef('canvas');
|
|
||||||
|
|
||||||
const reset = (_: MouseEvent) => {
|
|
||||||
zoom.value = minZoom.value;
|
|
||||||
|
|
||||||
dispX.value = 0;
|
|
||||||
dispY.value = 0;
|
|
||||||
}
|
|
||||||
function edgePos(side: 'bottom' | 'top' | 'left' | 'right', pos: { x: number, y: number }, offset: number): { x: number, y: number } {
|
|
||||||
switch (side) {
|
|
||||||
case "left":
|
|
||||||
return {
|
|
||||||
x: pos.x - offset,
|
|
||||||
y: pos.y
|
|
||||||
};
|
|
||||||
case "right":
|
|
||||||
return {
|
|
||||||
x: pos.x + offset,
|
|
||||||
y: pos.y
|
|
||||||
};
|
|
||||||
case "top":
|
|
||||||
return {
|
|
||||||
x: pos.x,
|
|
||||||
y: pos.y - offset
|
|
||||||
};
|
|
||||||
case "bottom":
|
|
||||||
return {
|
|
||||||
x: pos.x,
|
|
||||||
y: pos.y + offset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function getNode(id: string): CanvasNode | undefined
|
|
||||||
{
|
|
||||||
return props.canvas.nodes.find(e => e.id === id);
|
|
||||||
}
|
|
||||||
function posFromDir(e: { minX: number, minY: number, maxX: number, maxY: number }, t: 'bottom' | 'top' | 'left' | 'right'): { x: number, y: number } {
|
|
||||||
switch (t) {
|
|
||||||
case "top":
|
|
||||||
return { x: (e.minX + e.maxX) / 2, y: e.minY };
|
|
||||||
case "right":
|
|
||||||
return { x: e.maxX, y: (e.minY + e.maxY) / 2 };
|
|
||||||
case "bottom":
|
|
||||||
return { x: (e.minX + e.maxX) / 2, y: e.maxY };
|
|
||||||
case "left":
|
|
||||||
return { x: e.minX, y: (e.minY + e.maxY) / 2 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function getBbox(node: CanvasNode): { minX: number, minY: number, maxX: number, maxY: number } {
|
|
||||||
return { minX: node.x, minY: node.y, maxX: node.x + node.width, maxY: node.y + node.height };
|
|
||||||
}
|
|
||||||
function path(from: CanvasNode, fromSide: 'bottom' | 'top' | 'left' | 'right', to: CanvasNode, toSide: 'bottom' | 'top' | 'left' | 'right'): any {
|
|
||||||
if(from === undefined || to === undefined)
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
path: '',
|
|
||||||
from: {},
|
|
||||||
to: {},
|
|
||||||
toSide: '',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const start = posFromDir(getBbox(from), fromSide), end = posFromDir(getBbox(to), toSide);
|
|
||||||
return bezier(start, fromSide, end, toSide);
|
|
||||||
}
|
|
||||||
function bezier(from: { x: number, y: number }, fromSide: 'bottom' | 'top' | 'left' | 'right', to: { x: number, y: number }, toSide: 'bottom' | 'top' | 'left' | 'right'): any {
|
|
||||||
const r = Math.hypot(from.x - to.x, from.y - to.y), o = clamp(r / 2, 70, 150), a = edgePos(fromSide, from, o), s = edgePos(toSide, to, o);
|
|
||||||
return {
|
|
||||||
path: `M${from.x},${from.y} C${a.x},${a.y} ${s.x},${s.y} ${to.x},${to.y}`,
|
|
||||||
from: from,
|
|
||||||
to: to,
|
|
||||||
side: toSide,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
function labelCenter(from: CanvasNode, fromSide: 'bottom' | 'top' | 'left' | 'right', to: CanvasNode, toSide: 'bottom' | 'top' | 'left' | 'right'): string {
|
|
||||||
const start = posFromDir(getBbox(from), fromSide), end = posFromDir(getBbox(to), toSide);
|
|
||||||
const len = Math.hypot(start.x - end.x, start.y - end.y), offset = clamp(len / 2, 70, 150), b = edgePos(fromSide, start, offset), s = edgePos(toSide, end, offset);
|
|
||||||
const center = getCenter(start, end, b, s, 0.5);
|
|
||||||
return `translate(${center.x}px, ${center.y}px)`;
|
|
||||||
}
|
|
||||||
function getCenter(n: { x: number, y: number }, i: { x: number, y: number }, r: { x: number, y: number }, o: { x: number, y: number }, e: number): { x: number, y: number } {
|
|
||||||
const a = 1 - e, s = a * a * a, l = 3 * e * a * a, c = 3 * e * e * a, u = e * e * e;
|
|
||||||
return {
|
|
||||||
x: s * n.x + l * r.x + c * o.x + u * i.x,
|
|
||||||
y: s * n.y + l * r.y + c * o.y + u * i.y
|
|
||||||
};
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
|
|
||||||
stroke-light-red
|
|
||||||
stroke-light-orange
|
|
||||||
stroke-light-yellow
|
|
||||||
stroke-light-green
|
|
||||||
stroke-light-cyan
|
|
||||||
stroke-light-purple
|
|
||||||
dark:stroke-dark-red
|
|
||||||
dark:stroke-dark-orange
|
|
||||||
dark:stroke-dark-yellow
|
|
||||||
dark:stroke-dark-green
|
|
||||||
dark:stroke-dark-cyan
|
|
||||||
dark:stroke-dark-purple
|
|
||||||
fill-light-red
|
|
||||||
fill-light-orange
|
|
||||||
fill-light-yellow
|
|
||||||
fill-light-green
|
|
||||||
fill-light-cyan
|
|
||||||
fill-light-purple
|
|
||||||
dark:fill-dark-red
|
|
||||||
dark:fill-dark-orange
|
|
||||||
dark:fill-dark-yellow
|
|
||||||
dark:fill-dark-green
|
|
||||||
dark:fill-dark-cyan
|
|
||||||
dark:fill-dark-purple
|
|
||||||
bg-light-red
|
|
||||||
bg-light-orange
|
|
||||||
bg-light-yellow
|
|
||||||
bg-light-green
|
|
||||||
bg-light-cyan
|
|
||||||
bg-light-purple
|
|
||||||
dark:bg-dark-red
|
|
||||||
dark:bg-dark-orange
|
|
||||||
dark:bg-dark-yellow
|
|
||||||
dark:bg-dark-green
|
|
||||||
dark:bg-dark-cyan
|
|
||||||
dark:bg-dark-purple
|
|
||||||
border-light-red
|
|
||||||
border-light-orange
|
|
||||||
border-light-yellow
|
|
||||||
border-light-green
|
|
||||||
border-light-cyan
|
|
||||||
border-light-purple
|
|
||||||
dark:border-dark-red
|
|
||||||
dark:border-dark-orange
|
|
||||||
dark:border-dark-yellow
|
|
||||||
dark:border-dark-green
|
|
||||||
dark:border-dark-cyan
|
|
||||||
dark:border-dark-purple
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
const pinchHandler = usePinch(({ event, offset: [z] }: { event: Event, offset: number[] }) => {
|
|
||||||
zoom.value = clamp(z / 2048, minZoom.value, 3);
|
|
||||||
}, {
|
|
||||||
domTarget: canvas,
|
|
||||||
eventOptions: { passive: false, }
|
|
||||||
})
|
|
||||||
const dragHandler = useDrag(({ event, delta: [x, y] }: { event: Event, delta: number[] }) => {
|
|
||||||
dispX.value += x / zoom.value;
|
|
||||||
dispY.value += y / zoom.value;
|
|
||||||
}, {
|
|
||||||
domTarget: canvas,
|
|
||||||
eventOptions: { passive: false, }
|
|
||||||
})
|
|
||||||
const wheelHandler = useWheel(({ event, delta: [x, y] }: { event: Event, delta: number[] }) => {
|
|
||||||
zoom.value = clamp(zoom.value + y * -0.001, minZoom.value, 3);
|
|
||||||
}, {
|
|
||||||
domTarget: canvas,
|
|
||||||
eventOptions: { passive: false, }
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Suspense>
|
|
||||||
<template #default>
|
|
||||||
<div id="canvas" ref="canvas" class="absolute top-0 left-0 overflow-hidden w-full h-full touch-none"
|
|
||||||
:style="{ '--zoom-multiplier': (1 / Math.pow(zoom, 0.7)) }">
|
|
||||||
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 absolute sm:top-2 top-10 left-2 z-[35] overflow-hidden">
|
|
||||||
<Tooltip message="Zoom avant" side="right">
|
|
||||||
<div @click="zoom = clamp(zoom * 1.1, minZoom, 3)" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
|
||||||
<Icon icon="radix-icons:plus" />
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip message="Reset" side="right">
|
|
||||||
<div @click="zoom = 1" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
|
||||||
<Icon icon="radix-icons:reload" />
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip message="Tout contenir" side="right">
|
|
||||||
<div @click="reset" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
|
||||||
<Icon icon="radix-icons:corners" />
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip message="Zoom arrière" side="right">
|
|
||||||
<div @click="zoom = clamp(zoom * 0.9, minZoom, 3)" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
|
||||||
<Icon icon="radix-icons:minus" />
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
<div class="absolute top-0 left-0 w-full h-full origin-center pointer-events-none *:pointer-events-auto *:select-none"
|
|
||||||
:style="{transform: `scale(${zoom}) translate(${dispX}px, ${dispY}px)`}">
|
|
||||||
<div>
|
|
||||||
<CanvasNode v-for="node of props.canvas.nodes" :key="node.id" :node="node" :zoom="zoom" />
|
|
||||||
</div>
|
|
||||||
<template v-for="edge of props.canvas.edges">
|
|
||||||
<div :key="edge.id" v-if="edge.label" class="absolute z-10"
|
|
||||||
:style="{ transform: labelCenter(getNode(edge.fromNode)!, edge.fromSide, getNode(edge.toNode)!, edge.toSide) }">
|
|
||||||
<div class="relative bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 px-4 py-2 -translate-x-[50%] -translate-y-[50%]">{{ edge.label }}</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<svg class="absolute top-0 left-0 overflow-visible w-full h-full origin-top pointer-events-none">
|
|
||||||
<CanvasEdge v-for="edge of props.canvas.edges" :key="edge.id"
|
|
||||||
:path="path(getNode(edge.fromNode)!, edge.fromSide, getNode(edge.toNode)!, edge.toSide)"
|
|
||||||
:color="edge.color" :label="edge.label" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #fallback>
|
|
||||||
<div class="loading"></div>
|
|
||||||
</template>
|
|
||||||
</Suspense>
|
|
||||||
</template>
|
|
||||||
|
|
@ -37,7 +37,7 @@ const colors = computed(() => {
|
||||||
<div class="w-full h-full py-2 px-4 flex !bg-opacity-[0.07]" :class="colors.bg">
|
<div class="w-full h-full py-2 px-4 flex !bg-opacity-[0.07]" :class="colors.bg">
|
||||||
<template v-if="node.type === 'group' || zoom > Math.min(0.4, 1000 / size)">
|
<template v-if="node.type === 'group' || zoom > Math.min(0.4, 1000 / size)">
|
||||||
<div v-if="node.text?.length > 0" class="flex items-center">
|
<div v-if="node.text?.length > 0" class="flex items-center">
|
||||||
<Markdown :content="node.text" />
|
<MarkdownRenderer :content="node.text" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
<template>
|
||||||
|
<div class="absolute top-0 left-0 w-full h-full origin-center pointer-events-none *:pointer-events-auto *:select-none">
|
||||||
|
<div>
|
||||||
|
<CanvasNode v-for="node of canvas.nodes" :key="node.id" :node="node" :zoom="zoom" />
|
||||||
|
</div>
|
||||||
|
<template v-for="edge of canvas.edges">
|
||||||
|
<div :key="edge.id" v-if="edge.label" class="absolute z-10"
|
||||||
|
:style="{ transform: labelCenter(getNode(canvas.nodes, edge.fromNode)!, edge.fromSide, getNode(canvas.nodes, edge.toNode)!, edge.toSide) }">
|
||||||
|
<div class="relative bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 px-4 py-2 -translate-x-[50%] -translate-y-[50%]">{{ edge.label }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<svg class="absolute top-0 left-0 overflow-visible w-full h-full origin-top pointer-events-none">
|
||||||
|
<CanvasEdge v-for="edge of canvas.edges" :key="edge.id"
|
||||||
|
:path="path(getNode(canvas.nodes, edge.fromNode)!, edge.fromSide, getNode(canvas.nodes, edge.toNode)!, edge.toSide)"
|
||||||
|
:color="edge.color" :label="edge.label" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { canvas, zoom } = defineProps<{
|
||||||
|
canvas: CanvasContent
|
||||||
|
zoom: number
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { clamp } from '~/shared/general.utils';
|
||||||
|
import type { CanvasContent, CanvasNode } from '~/types/canvas';
|
||||||
|
|
||||||
|
function edgePos(side: 'bottom' | 'top' | 'left' | 'right', pos: { x: number, y: number }, offset: number): { x: number, y: number } {
|
||||||
|
switch (side) {
|
||||||
|
case "left":
|
||||||
|
return {
|
||||||
|
x: pos.x - offset,
|
||||||
|
y: pos.y
|
||||||
|
};
|
||||||
|
case "right":
|
||||||
|
return {
|
||||||
|
x: pos.x + offset,
|
||||||
|
y: pos.y
|
||||||
|
};
|
||||||
|
case "top":
|
||||||
|
return {
|
||||||
|
x: pos.x,
|
||||||
|
y: pos.y - offset
|
||||||
|
};
|
||||||
|
case "bottom":
|
||||||
|
return {
|
||||||
|
x: pos.x,
|
||||||
|
y: pos.y + offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getNode(nodes: CanvasNode[], id: string): CanvasNode | undefined
|
||||||
|
{
|
||||||
|
return nodes.find(e => e.id === id);
|
||||||
|
}
|
||||||
|
function posFromDir(e: { minX: number, minY: number, maxX: number, maxY: number }, t: 'bottom' | 'top' | 'left' | 'right'): { x: number, y: number } {
|
||||||
|
switch (t) {
|
||||||
|
case "top":
|
||||||
|
return { x: (e.minX + e.maxX) / 2, y: e.minY };
|
||||||
|
case "right":
|
||||||
|
return { x: e.maxX, y: (e.minY + e.maxY) / 2 };
|
||||||
|
case "bottom":
|
||||||
|
return { x: (e.minX + e.maxX) / 2, y: e.maxY };
|
||||||
|
case "left":
|
||||||
|
return { x: e.minX, y: (e.minY + e.maxY) / 2 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getBbox(node: CanvasNode): { minX: number, minY: number, maxX: number, maxY: number } {
|
||||||
|
return { minX: node.x, minY: node.y, maxX: node.x + node.width, maxY: node.y + node.height };
|
||||||
|
}
|
||||||
|
function path(from: CanvasNode, fromSide: 'bottom' | 'top' | 'left' | 'right', to: CanvasNode, toSide: 'bottom' | 'top' | 'left' | 'right'): any {
|
||||||
|
if(from === undefined || to === undefined)
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
path: '',
|
||||||
|
from: {},
|
||||||
|
to: {},
|
||||||
|
toSide: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const start = posFromDir(getBbox(from), fromSide), end = posFromDir(getBbox(to), toSide);
|
||||||
|
return bezier(start, fromSide, end, toSide);
|
||||||
|
}
|
||||||
|
function bezier(from: { x: number, y: number }, fromSide: 'bottom' | 'top' | 'left' | 'right', to: { x: number, y: number }, toSide: 'bottom' | 'top' | 'left' | 'right'): any {
|
||||||
|
const r = Math.hypot(from.x - to.x, from.y - to.y), o = clamp(r / 2, 70, 150), a = edgePos(fromSide, from, o), s = edgePos(toSide, to, o);
|
||||||
|
return {
|
||||||
|
path: `M${from.x},${from.y} C${a.x},${a.y} ${s.x},${s.y} ${to.x},${to.y}`,
|
||||||
|
from: from,
|
||||||
|
to: to,
|
||||||
|
side: toSide,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function labelCenter(from: CanvasNode, fromSide: 'bottom' | 'top' | 'left' | 'right', to: CanvasNode, toSide: 'bottom' | 'top' | 'left' | 'right'): string {
|
||||||
|
const start = posFromDir(getBbox(from), fromSide), end = posFromDir(getBbox(to), toSide);
|
||||||
|
const len = Math.hypot(start.x - end.x, start.y - end.y), offset = clamp(len / 2, 70, 150), b = edgePos(fromSide, start, offset), s = edgePos(toSide, end, offset);
|
||||||
|
const center = getCenter(start, end, b, s, 0.5);
|
||||||
|
return `translate(${center.x}px, ${center.y}px)`;
|
||||||
|
}
|
||||||
|
function getCenter(n: { x: number, y: number }, i: { x: number, y: number }, r: { x: number, y: number }, o: { x: number, y: number }, e: number): { x: number, y: number } {
|
||||||
|
const a = 1 - e, s = a * a * a, l = 3 * e * a * a, c = 3 * e * e * a, u = e * e * e;
|
||||||
|
return {
|
||||||
|
x: s * n.x + l * r.x + c * o.x + u * i.x,
|
||||||
|
y: s * n.y + l * r.y + c * o.y + u * i.y
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
<script lang="ts">
|
||||||
|
function cancelEvent(e: Event)
|
||||||
|
{
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useDrag, useHover, usePinch, useWheel } from '@vueuse/gesture';
|
||||||
|
import type { CanvasContent } from '~/types/canvas';
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
import { clamp } from '#shared/general.utils';
|
||||||
|
|
||||||
|
type CanvasOverview = any;
|
||||||
|
const { overview } = defineProps<{
|
||||||
|
overview: CanvasOverview
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { data: canvas, status } = await useFetch(`/api/file/content/${encodeURIComponent(overview.path)}`, {
|
||||||
|
transform: (input) => input && input.content ? JSON.parse(input?.content) as CanvasContent : undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
const { user } = useUserSession();
|
||||||
|
const isOwner = computed(() => user.value?.id === overview.owner);
|
||||||
|
|
||||||
|
const dispX = ref(0), dispY = ref(0), minZoom = ref(0.1), zoom = ref(0.5);
|
||||||
|
const canvasRef = useTemplateRef('canvasRef');
|
||||||
|
|
||||||
|
const reset = (_: MouseEvent) => {
|
||||||
|
zoom.value = minZoom.value;
|
||||||
|
|
||||||
|
dispX.value = 0;
|
||||||
|
dispY.value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
stroke-light-red
|
||||||
|
stroke-light-orange
|
||||||
|
stroke-light-yellow
|
||||||
|
stroke-light-green
|
||||||
|
stroke-light-cyan
|
||||||
|
stroke-light-purple
|
||||||
|
dark:stroke-dark-red
|
||||||
|
dark:stroke-dark-orange
|
||||||
|
dark:stroke-dark-yellow
|
||||||
|
dark:stroke-dark-green
|
||||||
|
dark:stroke-dark-cyan
|
||||||
|
dark:stroke-dark-purple
|
||||||
|
fill-light-red
|
||||||
|
fill-light-orange
|
||||||
|
fill-light-yellow
|
||||||
|
fill-light-green
|
||||||
|
fill-light-cyan
|
||||||
|
fill-light-purple
|
||||||
|
dark:fill-dark-red
|
||||||
|
dark:fill-dark-orange
|
||||||
|
dark:fill-dark-yellow
|
||||||
|
dark:fill-dark-green
|
||||||
|
dark:fill-dark-cyan
|
||||||
|
dark:fill-dark-purple
|
||||||
|
bg-light-red
|
||||||
|
bg-light-orange
|
||||||
|
bg-light-yellow
|
||||||
|
bg-light-green
|
||||||
|
bg-light-cyan
|
||||||
|
bg-light-purple
|
||||||
|
dark:bg-dark-red
|
||||||
|
dark:bg-dark-orange
|
||||||
|
dark:bg-dark-yellow
|
||||||
|
dark:bg-dark-green
|
||||||
|
dark:bg-dark-cyan
|
||||||
|
dark:bg-dark-purple
|
||||||
|
border-light-red
|
||||||
|
border-light-orange
|
||||||
|
border-light-yellow
|
||||||
|
border-light-green
|
||||||
|
border-light-cyan
|
||||||
|
border-light-purple
|
||||||
|
dark:border-dark-red
|
||||||
|
dark:border-dark-orange
|
||||||
|
dark:border-dark-yellow
|
||||||
|
dark:border-dark-green
|
||||||
|
dark:border-dark-cyan
|
||||||
|
dark:border-dark-purple
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
const cancelEvent = (e: Event) => e.preventDefault()
|
||||||
|
useHover(({ hovering }) => {
|
||||||
|
if (!hovering) {
|
||||||
|
//@ts-ignore
|
||||||
|
window.removeEventListener('wheel', cancelEvent, { passive: false });
|
||||||
|
document.removeEventListener('gesturestart', cancelEvent)
|
||||||
|
document.removeEventListener('gesturechange', cancelEvent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('wheel', cancelEvent, { passive: false });
|
||||||
|
document.addEventListener('gesturestart', cancelEvent)
|
||||||
|
document.addEventListener('gesturechange', cancelEvent)
|
||||||
|
}, {
|
||||||
|
domTarget: canvasRef,
|
||||||
|
})
|
||||||
|
|
||||||
|
const dragHandler = useDrag(({ delta: [x, y] }: { delta: number[] }) => {
|
||||||
|
dispX.value += x / zoom.value;
|
||||||
|
dispY.value += y / zoom.value;
|
||||||
|
}, {
|
||||||
|
domTarget: canvasRef,
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
const wheelHandler = useWheel(({ delta: [x, y] }: { delta: number[] }) => {
|
||||||
|
zoom.value = clamp(zoom.value + y * -0.001, minZoom.value, 3);
|
||||||
|
}, {
|
||||||
|
domTarget: canvasRef,
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
const pinchHandler = usePinch(({ offset: [z] }: { offset: number[] }) => {
|
||||||
|
zoom.value = clamp(z / 2048, minZoom.value, 3);
|
||||||
|
}, {
|
||||||
|
domTarget: canvasRef,
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="status === 'pending'" />
|
||||||
|
<div v-else-if="canvas">
|
||||||
|
<div ref="canvasRef" class="absolute top-0 left-0 overflow-hidden w-full h-full touch-none" :style="{ '--zoom-multiplier': (1 / Math.pow(zoom, 0.7)) }">
|
||||||
|
<div class="flex flex-col absolute sm:top-2 top-10 left-2 z-[35] overflow-hidden gap-4">
|
||||||
|
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10">
|
||||||
|
<Tooltip message="Zoom avant" side="right">
|
||||||
|
<div @click="zoom = clamp(zoom * 1.1, minZoom, 3)" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
||||||
|
<Icon icon="radix-icons:plus" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip message="Reset" side="right">
|
||||||
|
<div @click="zoom = 1" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
||||||
|
<Icon icon="radix-icons:reload" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip message="Tout contenir" side="right">
|
||||||
|
<div @click="reset" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
||||||
|
<Icon icon="radix-icons:corners" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip message="Zoom arrière" side="right">
|
||||||
|
<div @click="zoom = clamp(zoom * 0.9, minZoom, 3)" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
||||||
|
<Icon icon="radix-icons:minus" />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10" v-if="isOwner">
|
||||||
|
<Tooltip message="Modifier" side="right">
|
||||||
|
<NuxtLink :to="{ name: 'explore-edit', hash: '#' + overview.path }" class="w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer">
|
||||||
|
<Icon icon="radix-icons:pencil-1" />
|
||||||
|
</NuxtLink>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CanvasRenderer :style="{transform: `scale(${zoom}) translate(${dispX}px, ${dispY}px)`}" :canvas="canvas" :zoom="zoom" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
type MarkdownOverview = any;
|
||||||
|
const { overview } = defineProps<{
|
||||||
|
overview: MarkdownOverview
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { data: content, status } = await useFetch(`/api/file/content/${encodeURIComponent(overview.path)}`, { watch: [overview] });
|
||||||
|
|
||||||
|
const { user } = useUserSession();
|
||||||
|
const isOwner = computed(() => user.value?.id === overview.owner);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-1 justify-start items-start flex-col xl:px-24 md:px-8 px-4 py-6">
|
||||||
|
<Loading v-if="status === 'pending'" />
|
||||||
|
<template v-else>
|
||||||
|
<div class="flex flex-1 flex-row justify-between items-center">
|
||||||
|
<ProseH1>{{ overview.title }}</ProseH1>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<NuxtLink :href="{ name: 'explore-edit', hash: '#' + overview.path }" v-if="isOwner"><Button>Modifier</Button></NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<MarkdownRenderer v-if="content" :content="content.content" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -1,21 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<NuxtLink class="text-accent-blue inline-flex items-center" v-if="overview"
|
<NuxtLink class="text-accent-blue inline-flex items-center"
|
||||||
:to="{ name: 'explore-path', params: { path: overview.path }, hash: hash }" :class="class">
|
:to="overview ? { name: 'explore-path', params: { path: overview.path }, hash: hash } : href" :class="class">
|
||||||
<HoverCard class="max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]" :class="{'overflow-hidden !p-0': overview.type === 'canvas'}" @open="load">
|
<HoverCard class="max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]" :class="{'overflow-hidden !p-0': overview?.type === 'canvas'}" @open="load" :disabled="!overview">
|
||||||
<template #content>
|
<template #content>
|
||||||
<template v-if="loading">
|
<Loading v-if="loading" />
|
||||||
<Loading />
|
<MarkdownRenderer v-else-if="overview?.type === 'markdown'" class="px-10" :content="content!.content" :filter="hash.substring(1)" />
|
||||||
</template>
|
<Canvas v-else-if="overview?.type === 'canvas'" class="w-[600px] h-[600px] relative" :canvas="JSON.parse(content!.content)" />
|
||||||
<template v-else-if="overview.type === 'markdown'">
|
|
||||||
<div class="px-10">
|
|
||||||
<Markdown :content="content!.content" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="overview.type === 'canvas'">
|
|
||||||
<div class="w-[600px] h-[600px] relative">
|
|
||||||
<Canvas :canvas="JSON.parse(content!.content)" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<slot v-bind="$attrs"></slot>
|
<slot v-bind="$attrs"></slot>
|
||||||
|
|
@ -23,11 +13,6 @@
|
||||||
</template>
|
</template>
|
||||||
</HoverCard>
|
</HoverCard>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink v-else-if="href" :to="href" :class="class" class="text-accent-blue inline-flex items-center">
|
|
||||||
<slot v-bind="$attrs"></slot>
|
|
||||||
<Icon class="w-4 h-4 inline-block" v-if="overview && overview.type !== 'markdown'" :height="20" :width="20" :icon="iconByType[overview.type]" />
|
|
||||||
</NuxtLink>
|
|
||||||
<slot :class="class" v-else v-bind="$attrs"></slot>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
|
||||||
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
|
|
@ -48,6 +48,11 @@ export const explorerContentTable = sqliteTable("explorer_content", {
|
||||||
timestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
timestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const emailValidationTable = sqliteTable("email_validation", {
|
||||||
|
id: text().primaryKey(),
|
||||||
|
timestamp: int({ mode: 'timestamp' }).notNull(),
|
||||||
|
})
|
||||||
|
|
||||||
export const usersRelation = relations(usersTable, ({ one, many }) => ({
|
export const usersRelation = relations(usersTable, ({ one, many }) => ({
|
||||||
data: one(usersDataTable, { fields: [usersTable.id], references: [usersDataTable.id], }),
|
data: one(usersDataTable, { fields: [usersTable.id], references: [usersDataTable.id], }),
|
||||||
session: many(userSessionsTable),
|
session: many(userSessionsTable),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
CREATE TABLE `email_validation` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`timestamp` integer NOT NULL
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,359 @@
|
||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "a2731c1f-4150-4423-946e-670d794f8961",
|
||||||
|
"prevId": "b6acf5d6-d8df-4308-8d4d-55c25741cc4f",
|
||||||
|
"tables": {
|
||||||
|
"email_validation": {
|
||||||
|
"name": "email_validation",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"explorer_content": {
|
||||||
|
"name": "explorer_content",
|
||||||
|
"columns": {
|
||||||
|
"path": {
|
||||||
|
"name": "path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"name": "owner",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "blob",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"navigable": {
|
||||||
|
"name": "navigable",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"private": {
|
||||||
|
"name": "private",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"name": "order",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"visit": {
|
||||||
|
"name": "visit",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"explorer_content_owner_users_id_fk": {
|
||||||
|
"name": "explorer_content_owner_users_id_fk",
|
||||||
|
"tableFrom": "explorer_content",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"owner"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"user_permissions": {
|
||||||
|
"name": "user_permissions",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"permission": {
|
||||||
|
"name": "permission",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"user_permissions_id_users_id_fk": {
|
||||||
|
"name": "user_permissions_id_users_id_fk",
|
||||||
|
"tableFrom": "user_permissions",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"user_permissions_id_permission_pk": {
|
||||||
|
"columns": [
|
||||||
|
"id",
|
||||||
|
"permission"
|
||||||
|
],
|
||||||
|
"name": "user_permissions_id_permission_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"user_sessions": {
|
||||||
|
"name": "user_sessions",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"user_sessions_user_id_users_id_fk": {
|
||||||
|
"name": "user_sessions_user_id_users_id_fk",
|
||||||
|
"tableFrom": "user_sessions",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"user_sessions_id_user_id_pk": {
|
||||||
|
"columns": [
|
||||||
|
"id",
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"name": "user_sessions_id_user_id_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"users_data": {
|
||||||
|
"name": "users_data",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"signin": {
|
||||||
|
"name": "signin",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"lastTimestamp": {
|
||||||
|
"name": "lastTimestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"logCount": {
|
||||||
|
"name": "logCount",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"users_data_id_users_id_fk": {
|
||||||
|
"name": "users_data_id_users_id_fk",
|
||||||
|
"tableFrom": "users_data",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"name": "users",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"name": "username",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"hash": {
|
||||||
|
"name": "hash",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"name": "state",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"users_username_unique": {
|
||||||
|
"name": "users_username_unique",
|
||||||
|
"columns": [
|
||||||
|
"username"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
},
|
||||||
|
"users_email_unique": {
|
||||||
|
"name": "users_email_unique",
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
},
|
||||||
|
"users_hash_unique": {
|
||||||
|
"name": "users_hash_unique",
|
||||||
|
"columns": [
|
||||||
|
"hash"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -36,6 +36,13 @@
|
||||||
"when": 1732722840534,
|
"when": 1732722840534,
|
||||||
"tag": "0004_ancient_thunderball",
|
"tag": "0004_ancient_thunderball",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 5,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1734426608563,
|
||||||
|
"tag": "0005_panoramic_slayback",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -153,7 +153,7 @@ export default defineNuxtConfig({
|
||||||
xssValidator: false,
|
xssValidator: false,
|
||||||
},
|
},
|
||||||
sitemap: {
|
sitemap: {
|
||||||
exclude: ['/admin/**', '/explore/edit', '/user/mailvalidated'],
|
exclude: ['/admin/**', '/explore/edit', '/user/mailvalidated', '/user/changing-password', '/user/reset-password'],
|
||||||
sources: ['/api/__sitemap__/urls']
|
sources: ['/api/__sitemap__/urls']
|
||||||
},
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"drizzle-orm": "^0.35.3",
|
"drizzle-orm": "^0.35.3",
|
||||||
"hast": "^1.0.0",
|
"hast": "^1.0.0",
|
||||||
|
"hast-util-heading": "^3.0.0",
|
||||||
|
"hast-util-heading-rank": "^3.0.0",
|
||||||
"lodash.capitalize": "^4.2.1",
|
"lodash.capitalize": "^4.2.1",
|
||||||
"mdast-util-find-and-replace": "^3.0.1",
|
"mdast-util-find-and-replace": "^3.0.1",
|
||||||
"nodemailer": "^6.9.16",
|
"nodemailer": "^6.9.16",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="overviewStatus === 'pending'" class="flex">
|
<div v-if="status === 'pending'" class="flex">
|
||||||
<Head>
|
<Head>
|
||||||
<Title>d[any] - Chargement</Title>
|
<Title>d[any] - Chargement</Title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
@ -9,37 +9,15 @@
|
||||||
<Head>
|
<Head>
|
||||||
<Title>d[any] - {{ overview.title }}</Title>
|
<Title>d[any] - {{ overview.title }}</Title>
|
||||||
</Head>
|
</Head>
|
||||||
<template v-if="overview.type === 'markdown'">
|
<Markdown v-if="overview.type === 'markdown'" :overview="overview" />
|
||||||
<div class="flex flex-1 justify-start items-start flex-col xl:px-24 md:px-8 px-4 py-6">
|
<Canvas v-else-if="overview.type === 'canvas'" :overview="overview" />
|
||||||
<div class="flex flex-1 flex-row justify-between items-center">
|
<ProseH2 v-else class="flex-1 text-center">Impossible d'afficher le contenu demandé</ProseH2>
|
||||||
<ProseH1>{{ overview.title }}</ProseH1>
|
|
||||||
<div class="flex gap-4">
|
|
||||||
<NuxtLink :href="{ name: 'explore-edit', hash: '#' + overview.path }" v-if="isOwner"><Button>Modifier</Button></NuxtLink>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div v-else-if="status === 'error'">
|
||||||
<Markdown v-if="content" :content="content.content" />
|
|
||||||
<Loading v-else />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="overview.type === 'canvas'">
|
|
||||||
<Canvas v-if="content" :canvas="JSON.parse(content.content)" />
|
|
||||||
<Loading v-else />
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<ProseH2 class="flex-1 text-center">Impossible d'afficher le contenu demandé</ProseH2>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="overviewStatus === 'error'">
|
|
||||||
<Head>
|
<Head>
|
||||||
<Title>d[any] - Erreur</Title>
|
<Title>d[any] - Erreur</Title>
|
||||||
</Head>
|
</Head>
|
||||||
<span>{{ overviewError?.message }}</span>
|
<span>{{ error?.message }}</span>
|
||||||
</div>
|
|
||||||
<div v-else-if="contentStatus === 'error'">
|
|
||||||
<Head>
|
|
||||||
<Title>d[any] - Erreur</Title>
|
|
||||||
</Head>
|
|
||||||
<span>{{ contentError?.message }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<Head>
|
<Head>
|
||||||
|
|
@ -53,9 +31,5 @@
|
||||||
const route = useRouter().currentRoute;
|
const route = useRouter().currentRoute;
|
||||||
const path = computed(() => Array.isArray(route.value.params.path) ? route.value.params.path[0] : route.value.params.path);
|
const path = computed(() => Array.isArray(route.value.params.path) ? route.value.params.path[0] : route.value.params.path);
|
||||||
|
|
||||||
const { user } = useUserSession();
|
const { data: overview, status, error } = await useFetch(`/api/file/overview/${encodeURIComponent(path.value)}`, { watch: [route, path], });
|
||||||
|
|
||||||
const { data: overview, status: overviewStatus, error: overviewError } = await useFetch(`/api/file/overview/${encodeURIComponent(path.value)}`, { watch: [route, path], });
|
|
||||||
const { data: content, status: contentStatus, error: contentError } = await useFetch(`/api/file/content/${encodeURIComponent(path.value)}`, { watch: [route, path], });
|
|
||||||
const isOwner = computed(() => user.value?.id === overview.value?.owner);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -152,7 +152,7 @@
|
||||||
</SplitterPanel>
|
</SplitterPanel>
|
||||||
<SplitterResizeHandle class="bg-light-35 dark:bg-dark-35 w-px xl!mx-4 mx-2" />
|
<SplitterResizeHandle class="bg-light-35 dark:bg-dark-35 w-px xl!mx-4 mx-2" />
|
||||||
<SplitterPanel asChild collapsible :collapsedSize="0" :minSize="20" v-slot="{ isCollapsed }">
|
<SplitterPanel asChild collapsible :collapsedSize="0" :minSize="20" v-slot="{ isCollapsed }">
|
||||||
<div class="flex-1 max-h-full !overflow-y-auto px-8" :class="{ 'hidden': isCollapsed }"><Markdown :content="debounced" :proses="{ 'a': FakeA }" /></div>
|
<div class="flex-1 max-h-full !overflow-y-auto px-8" :class="{ 'hidden': isCollapsed }"><MarkdownRenderer :content="debounced" :proses="{ 'a': FakeA }" /></div>
|
||||||
</SplitterPanel>
|
</SplitterPanel>
|
||||||
</SplitterGroup>
|
</SplitterGroup>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
<template>
|
||||||
|
<Head>
|
||||||
|
<Title>d[any] - Reinitialisation de mon mot de passe</Title>
|
||||||
|
</Head>
|
||||||
|
<div class="flex flex-1 flex-col justify-center items-center">
|
||||||
|
<div class="flex gap-8 items-center">
|
||||||
|
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
||||||
|
<ProseH4>Reinitialisation de mon mot de passe</ProseH4>
|
||||||
|
</div>
|
||||||
|
<form @submit.prevent="() => submit()" class="flex flex-1 flex-col justify-center items-stretch">
|
||||||
|
<TextInput type="text" label="Utilisateur ou email" autocomplete="username" v-model="email"/>
|
||||||
|
<Button class="border border-light-35 dark:border-dark-35 self-center" :loading="status === 'pending'">Envoyer un email</Button>
|
||||||
|
</form>
|
||||||
|
<div v-if="status === 'success'" class="border border-light-green dark:border-dark-green bg-light-greenBack dark:bg-dark-greenBack text-wrap mt-4 py-2 px-4 max-w-96">
|
||||||
|
Un mail vous a été envoyé si un compte existe pour cet identifiant.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'login',
|
||||||
|
usersGoesTo: '/user/profile',
|
||||||
|
});
|
||||||
|
|
||||||
|
const toaster = useToast();
|
||||||
|
|
||||||
|
const email = ref(''), status = ref<'idle' | 'pending' | 'success' | 'error'>('idle');
|
||||||
|
|
||||||
|
async function submit()
|
||||||
|
{
|
||||||
|
status.value = 'pending';
|
||||||
|
try {
|
||||||
|
await $fetch(`/api/auth/request-reset`, {
|
||||||
|
body: { profile: email.value },
|
||||||
|
method: 'post',
|
||||||
|
});
|
||||||
|
status.value = 'success';
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
status.value = 'error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
<template>
|
||||||
|
<Head>
|
||||||
|
<Title>d[any] - Reinitialisation de mon mot de passe</Title>
|
||||||
|
</Head>
|
||||||
|
<div class="flex flex-1 flex-col justify-center items-center">
|
||||||
|
<div class="flex gap-8 items-center">
|
||||||
|
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
||||||
|
<ProseH4>Reinitialisation de mon mot de passe</ProseH4>
|
||||||
|
</div>
|
||||||
|
<form @submit.prevent="submit" class="flex flex-1 flex-col justify-center items-stretch">
|
||||||
|
<TextInput type="password" label="Nouveau mot de passe" autocomplete="newPassword" v-model="newPasswd" :class="{ '!border-light-red !dark:border-dark-red': error }"/>
|
||||||
|
<div class="grid grid-cols-2 flex-col font-light border border-light-35 dark:border-dark-35 px-4 py-2 m-4 ms-0 text-sm leading-[18px] lg:text-base order-8 col-span-2 md:col-span-1 md:order-none">
|
||||||
|
<span class="col-span-2">Prérequis de sécurité</span>
|
||||||
|
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedLength}"><Icon v-show="!checkedLength" icon="radix-icons:cross-2" />8 à 128 caractères</span>
|
||||||
|
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedLower}"><Icon v-show="!checkedLower" icon="radix-icons:cross-2" />Une minuscule</span>
|
||||||
|
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedUpper}"><Icon v-show="!checkedUpper" icon="radix-icons:cross-2" />Une majuscule</span>
|
||||||
|
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedDigit}"><Icon v-show="!checkedDigit" icon="radix-icons:cross-2" />Un chiffre</span>
|
||||||
|
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedSymbol}"><Icon v-show="!checkedSymbol" icon="radix-icons:cross-2" />Un caractère special</span>
|
||||||
|
</div>
|
||||||
|
<TextInput type="password" label="Repeter le nouveau mot de passe" autocomplete="newPassword" v-model="repeatPasswd" :class="{ 'border-light-red dark:border-dark-red': manualError }"/>
|
||||||
|
<Button class="border border-light-35 dark:border-dark-35 self-center" :loading="status === 'pending'">Reinitialiser</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'login',
|
||||||
|
usersGoesTo: '/user/login',
|
||||||
|
});
|
||||||
|
|
||||||
|
const query = useRouter().currentRoute.value.query;
|
||||||
|
|
||||||
|
const toaster = useToast();
|
||||||
|
const status = ref<'idle' | 'pending' | 'success' | 'error'>('idle'), manualError = ref(false);
|
||||||
|
const oldPasswd = ref(''), newPasswd = ref(''), repeatPasswd = ref('');
|
||||||
|
|
||||||
|
const checkedLength = computed(() => newPasswd.value.length >= 8 && newPasswd.value.length <= 128);
|
||||||
|
const checkedLower = computed(() => newPasswd.value.toUpperCase() !== newPasswd.value);
|
||||||
|
const checkedUpper = computed(() => newPasswd.value.toLowerCase() !== newPasswd.value);
|
||||||
|
const checkedDigit = computed(() => /[0-9]/.test(newPasswd.value));
|
||||||
|
const checkedSymbol = computed(() => " !\"#$%&'()*+,-./:;<=>?@[]^_`{|}~".split("").some(e => newPasswd.value.includes(e)));
|
||||||
|
|
||||||
|
const equalsPasswd = computed(() => newPasswd.value && repeatPasswd.value && newPasswd.value === repeatPasswd.value);
|
||||||
|
|
||||||
|
const error = computed(() => !checkedLength.value || !checkedLower.value || !checkedUpper.value || !checkedDigit.value || !checkedSymbol.value);
|
||||||
|
|
||||||
|
async function submit()
|
||||||
|
{
|
||||||
|
if(!equalsPasswd.value)
|
||||||
|
{
|
||||||
|
manualError.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
manualError.value = false;
|
||||||
|
status.value = 'pending';
|
||||||
|
try {
|
||||||
|
const result = await $fetch(`/api/users/${query.i}/reset-password`, {
|
||||||
|
method: 'post',
|
||||||
|
body: {
|
||||||
|
password: newPasswd.value,
|
||||||
|
},
|
||||||
|
query: query,
|
||||||
|
});
|
||||||
|
|
||||||
|
if(result && result.success)
|
||||||
|
{
|
||||||
|
status.value = 'success';
|
||||||
|
|
||||||
|
toaster.add({ content: 'Votre mot de passe a été modifié avec succès.', duration: 10000, timer: true, type: 'success' });
|
||||||
|
useRouter().push({ name: 'user-login' });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw result.error ?? new Error('Erreur inconnue.');
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
status.value = 'error';
|
||||||
|
|
||||||
|
const err = e as any;
|
||||||
|
toaster.add({ content: err?.data?.message ?? err?.message ?? 'Erreur inconnue', duration: 10000, timer: true, type: 'error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
<template>
|
||||||
|
<Head>
|
||||||
|
<Title>d[any] - Modification de mon mot de passe</Title>
|
||||||
|
</Head>
|
||||||
|
<div class="flex flex-1 flex-col justify-center items-center">
|
||||||
|
<div class="flex gap-8 items-center">
|
||||||
|
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
||||||
|
<ProseH4>Modification de mon mot de passe</ProseH4>
|
||||||
|
</div>
|
||||||
|
<form @submit.prevent="submit" class="flex flex-1 flex-col justify-center items-stretch">
|
||||||
|
<TextInput type="password" label="Ancien mot de passe" autocomplete="currentPassword" v-model="oldPasswd"/>
|
||||||
|
<TextInput type="password" label="Nouveau mot de passe" autocomplete="newPassword" v-model="newPasswd" :class="{ 'border-light-red dark:border-dark-red': error }"/>
|
||||||
|
<div class="grid grid-cols-2 flex-col font-light border border-light-35 dark:border-dark-35 px-4 py-2 m-4 ms-0 text-sm leading-[18px] lg:text-base order-8 col-span-2 md:col-span-1 md:order-none">
|
||||||
|
<span class="col-span-2">Prérequis de sécurité</span>
|
||||||
|
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedLength}"><Icon v-show="!checkedLength" icon="radix-icons:cross-2" />8 à 128 caractères</span>
|
||||||
|
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedLower}"><Icon v-show="!checkedLower" icon="radix-icons:cross-2" />Une minuscule</span>
|
||||||
|
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedUpper}"><Icon v-show="!checkedUpper" icon="radix-icons:cross-2" />Une majuscule</span>
|
||||||
|
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedDigit}"><Icon v-show="!checkedDigit" icon="radix-icons:cross-2" />Un chiffre</span>
|
||||||
|
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedSymbol}"><Icon v-show="!checkedSymbol" icon="radix-icons:cross-2" />Un caractère special</span>
|
||||||
|
</div>
|
||||||
|
<TextInput type="password" label="Repeter le nouveau mot de passe" autocomplete="newPassword" v-model="repeatPasswd" :class="{ 'border-light-red dark:border-dark-red': manualError }"/>
|
||||||
|
<Button class="border border-light-35 dark:border-dark-35 self-center" :loading="status === 'pending'">Mettre à jour mon mot de passe</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'login',
|
||||||
|
guestsGoesTo: '/user/login',
|
||||||
|
});
|
||||||
|
|
||||||
|
const toaster = useToast();
|
||||||
|
const { user } = useUserSession();
|
||||||
|
const status = ref<'idle' | 'pending' | 'success' | 'error'>('idle'), manualError = ref(false);
|
||||||
|
const oldPasswd = ref(''), newPasswd = ref(''), repeatPasswd = ref('');
|
||||||
|
|
||||||
|
const checkedLength = computed(() => newPasswd.value.length >= 8 && newPasswd.value.length <= 128);
|
||||||
|
const checkedLower = computed(() => newPasswd.value.toUpperCase() !== newPasswd.value);
|
||||||
|
const checkedUpper = computed(() => newPasswd.value.toLowerCase() !== newPasswd.value);
|
||||||
|
const checkedDigit = computed(() => /[0-9]/.test(newPasswd.value));
|
||||||
|
const checkedSymbol = computed(() => " !\"#$%&'()*+,-./:;<=>?@[]^_`{|}~".split("").some(e => newPasswd.value.includes(e)));
|
||||||
|
|
||||||
|
const equalsPasswd = computed(() => newPasswd.value && repeatPasswd.value && newPasswd.value === repeatPasswd.value);
|
||||||
|
|
||||||
|
const error = computed(() => !checkedLength.value || !checkedLower.value || !checkedUpper.value || !checkedDigit.value || !checkedSymbol.value);
|
||||||
|
|
||||||
|
async function submit()
|
||||||
|
{
|
||||||
|
if(!equalsPasswd.value)
|
||||||
|
{
|
||||||
|
manualError.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
manualError.value = false;
|
||||||
|
status.value = 'pending';
|
||||||
|
try {
|
||||||
|
const result = await $fetch(`/api/users/${user.value?.id}/change-password`, {
|
||||||
|
method: 'post',
|
||||||
|
body: {
|
||||||
|
oldPassword: oldPasswd.value,
|
||||||
|
newPassword: newPasswd.value,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if(result && result.success)
|
||||||
|
{
|
||||||
|
status.value = 'success';
|
||||||
|
|
||||||
|
toaster.add({ content: 'Votre mot de passe a été modifié avec succès.', duration: 10000, timer: true, type: 'success' });
|
||||||
|
useRouter().push({ name: 'user-profile' });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
status.value = 'error';
|
||||||
|
|
||||||
|
toaster.add({ content: result.error ?? 'Erreur inconnue', duration: 10000, timer: true, type: 'error' });
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
status.value = 'error';
|
||||||
|
|
||||||
|
toaster.add({ content: (e as Error).message ?? e, duration: 10000, timer: true, type: 'error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
<TextInput type="text" label="Utilisateur ou email" autocomplete="username" v-model="state.usernameOrEmail"/>
|
<TextInput type="text" label="Utilisateur ou email" autocomplete="username" v-model="state.usernameOrEmail"/>
|
||||||
<TextInput type="password" label="Mot de passe" autocomplete="current-password" v-model="state.password"/>
|
<TextInput type="password" label="Mot de passe" autocomplete="current-password" v-model="state.password"/>
|
||||||
<Button class="border border-light-35 dark:border-dark-35 self-center" :loading="status === 'pending'">Se connecter</Button>
|
<Button class="border border-light-35 dark:border-dark-35 self-center" :loading="status === 'pending'">Se connecter</Button>
|
||||||
|
<NuxtLink class="mt-4 text-center block text-sm font-semibold tracking-wide hover:text-accent-blue" :to="{ name: 'user-reset-password' }">Mot de passe oublié ?</NuxtLink>
|
||||||
<NuxtLink class="mt-4 text-center block text-sm font-semibold tracking-wide hover:text-accent-blue" :to="{ name: 'user-register' }">Pas de compte ?</NuxtLink>
|
<NuxtLink class="mt-4 text-center block text-sm font-semibold tracking-wide hover:text-accent-blue" :to="{ name: 'user-register' }">Pas de compte ?</NuxtLink>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ async function revalidateUser()
|
||||||
{
|
{
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
await $fetch(`/api/users/${user.value?.id}/revalidate`, {
|
await $fetch(`/api/users/${user.value?.id}/revalidate`, {
|
||||||
method: 'get'
|
method: 'post'
|
||||||
});
|
});
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
toaster.add({ closeable: false, duration: 10000, timer: true, content: 'Un mail vous a été envoyé.', type: 'info' });
|
toaster.add({ closeable: false, duration: 10000, timer: true, content: 'Un mail vous a été envoyé.', type: 'info' });
|
||||||
|
|
@ -54,7 +54,7 @@ async function deleteUser()
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col self-center flex-1 gap-4">
|
<div class="flex flex-col self-center flex-1 gap-4">
|
||||||
<Button @click="clear">Se deconnecter</Button>
|
<Button @click="clear">Se deconnecter</Button>
|
||||||
<Button disabled><Tooltip message="En cours de développement">Modifier mon profil</Tooltip></Button>
|
<NuxtLink :to="{ name: 'user-changing-password' }" class="flex flex-1"><Button>Modifier mon mot de passe</Button></NuxtLink>
|
||||||
<AlertDialogRoot>
|
<AlertDialogRoot>
|
||||||
<AlertDialogTrigger asChild><Button :loading="loading"
|
<AlertDialogTrigger asChild><Button :loading="loading"
|
||||||
class="border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red hover:bg-light-redBack dark:hover:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red dark:focus:shadow-dark-red">Supprimer
|
class="border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red hover:bg-light-redBack dark:hover:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red dark:focus:shadow-dark-red">Supprimer
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ const baseItem = z.object({
|
||||||
navigable: z.boolean(),
|
navigable: z.boolean(),
|
||||||
private: z.boolean(),
|
private: z.boolean(),
|
||||||
order: z.number().finite(),
|
order: z.number().finite(),
|
||||||
content: z.string().optional(),
|
content: z.string().optional().or(z.null()),
|
||||||
});
|
});
|
||||||
export const item: z.ZodType<ProjectItem> = baseItem.extend({
|
export const item: z.ZodType<ProjectItem> = baseItem.extend({
|
||||||
children: z.lazy(() => item.array().optional()),
|
children: z.lazy(() => item.array().optional()),
|
||||||
|
|
|
||||||
|
|
@ -71,18 +71,27 @@ export default defineEventHandler(async (e): Promise<Return> => {
|
||||||
|
|
||||||
db.insert(usersDataTable).values({ id: sql.placeholder('id') }).prepare().run({ id: id.id });
|
db.insert(usersDataTable).values({ id: sql.placeholder('id') }).prepare().run({ id: id.id });
|
||||||
|
|
||||||
logSession(e, await setUserSession(e, { user: { id: id.id, username: body.data.username, email: body.data.email, state: 0, signin: new Date(), permissions: [] } }) as UserSessionRequired);
|
logSession(e, await setUserSession(e, { user: { id: id.id, username: body.data.username, email: body.data.email, state: 0, signin: new Date(), permissions: [], lastTimestamp: new Date(), logCount: 1 } }) as UserSessionRequired);
|
||||||
|
|
||||||
|
const emailId = Bun.hash('register' + id.id + hash, Date.now());
|
||||||
|
const timestamp = Date.now() + 1000 * 60 * 60;
|
||||||
|
|
||||||
|
await runTask('validation', {
|
||||||
|
payload: {
|
||||||
|
type: 'validation',
|
||||||
|
id: emailId, timestamp,
|
||||||
|
}
|
||||||
|
});
|
||||||
await runTask('mail', {
|
await runTask('mail', {
|
||||||
payload: {
|
payload: {
|
||||||
type: 'mail',
|
type: 'mail',
|
||||||
to: [body.data.email],
|
to: [body.data.email],
|
||||||
template: 'registration',
|
template: 'registration',
|
||||||
data: {
|
data: {
|
||||||
|
id: emailId, timestamp,
|
||||||
|
userId: id,
|
||||||
username: body.data.username,
|
username: body.data.username,
|
||||||
timestamp: Date.now(),
|
},
|
||||||
id: id.id,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { hash } from 'bun';
|
||||||
|
import { eq, or } from 'drizzle-orm';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { usersTable } from '~/db/schema';
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
profile: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const db = useDatabase();
|
||||||
|
const body = await readValidatedBody(e, schema.safeParse);
|
||||||
|
|
||||||
|
if (!body.success)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 406);
|
||||||
|
return { success: false, error: body.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = db.select({ id: usersTable.id, email: usersTable.email, username: usersTable.username, hash: usersTable.hash }).from(usersTable).where(or(eq(usersTable.email, body.data.profile), eq(usersTable.username, body.data.profile))).get();
|
||||||
|
|
||||||
|
if(result && result.id)
|
||||||
|
{
|
||||||
|
const id = hash('reset' + result.id + result.hash, Date.now());
|
||||||
|
const timestamp = Date.now() + 1000 * 60 * 60;
|
||||||
|
await runTask('validation', {
|
||||||
|
payload: {
|
||||||
|
type: 'validation',
|
||||||
|
id, timestamp,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await runTask('mail', {
|
||||||
|
payload: {
|
||||||
|
type: 'mail',
|
||||||
|
data: {
|
||||||
|
id, timestamp,
|
||||||
|
userId: result.id,
|
||||||
|
username: result.username,
|
||||||
|
},
|
||||||
|
template: 'reset-password',
|
||||||
|
to: [result.email],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(err: any)
|
||||||
|
{
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
return { success: false, error: err as Error };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { count, eq, sql } from 'drizzle-orm';
|
||||||
|
import { ZodError, type ZodIssue } from 'zod';
|
||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { usersDataTable, usersTable } from '~/db/schema';
|
||||||
|
import { schema } from '~/schemas/registration';
|
||||||
|
import { checkSession, logSession } from '~/server/utils/user';
|
||||||
|
import type { UserSession, UserSessionRequired } from '~/types/auth';
|
||||||
|
|
||||||
|
interface SuccessHandler
|
||||||
|
{
|
||||||
|
success: true;
|
||||||
|
session: UserSession;
|
||||||
|
}
|
||||||
|
interface ErrorHandler
|
||||||
|
{
|
||||||
|
success: false;
|
||||||
|
error: Error | ZodError<{
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
type Return = SuccessHandler | ErrorHandler;
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e): Promise<Return> => {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const session = await getUserSession(e);
|
||||||
|
const db = useDatabase();
|
||||||
|
|
||||||
|
const checkedSession = await checkSession(e, session);
|
||||||
|
|
||||||
|
if(checkedSession !== undefined)
|
||||||
|
return checkedSession;
|
||||||
|
|
||||||
|
const body = await readValidatedBody(e, schema.safeParse);
|
||||||
|
|
||||||
|
if (!body.success)
|
||||||
|
{
|
||||||
|
await clearUserSession(e);
|
||||||
|
|
||||||
|
setResponseStatus(e, 406);
|
||||||
|
return { success: false, error: body.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkUsername = db.select({ count: count() }).from(usersTable).where(eq(usersTable.username, sql.placeholder('username'))).prepare().get({ username: body.data.username });
|
||||||
|
const checkEmail = db.select({ count: count() }).from(usersTable).where(eq(usersTable.email, sql.placeholder('email'))).prepare().get({ email: body.data.email });
|
||||||
|
|
||||||
|
const errors: ZodIssue[] = [];
|
||||||
|
if(!checkUsername || checkUsername.count !== 0)
|
||||||
|
errors.push({ code: 'custom', path: ['username'], message: "Ce nom d'utilisateur est déjà utilisé" });
|
||||||
|
if(!checkEmail || checkEmail.count !== 0)
|
||||||
|
errors.push({ code: 'custom', path: ['email'], message: "Cette adresse mail est déjà utilisée" });
|
||||||
|
|
||||||
|
if(errors.length > 0)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 406);
|
||||||
|
return { success: false, error: new ZodError(errors) };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const hash = await Bun.password.hash(body.data.password);
|
||||||
|
db.insert(usersTable).values({ username: sql.placeholder('username'), email: sql.placeholder('email'), hash: sql.placeholder('hash'), state: sql.placeholder('state') }).prepare().run({ username: body.data.username, email: body.data.email, hash, state: 0 });
|
||||||
|
const id = db.select({ id: usersTable.id }).from(usersTable).where(eq(usersTable.username, sql.placeholder('username'))).prepare().get({ username: body.data.username });
|
||||||
|
|
||||||
|
if(!id || !id.id)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 406);
|
||||||
|
return { success: false, error: new Error('Erreur de création de compte') };
|
||||||
|
}
|
||||||
|
|
||||||
|
db.insert(usersDataTable).values({ id: sql.placeholder('id') }).prepare().run({ id: id.id });
|
||||||
|
|
||||||
|
logSession(e, await setUserSession(e, { user: { id: id.id, username: body.data.username, email: body.data.email, state: 0, signin: new Date(), permissions: [], lastTimestamp: new Date(), logCount: 1 } }) as UserSessionRequired);
|
||||||
|
|
||||||
|
await runTask('mail', {
|
||||||
|
payload: {
|
||||||
|
type: 'mail',
|
||||||
|
to: [body.data.email],
|
||||||
|
template: 'registration',
|
||||||
|
data: {
|
||||||
|
username: body.data.username,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
id: id.id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setResponseStatus(e, 201);
|
||||||
|
return { success: true, session };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(err: any)
|
||||||
|
{
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
return { success: false, error: err as Error };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -31,7 +31,7 @@ export function convertToStorableLinks(content: string, path: string[]): string
|
||||||
return content.replaceAll(/!?\[\[([^\[\]\|\#]+)?(#+[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/g, (e: string, a1?: string, a2?: string , a3?: string) => {
|
return content.replaceAll(/!?\[\[([^\[\]\|\#]+)?(#+[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/g, (e: string, a1?: string, a2?: string , a3?: string) => {
|
||||||
const parsed = parsePath(a1 ?? '%%%%----%%%%----%%%%');
|
const parsed = parsePath(a1 ?? '%%%%----%%%%----%%%%');
|
||||||
const replacer = path.find(e => e.endsWith(parsed));
|
const replacer = path.find(e => e.endsWith(parsed));
|
||||||
const value = `[[${replacer ?? ''}${a2 ?? ''}${(!a3 && replacer !== parsed ? '|' + a1 : a3) ?? ''}]]`;
|
const value = `[[${a1 ? (replacer ?? '') : ''}${a2 ?? ''}${(!a3 && a1 && replacer !== parsed ? '|' + a1 : a3) ?? ''}]]`;
|
||||||
return value;
|
return value;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -50,11 +50,6 @@ export default defineEventHandler(async (e) => {
|
||||||
content.content = convertFromStorableLinks(content.content);
|
content.content = convertFromStorableLinks(content.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(session && session.user && session.user.id !== content.owner)
|
|
||||||
{
|
|
||||||
content.content = content.content.replace(/%%(.+)%%/g, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
return { content: content.content };
|
return { content: content.content };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import useDatabase from '~/composables/useDatabase';
|
||||||
import { explorerContentTable } from '~/db/schema';
|
import { explorerContentTable } from '~/db/schema';
|
||||||
import { project, type ProjectItem } from '~/schemas/project';
|
import { project, type ProjectItem } from '~/schemas/project';
|
||||||
import { parsePath } from "#shared/general.utils";
|
import { parsePath } from "#shared/general.utils";
|
||||||
import { eq, getTableColumns } from "drizzle-orm";
|
import { eq, getTableColumns, sql } from "drizzle-orm";
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
export default defineEventHandler(async (e) => {
|
||||||
const { user } = await getUserSession(e);
|
const { user } = await getUserSession(e);
|
||||||
|
|
@ -42,6 +42,8 @@ export default defineEventHandler(async (e) => {
|
||||||
const item = items[i];
|
const item = items[i];
|
||||||
const old = full[item.match!];
|
const old = full[item.match!];
|
||||||
|
|
||||||
|
const path = [item.parent, parsePath(item.name === '' ? item.title : item.name)].filter(e => !!e).join('/');
|
||||||
|
|
||||||
tx.insert(explorerContentTable).values({
|
tx.insert(explorerContentTable).values({
|
||||||
path: item.path,
|
path: item.path,
|
||||||
owner: user.id,
|
owner: user.id,
|
||||||
|
|
@ -53,7 +55,7 @@ export default defineEventHandler(async (e) => {
|
||||||
content: item.content ?? old.content,
|
content: item.content ?? old.content,
|
||||||
}).onConflictDoUpdate({
|
}).onConflictDoUpdate({
|
||||||
set: {
|
set: {
|
||||||
path: [item.parent, parsePath(item.name === '' ? item.title : item.name)].filter(e => !!e).join('/'),
|
path: path,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
type: item.type,
|
type: item.type,
|
||||||
navigable: item.navigable,
|
navigable: item.navigable,
|
||||||
|
|
@ -64,6 +66,11 @@ export default defineEventHandler(async (e) => {
|
||||||
},
|
},
|
||||||
target: explorerContentTable.path,
|
target: explorerContentTable.path,
|
||||||
}).run();
|
}).run();
|
||||||
|
|
||||||
|
if(item.path !== path)
|
||||||
|
{
|
||||||
|
tx.update(explorerContentTable).set({ content: sql`replace(${explorerContentTable.content}, ${sql.placeholder('old')}, ${sql.placeholder('new')})` }).prepare().run({ 'old': item.path, 'new': path });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for(let i = 0; i < full.length; i++)
|
for(let i = 0; i < full.length; i++)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { and, count, eq } from 'drizzle-orm';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { usersTable } from '~/db/schema';
|
||||||
|
import { schema as registration } from '~/schemas/registration';
|
||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
newPassword: registration.shape.password,
|
||||||
|
oldPassword: registration.shape.password,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const session = await getUserSession(e);
|
||||||
|
|
||||||
|
if(!session || !session.user || !session.user.id)
|
||||||
|
{
|
||||||
|
return createError({
|
||||||
|
statusCode: 401,
|
||||||
|
message: 'Unauthorized',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = getRouterParam(e, 'id');
|
||||||
|
|
||||||
|
if(!id)
|
||||||
|
{
|
||||||
|
return createError({
|
||||||
|
statusCode: 403,
|
||||||
|
message: 'Forbidden',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(session.user.id.toString() !== id)
|
||||||
|
{
|
||||||
|
return createError({
|
||||||
|
statusCode: 401,
|
||||||
|
message: 'Unauthorized',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await readValidatedBody(e, schema.safeParse);
|
||||||
|
|
||||||
|
if(!body.success)
|
||||||
|
throw body.error;
|
||||||
|
|
||||||
|
const db = useDatabase();
|
||||||
|
console.log(body.data.oldPassword, await Bun.password.hash(body.data.oldPassword));
|
||||||
|
const check = db.select({ hash: usersTable.hash }).from(usersTable).where(eq(usersTable.id, session.user.id)).get();
|
||||||
|
|
||||||
|
if(!check || !check.hash)
|
||||||
|
{
|
||||||
|
return createError({
|
||||||
|
statusCode: 401,
|
||||||
|
message: 'Unauthorized',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(!await Bun.password.verify(body.data.oldPassword, check.hash))
|
||||||
|
{
|
||||||
|
return { success: false, error: "Ancien mot de passe incorrect" };
|
||||||
|
}
|
||||||
|
|
||||||
|
db.update(usersTable).set({ hash: await Bun.password.hash(body.data.newPassword) }).where(eq(usersTable.id, session.user.id)).run();
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
catch(err: any)
|
||||||
|
{
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
return createError({
|
||||||
|
statusCode: 500,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { eq, getTableColumns, lte } from 'drizzle-orm';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { emailValidationTable, usersTable } from '~/db/schema';
|
||||||
|
|
||||||
|
const querySchema = z.object({
|
||||||
|
h: z.coerce.string(),
|
||||||
|
i: z.coerce.string(),
|
||||||
|
u: z.coerce.number(),
|
||||||
|
t: z.coerce.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const bodySchema = z.object({
|
||||||
|
password: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const query = await getValidatedQuery(e, querySchema.safeParse);
|
||||||
|
|
||||||
|
if(!query.success)
|
||||||
|
throw query.error;
|
||||||
|
|
||||||
|
if(Bun.hash('2' + query.data.u.toString(), query.data.t).toString() !== query.data.h)
|
||||||
|
{
|
||||||
|
return createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'Requete incorrecte',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(Date.now() > query.data.t + (60 * 60 * 1000))
|
||||||
|
{
|
||||||
|
return createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'La requete a expirée',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await readValidatedBody(e, bodySchema.safeParse);
|
||||||
|
|
||||||
|
if(!body.success)
|
||||||
|
throw body.error;
|
||||||
|
|
||||||
|
const db = useDatabase();
|
||||||
|
db.delete(emailValidationTable).where(lte(emailValidationTable.timestamp, new Date())).run();
|
||||||
|
const validate = db.select(getTableColumns(emailValidationTable)).from(emailValidationTable).where(eq(emailValidationTable.id, query.data.i)).get();
|
||||||
|
|
||||||
|
if(!validate || validate.timestamp <= new Date())
|
||||||
|
{
|
||||||
|
return createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'La requete a expirée',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
db.delete(emailValidationTable).where(eq(emailValidationTable.id, query.data.i)).run();
|
||||||
|
const result = db.select({ state: usersTable.state }).from(usersTable).where(eq(usersTable.id, query.data.u)).get();
|
||||||
|
|
||||||
|
if(result === undefined)
|
||||||
|
{
|
||||||
|
return createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'Aucune donnée utilisateur trouvée',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
db.update(usersTable).set({ hash: await Bun.password.hash(body.data.password) }).where(eq(usersTable.id, query.data.u)).run();
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
catch(err: any)
|
||||||
|
{
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
return { success: false, error: err as Error };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { hash } from "bun";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import useDatabase from "~/composables/useDatabase";
|
import useDatabase from "~/composables/useDatabase";
|
||||||
import { usersTable } from "~/db/schema";
|
import { usersTable } from "~/db/schema";
|
||||||
|
|
@ -31,7 +32,7 @@ export default defineEventHandler(async (e) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const db = useDatabase();
|
const db = useDatabase();
|
||||||
const data = db.select({ state: usersTable.state }).from(usersTable).where(eq(usersTable.id, session.user.id)).get();
|
const data = db.select({ id: usersTable.id, email: usersTable.email, username: usersTable.username, hash: usersTable.hash, state: usersTable.state }).from(usersTable).where(eq(usersTable.id, session.user.id)).get();
|
||||||
|
|
||||||
if(!data)
|
if(!data)
|
||||||
{
|
{
|
||||||
|
|
@ -46,16 +47,25 @@ export default defineEventHandler(async (e) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emailId = hash('register' + data.id + data.hash, Date.now());
|
||||||
|
const timestamp = Date.now() + 1000 * 60 * 60;
|
||||||
|
|
||||||
|
await runTask('validation', {
|
||||||
|
payload: {
|
||||||
|
type: 'validation',
|
||||||
|
id: emailId, timestamp,
|
||||||
|
}
|
||||||
|
});
|
||||||
await runTask('mail', {
|
await runTask('mail', {
|
||||||
payload: {
|
payload: {
|
||||||
type: 'mail',
|
type: 'mail',
|
||||||
to: [session.user.email],
|
to: [data.email],
|
||||||
template: 'registration', //@TODO
|
template: 'registration',
|
||||||
data: {
|
data: {
|
||||||
username: session.user.username,
|
id: emailId, timestamp,
|
||||||
timestamp: Date.now(),
|
userId: id,
|
||||||
id: session.user.id,
|
username: data.username,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -3,12 +3,12 @@
|
||||||
<p style="font-variant: small-caps; margin-bottom: 1rem; font-size: 1.25rem; line-height: 1.75rem;">Bienvenue sur d[any], <span>{{ username }}</span>.</p>
|
<p style="font-variant: small-caps; margin-bottom: 1rem; font-size: 1.25rem; line-height: 1.75rem;">Bienvenue sur d[any], <span>{{ username }}</span>.</p>
|
||||||
<p>Nous vous invitons à valider votre compte afin de profiter de toutes les fonctionnalités de d[any].</p>
|
<p>Nous vous invitons à valider votre compte afin de profiter de toutes les fonctionnalités de d[any].</p>
|
||||||
<div style="padding-top: 1rem; padding-bottom: 1rem; text-align: center;">
|
<div style="padding-top: 1rem; padding-bottom: 1rem; text-align: center;">
|
||||||
<a :href="`https://d-any.com/user/mailvalidation?u=${id}&t=${timestamp}&h=${hash}`" target="_blank"><span style="display: inline-block; border-width: 1px; border-color: #525252; background-color: #e5e5e5; padding-left: 0.75rem; padding-right: 0.75rem; padding-top: 0.25rem; padding-bottom: 0.25rem; font-weight: 200; color: #171717; text-decoration: none;">Je valide mon compte</span></a>
|
<a :href="`https://d-any.com/user/mailvalidation?u=${userId}&i=${id}&t=${timestamp}&h=${hash}`" target="_blank"><span style="display: inline-block; border-width: 1px; border-color: #525252; background-color: #e5e5e5; padding-left: 0.75rem; padding-right: 0.75rem; padding-top: 0.25rem; padding-bottom: 0.25rem; font-weight: 200; color: #171717; text-decoration: none;">Je valide mon compte</span></a>
|
||||||
<span style="display: block; padding-top: 0.5rem; font-size: 0.75rem; line-height: 1rem;">Ce lien est valable 1 heure.</span>
|
<span style="display: block; padding-top: 0.5rem; font-size: 0.75rem; line-height: 1rem;">Ce lien est valable 1 heure.</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>Vous pouvez egalement copier le lien suivant pour valider votre compte: </span>
|
<span>Vous pouvez egalement copier le lien suivant pour valider votre compte: </span>
|
||||||
<pre style="display: inline-block; border-bottom-width: 1px; font-size: 0.75rem; line-height: 1rem; color: #171717; font-weight: 400; text-decoration: none;">{{ `https://d-any.com/user/mailvalidation?u=${id}&t=${timestamp}&h=${hash}` }}</pre>
|
<pre style="display: inline-block; border-bottom-width: 1px; font-size: 0.75rem; line-height: 1rem; color: #171717; font-weight: 400; text-decoration: none;">{{ `https://d-any.com/user/mailvalidation?u=${userId}&i=${id}&t=${timestamp}&h=${hash}` }}</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -17,10 +17,11 @@
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import Bun from 'bun';
|
import Bun from 'bun';
|
||||||
|
|
||||||
const { id, username, timestamp } = defineProps<{
|
const { id, userId, username, timestamp } = defineProps<{
|
||||||
id: number
|
id: number
|
||||||
|
userId: number
|
||||||
username: string
|
username: string
|
||||||
timestamp: number
|
timestamp: number
|
||||||
}>();
|
}>();
|
||||||
const hash = computed(() => Bun.hash(id.toString(), timestamp));
|
const hash = computed(() => Bun.hash('1' + userId.toString(), timestamp));
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<div style="max-width: 800px; margin-left: auto; margin-right: auto;">
|
||||||
|
<p style="font-variant: small-caps; margin-bottom: 1rem; font-size: 1.25rem; line-height: 1.75rem;">Bonjour <span>{{ username }}</span>.</p>
|
||||||
|
<p>Vous avez demandé à réinitialiser votre mot de passe aujourd'hui à {{ format(new Date(timestamp), 'HH:mm') }}.</p>
|
||||||
|
<div style="padding-top: 1rem; padding-bottom: 1rem; text-align: center;">
|
||||||
|
<a :href="`https://d-any.com/user/resetting-password?u=${userId}&i=${id}&t=${timestamp}&h=${hash}`" target="_blank"><span style="display: inline-block; border-width: 1px; border-color: #525252; background-color: #e5e5e5; padding-left: 0.75rem; padding-right: 0.75rem; padding-top: 0.25rem; padding-bottom: 0.25rem; font-weight: 200; color: #171717; text-decoration: none;">Je change mon mot de passe.</span></a>
|
||||||
|
<span style="display: block; padding-top: 0.5rem; font-size: 0.75rem; line-height: 1rem;">Ce lien est valable 1 heure.</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Vous pouvez egalement copier le lien suivant pour changer votre mot de passe: </span>
|
||||||
|
<pre style="display: inline-block; border-bottom-width: 1px; font-size: 0.75rem; line-height: 1rem; color: #171717; font-weight: 400; text-decoration: none;">{{ `https://d-any.com/user/resetting-password?u=${userId}&i=${id}&t=${timestamp}&h=${hash}` }}</pre>
|
||||||
|
</div>
|
||||||
|
<span>Si vous n'êtes pas à l'origine de cette demande, vous n'avez pas à modifier votre mot de passe, ce dernier est conservé tant que vous n'interagissez pas avec le lien ci-dessus.</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import Bun from 'bun';
|
||||||
|
import { format } from '~/shared/general.utils';
|
||||||
|
|
||||||
|
const { id, userId, username, timestamp } = defineProps<{
|
||||||
|
id: number
|
||||||
|
userId: number
|
||||||
|
username: string
|
||||||
|
timestamp: number
|
||||||
|
}>();
|
||||||
|
const hash = computed(() => Bun.hash('2' + userId.toString(), timestamp));
|
||||||
|
</script>
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { eq } from "drizzle-orm";
|
import { eq, getTableColumns, lte } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import useDatabase from "~/composables/useDatabase";
|
import useDatabase from "~/composables/useDatabase";
|
||||||
import { usersTable } from "~/db/schema";
|
import { emailValidationTable, usersTable } from "~/db/schema";
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
h: z.coerce.string(),
|
h: z.coerce.string(),
|
||||||
|
i: z.coerce.string(),
|
||||||
u: z.coerce.number(),
|
u: z.coerce.number(),
|
||||||
t: z.coerce.number(),
|
t: z.coerce.number(),
|
||||||
});
|
});
|
||||||
|
|
@ -15,22 +16,33 @@ export default defineEventHandler(async (e) => {
|
||||||
if(!query.success)
|
if(!query.success)
|
||||||
throw query.error;
|
throw query.error;
|
||||||
|
|
||||||
if(Bun.hash(query.data.u.toString(), query.data.t).toString() !== query.data.h)
|
if(Bun.hash('1' + query.data.u.toString(), query.data.t).toString() !== query.data.h)
|
||||||
{
|
{
|
||||||
return createError({
|
return createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
message: 'Lien incorrect',
|
message: 'Lien incorrect',
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
if(Date.now() > query.data.t + (60 * 60 * 1000))
|
if(Date.now() > query.data.t + (60 * 60 * 1000))
|
||||||
{
|
{
|
||||||
return createError({
|
return createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
message: 'Le lien a expiré',
|
message: 'Le lien a expiré',
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const db = useDatabase();
|
const db = useDatabase();
|
||||||
|
const validate = db.select(getTableColumns(emailValidationTable)).from(emailValidationTable).where(eq(emailValidationTable.id, query.data.i)).get();
|
||||||
|
|
||||||
|
if(!validate || validate.timestamp <= new Date())
|
||||||
|
{
|
||||||
|
return createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'Le lien a expiré',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
db.delete(emailValidationTable).where(lte(emailValidationTable.timestamp, new Date())).run();
|
||||||
const result = db.select({ state: usersTable.state }).from(usersTable).where(eq(usersTable.id, query.data.u)).get();
|
const result = db.select({ state: usersTable.state }).from(usersTable).where(eq(usersTable.id, query.data.u)).get();
|
||||||
|
|
||||||
if(result === undefined)
|
if(result === undefined)
|
||||||
|
|
@ -38,14 +50,14 @@ export default defineEventHandler(async (e) => {
|
||||||
return createError({
|
return createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
message: 'Aucune donnée utilisateur trouvée',
|
message: 'Aucune donnée utilisateur trouvée',
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
if(result?.state === 1)
|
if(result?.state === 1)
|
||||||
{
|
{
|
||||||
return createError({
|
return createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
message: 'Votre compte a déjà été validé',
|
message: 'Votre compte a déjà été validé',
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
db.update(usersTable).set({ state: 1 }).where(eq(usersTable.id, query.data.u)).run();
|
db.update(usersTable).set({ state: 1 }).where(eq(usersTable.id, query.data.u)).run();
|
||||||
|
|
|
||||||
|
|
@ -3,28 +3,24 @@ import { createSSRApp, h } from 'vue';
|
||||||
import { renderToString } from 'vue/server-renderer';
|
import { renderToString } from 'vue/server-renderer';
|
||||||
|
|
||||||
import base from '../components/mail/base.vue';
|
import base from '../components/mail/base.vue';
|
||||||
import registration from '../components/mail/registration.vue';
|
import Registration from '../components/mail/registration.vue';
|
||||||
//import revalidation from '../components/mail/revalidation.vue';
|
import ResetPassword from '../components/mail/reset-password.vue';
|
||||||
|
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const [domain, selector, dkim] = config.mail.dkim.split(":");
|
const [domain, selector, dkim] = config.mail.dkim.split(":");
|
||||||
|
|
||||||
export const templates: Record<string, { component: any, subject: string }> = {
|
export const templates: Record<string, { component: any, subject: string }> = {
|
||||||
"registration": { component: registration, subject: 'Bienvenue sur d[any] 😎' },
|
"registration": { component: Registration, subject: 'Bienvenue sur d[any] 😎' },
|
||||||
// "revalidate-mail": { component: revalidation, subject: 'd[any]: Valider votre email' },
|
"reset-password": { component: ResetPassword, subject: 'Réinitialisation de votre mot de passe' },
|
||||||
};
|
};
|
||||||
|
|
||||||
import 'nitropack/types';
|
|
||||||
import type Mail from 'nodemailer/lib/mailer';
|
import type Mail from 'nodemailer/lib/mailer';
|
||||||
declare module 'nitropack/types'
|
interface MailPayload
|
||||||
{
|
{
|
||||||
interface TaskPayload
|
|
||||||
{
|
|
||||||
type: 'mail'
|
type: 'mail'
|
||||||
to: string[]
|
to: string[]
|
||||||
template: string
|
template: string
|
||||||
data: Record<string, any>
|
data: Record<string, any>
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const transport = nodemailer.createTransport({
|
const transport = nodemailer.createTransport({
|
||||||
|
|
@ -57,7 +53,7 @@ export default defineTask({
|
||||||
throw new Error(`Données inconnues`);
|
throw new Error(`Données inconnues`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = e.payload;
|
const payload = e.payload as MailPayload;
|
||||||
const template = templates[payload.template];
|
const template = templates[payload.template];
|
||||||
|
|
||||||
if(!template)
|
if(!template)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { lt } from "drizzle-orm";
|
||||||
|
import { emailValidationTable } from "~/db/schema";
|
||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
|
||||||
|
interface ValidationPayload
|
||||||
|
{
|
||||||
|
type: 'validation'
|
||||||
|
id: string
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineTask({
|
||||||
|
meta: {
|
||||||
|
name: 'validation',
|
||||||
|
description: 'Add email ID to DB',
|
||||||
|
},
|
||||||
|
async run(e) {
|
||||||
|
try {
|
||||||
|
if(e.payload.type !== 'validation')
|
||||||
|
{
|
||||||
|
throw new Error(`Données inconnues`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = e.payload as ValidationPayload;
|
||||||
|
const db = useDatabase();
|
||||||
|
|
||||||
|
db.delete(emailValidationTable).where(lt(emailValidationTable.timestamp, new Date())).run();
|
||||||
|
db.insert(emailValidationTable).values({ id: payload.id, timestamp: new Date(payload.timestamp) }).run();
|
||||||
|
|
||||||
|
return { result: true };
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
console.error(e);
|
||||||
|
|
||||||
|
return { result: false, error: e };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
7
todo.md
7
todo.md
|
|
@ -1,7 +1,8 @@
|
||||||
- [ ] Rename auto des liens au changement de path
|
- [x] Mot de passe oublié
|
||||||
- [ ] Autocomplete des liens dans l'editeur
|
- [x] Rename auto des liens au changement de path
|
||||||
|
- [x] Filtrage de lien avec le header id
|
||||||
- [ ] Editeur de graphe
|
- [ ] Editeur de graphe
|
||||||
- [ ] Filtrage de lien avec le header id
|
- [ ] Autocomplete des liens dans l'editeur
|
||||||
- [ ] Embed de lien (le ![[]] de Obsidian)
|
- [ ] Embed de lien (le ![[]] de Obsidian)
|
||||||
- [ ] Rework la structure projet
|
- [ ] Rework la structure projet
|
||||||
- [ ] Limite de taille par projet (100 Mo ?)
|
- [ ] Limite de taille par projet (100 Mo ?)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue