Compare commits

..

No commits in common. "f22e63bd4d28cf29de5fa58a4e07decf62bc82ec" and "1c239f161b0a638d0c2963c0746339b911c09519" have entirely different histories.

37 changed files with 234 additions and 792 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -1,16 +1,13 @@
<template>
<template v-for="(item, idx) of options">
<template v-if="item.type === 'item'">
<DropdownMenuItem :disabled="item.disabled" :textValue="item.label" @select="item.select" :class="{'!pe-1': item.kbd}" class="group cursor-pointer text-base text-light-100 dark:text-dark-100 leading-none flex items-center py-1.5 relative ps-7 pe-4 select-none outline-none data-[disabled]:text-light-60 dark:data-[disabled]:text-dark-60 data-[disabled]:pointer-events-none data-[highlighted]:bg-light-35 dark:data-[highlighted]:bg-dark-35">
<DropdownMenuItem :disabled="item.disabled" :textValue="item.label" @select="item.select" :class="{'pe-1': item.kbd}" class="group cursor-pointer text-base text-light-100 dark:text-dark-100 leading-none flex items-center py-1.5 relative ps-7 pe-4 select-none outline-none data-[disabled]:text-light-60 dark:data-[disabled]:text-dark-60 data-[disabled]:pointer-events-none data-[highlighted]:bg-light-35 dark:data-[highlighted]:bg-dark-35">
<Icon v-if="item.icon" :icon="item.icon" class="absolute left-1.5" />
<div class="flex flex-1 justify-between">
<span>{{ item.label }}</span>
<span v-if="item.kbd" class="mx-2 text-xs font-mono text-light-70 dark:text-dark-70 relative top-0.5"> {{ item.kbd }} </span>
</div>
<span>{{ item.label }}</span>
<span v-if="item.kbd" class="mx-2 text-xs font-mono text-light-70 dark:text-dark-70 relative top-0.5"> {{ item.kbd }} </span>
</DropdownMenuItem>
</template>
<!-- TODO -->
<template v-else-if="item.type === 'checkbox'">
<DropdownMenuCheckboxItem :disabled="item.disabled" :textValue="item.label" @update:checked="item.select">
<DropdownMenuItemIndicator>
@ -21,7 +18,6 @@
</DropdownMenuCheckboxItem>
</template>
<!-- TODO -->
<template v-if="item.type === 'radio'">
<DropdownMenuLabel>{{ item.label }}</DropdownMenuLabel>
<DropdownMenuRadioGroup @update:model-value="item.change">

View File

@ -1,6 +1,6 @@
<template>
<Label class="my-2 flex flex-1 items-center justify-between flex-col md:flex-row">
<span class="pb-1 md:p-0">{{ label }}</span>
<Label class="py-4 flex flex-row justify-center items-center">
<span>{{ label }}</span>
<SelectRoot v-model="model">
<SelectTrigger :disabled="disabled" class="mx-4 inline-flex min-w-[160px] items-center justify-between px-3 text-sm font-semibold leading-none h-8 gap-1
bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 outline-none data-[placeholder]:font-normal

View File

@ -5,7 +5,7 @@
class="mx-4 caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50
bg-light-20 dark:bg-dark-20 appearance-none outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20"
:type="type" v-model="model" :data-disabled="disabled || undefined" v-bind="$attrs" @change="(e) => emits('change', e)" @input="(e) => emits('input', e)">
:type="type" v-model="model" :data-disabled="disabled || undefined" v-bind="$attrs">
</Label>
</template>
@ -16,10 +16,5 @@ const { type = 'text', label, disabled = false, placeholder } = defineProps<{
disabled?: boolean
placeholder?: string
}>();
const emits = defineEmits<{
change: [Event]
input: [Event]
}>();
const model = defineModel<string>();
</script>

View File

@ -1,20 +1,56 @@
<template>
<TreeRoot v-slot="{ flattenItems }" class="list-none select-none text-light-100 dark:text-dark-100 p-2 xl:text-base text-sm" :items="model" :get-key="getKey" :defaultExpanded="flatten(model)">
<TreeRoot v-slot="{ flattenItems }" class="list-none select-none text-light-100 dark:text-dark-100 p-2 xl:text-base text-sm" :items="model" :get-key="(item) => item.link ?? item.label" :defaultExpanded="flatten(model)">
<TreeItem v-for="item in flattenItems" v-slot="{ isExpanded }" :key="item._id" :style="{ 'padding-left': `${item.level - 0.5}em` }" v-bind="item.bind" class="flex items-center px-2 outline-none relative cursor-pointer">
<slot :isExpanded="isExpanded" :item="item" />
<NuxtLink :href="item.value.link && !item.hasChildren ? { name: 'explore-path', params: { path: item.value.link } } : undefined" no-prefetch class="flex flex-1 items-center border-light-35 dark:border-dark-35 hover:border-accent-blue" :class="{ 'border-s': !item.hasChildren, 'font-medium': item.hasChildren }" active-class="text-accent-blue border-s-2 !border-accent-blue">
<Icon v-if="item.hasChildren" icon="radix-icons:chevron-right" :class="{ 'rotate-90': isExpanded }" class="h-4 w-4 transition-transform absolute" :style="{ 'left': `${item.level - 1}em` }" />
<div class="pl-3 py-1 flex-1 truncate" :data-tag="item.value.tag">
{{ item.value.label }}
</div>
</NuxtLink>
</TreeItem>
</TreeRoot>
</template>
<script setup lang="ts" generic="T extends Record<string, any>">
const { getKey } = defineProps<{
getKey: (val: T) => string
}>();
<script setup lang="ts">
import { Icon } from '@iconify/vue/dist/iconify.js';
const model = defineModel<T[]>();
function flatten(arr: T[]): string[]
interface TreeItem
{
return arr.filter(e => e.open).flatMap(e => [getKey(e), ...flatten(e.children ?? [])]);
label: string
link?: string
tag?: string
open?: boolean
children?: TreeItem[]
}
</script>
const model = defineModel<TreeItem[]>();
function flatten(arr: TreeItem[]): string[]
{
return arr.filter(e => e.open).flatMap(e => [e?.link ?? e.label, ...flatten(e.children ?? [])]);
}
</script>
<style>
[data-tag="canvas"]:after,
[data-tag="private"]:after
{
@apply text-sm;
@apply font-normal;
@apply float-end;
@apply border ;
@apply border-light-35 ;
@apply dark:border-dark-35;
@apply px-1;
@apply bg-light-20;
@apply dark:bg-dark-20;
font-variant: small-caps;
}
[data-tag="canvas"]:after
{
content: 'Canvas'
}
[data-tag="private"]:after
{
content: 'Privé'
}
</style>

View File

@ -31,8 +31,12 @@
<script setup lang="ts">
import { parseURL } from 'ufo';
import { Icon } from '@iconify/vue/dist/iconify.js';
import { iconByType } from '#shared/general.utils';
const iconByType: Record<string, string> = {
'folder': 'circum:folder-on',
'canvas': 'ph:graph-light',
'file': 'radix-icons:file',
}
const { href } = defineProps<{
href: string
class?: string

BIN
db.sqlite

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -8,7 +8,7 @@
<Icon icon="si:error-line" class="w-12 h-12 text-light-60 dark:text-dark-60"/>
<div class="text-3xl">Une erreur est survenue.</div>
</div>
<pre class="text-center text-wrap">Erreur {{ error?.statusCode }}: {{ error?.message }}</pre>
<pre class="">Erreur {{ error?.statusCode }}: {{ error?.message }}</pre>
<Button @click="handleError">Revenir en lieu sûr</Button>
</div>
</template>

View File

@ -76,21 +76,11 @@
<NuxtLink :href="{ name: 'explore' }" no-prefetch class="flex flex-1 font-bold text-lg items-center border-light-35 dark:border-dark-35 hover:border-accent-blue" active-class="text-accent-blue border-s-2 !border-accent-blue">
<div class="pl-3 py-1 flex-1 truncate">Projet</div>
</NuxtLink>
<Tree v-if="pages" v-model="pages" :getKey="(item) => item.path">
<template #default="{ item, isExpanded }">
<NuxtLink :href="item.value.path && !item.hasChildren ? { name: 'explore-path', params: { path: item.value.path } } : undefined" no-prefetch class="flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple" :class="{ 'font-medium': item.hasChildren }" active-class="text-accent-blue" :data-private="item.value.private">
<Icon v-if="item.hasChildren" icon="radix-icons:chevron-right" :class="{ 'rotate-90': isExpanded }" class="h-4 w-4 transition-transform absolute" :style="{ 'left': `${item.level - 1}em` }" />
<Icon v-else-if="iconByType[item.value.type]" :icon="iconByType[item.value.type]" class="w-5 h-5" />
<div class="pl-3 py-1 flex-1 truncate">
{{ item.value.title }}
</div>
<Tooltip message="Privé" side="right"><Icon v-show="item.value.private" icon="radix-icons:lock-closed" /></Tooltip>
</NuxtLink>
</template>
</Tree>
<Tree v-if="pages" v-model="pages"/>
</div>
<div class="xl:px-12 px-6 text-start text-xs text-light-60 dark:text-dark-60 relative top-4">
<NuxtLink class="hover:underline italic" :to="{ name: 'roadmap' }">Roadmap</NuxtLink> - <NuxtLink class="hover:underline italic" :to="{ name: 'legal' }">Mentions légales</NuxtLink>
<NuxtLink class="hover:underline italic" :to="{ name: 'roadmap' }">Roadmap</NuxtLink> -
<NuxtLink class="hover:underline italic" :to="{ name: 'legal' }">Mentions légales</NuxtLink>
<p>Copyright Peaceultime - 2024</p>
</div>
</div>
@ -103,7 +93,6 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue/dist/iconify.js';
import type { NavigationTreeItem } from '~/server/api/navigation.get';
import { iconByType } from '#shared/general.utils';
const open = ref(false);
const { loggedIn, clear } = useUserSession();
@ -120,8 +109,8 @@ const { data: pages } = await useLazyFetch('/api/navigation', {
watch: [useRouter().currentRoute]
});
function transform(list: NavigationTreeItem[] | undefined): NavigationTreeItem[] | undefined
function transform(list: NavigationTreeItem[] | undefined): any[] | undefined
{
return list?.map(e => ({ ...e, open: path.value?.startsWith(e.path), children: transform(e.children) }));
return list?.map(e => ({ label: e.title, children: transform(e?.children ?? undefined), link: e.path, tag: e.private ? 'private' : e.type, open: path.value?.startsWith(e.path)}))
}
</script>

View File

@ -1,7 +1,4 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
import vuePlugin from 'rollup-plugin-vue'
import postcssPlugin from 'rollup-plugin-postcss'
export default defineNuxtConfig({
compatibilityDate: '2024-04-03',
modules: [
@ -117,42 +114,17 @@ export default defineNuxtConfig({
path: '~/components',
pathPrefix: false,
},
{
path: '~/server/components',
pathPrefix: true,
global: true,
},
],
nitro: {
alias: {
'public': '//public',
},
publicAssets: [{
baseURL: 'public',
dir: 'public',
}],
preset: 'bun',
experimental: {
tasks: true,
},
rollupConfig: {
plugins: [
vuePlugin({ include: /\.vue$/, target: 'node' })
]
},
},
runtimeConfig: {
session: {
password: '699c46bd-9aaa-4364-ad01-510ee4fe7013'
},
database: 'db.sqlite',
mail: {
host: '',
port: '',
user: '',
passwd: '',
dkim: '',
}
database: 'db.sqlite'
},
security: {
rateLimiter: false,

View File

@ -20,7 +20,6 @@
"hast": "^1.0.0",
"lodash.capitalize": "^4.2.1",
"mdast-util-find-and-replace": "^3.0.1",
"nodemailer": "^6.9.16",
"nuxt": "^3.14.159",
"nuxt-security": "^2.0.0",
"radix-vue": "^1.9.8",
@ -30,8 +29,6 @@
"remark-gfm": "^4.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.1",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-vue": "^6.0.0",
"unified": "^11.0.5",
"unist-util-visit": "^5.0.0",
"vue": "latest",
@ -41,7 +38,6 @@
"devDependencies": {
"@types/bun": "^1.1.12",
"@types/lodash.capitalize": "^4.2.9",
"@types/nodemailer": "^6.4.16",
"@types/unist": "^3.0.3",
"better-sqlite3": "^11.5.0",
"bun-types": "^1.1.34",

View File

@ -1,54 +1,27 @@
<script lang="ts">
const mailSchema = z.object({
to: z.string().email(),
template: z.string(),
data: z.string(),
});
const schemaList: Record<string, z.ZodObject<any> | null> = {
'pull': null,
'push': null,
'mail': mailSchema,
}
</script>
<script setup lang="ts">
import { z } from 'zod';
definePageMeta({
rights: ['admin'],
})
const job = ref<string>('');
const toaster = useToast();
const payload = reactive<Record<string, any>>({
data: JSON.stringify({ username: "Peaceultime", id: 1, timestamp: Date.now() }),
to: 'clem31470@gmail.com',
});
const data = ref(), status = ref<'idle' | 'pending' | 'success' | 'error'>('idle'), success = ref(false), error = ref<Error | null>();
const data = ref(), status = ref<'idle' | 'pending' | 'success' | 'error'>('idle'), success = ref(false), err = ref(false), error = ref();
async function fetch()
{
status.value = 'pending';
data.value = null;
error.value = null;
err.value = false;
success.value = false;
try
{
const schema = schemaList[job.value];
if(schema)
{
console.log(payload);
const parsedPayload = schema.parse(payload);
}
data.value = await $fetch(`/api/admin/jobs/${job.value}`, {
method: 'POST',
body: payload,
});
status.value = 'success';
error.value = null;
err.value = false;
success.value = true;
toaster.add({ duration: 10000, content: data.value ?? 'Job executé avec succès', type: 'success', timer: true, });
@ -56,10 +29,11 @@ async function fetch()
catch(e)
{
status.value = 'error';
error.value = e as Error;
error.value = e;
err.value = true;
success.value = false;
toaster.add({ duration: 10000, content: error.value.message, type: 'error', timer: true, });
toaster.add({ duration: 10000, content: error.value, type: 'error', timer: true, });
}
}
</script>
@ -68,20 +42,12 @@ async function fetch()
<Head>
<Title>d[any] - Administration</Title>
</Head>
<div class="flex flex-col justify-start items-center">
<div class="flex flex-col justify-start">
<ProseH2>Administration</ProseH2>
<div class="flex flex-row w-full gap-8">
<Select label="Job" v-model="job">
<SelectItem label="Récupérer les données d'Obsidian" value="pull" />
<SelectItem label="Envoyer les données dans Obsidian" value="push" disabled />
<SelectItem label="Envoyer un mail de test" value="mail" />
</Select>
<Select v-if="job === 'mail'" v-model="payload.template" label="Modèle" class="w-full" ><SelectItem label="Inscription" value="registration" /></Select>
</div>
<div v-if="job === 'mail'" class="flex justify-center items-center flex-col">
<TextInput label="Destinataire" class="w-full" v-model="payload.to" />
<textarea v-model="payload.data" class="w-[640px] bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 outline-none m-2 px-2"></textarea>
</div>
<Select label="Job" v-model="job">
<SelectItem label="Récupérer les données d'Obsidian" value="pull" />
<SelectItem label="Envoyer les données dans Obsidian" value="push" disabled />
</Select>
<Button class="self-center" @click="() => !!job && fetch()" :loading="status === 'pending'">
<span>Executer</span>
</Button>

View File

@ -3,103 +3,40 @@
<Title>d[any] - Configuration du projet</Title>
</Head>
<div class="flex flex-1 flex-row gap-4 p-6 items-start" v-if="navigation">
<div class="flex flex-1 flex-col w-[450px] max-w-[450px] max-h-full">
<DraggableTree class="list-none select-none border border-light-35 dark:border-dark-35 text-light-100 dark:text-dark-100 p-2 xl:text-base text-sm overflow-auto"
:items="navigation ?? undefined" :get-key="(item: Partial<ProjectExtendedItem>) => item.path !== undefined ? getPath(item as ProjectExtendedItem) : ''" @updateTree="drop">
<template #default="{ handleToggle, handleSelect, isExpanded, isSelected, isDragging, item }">
<div class="flex flex-1 items-center px-2" :class="{ 'opacity-50': isDragging }" :style="{ 'padding-left': `${item.level - 0.5}em` }">
<span class="py-2 px-2" @click="handleToggle" v-if="item.hasChildren" >
<Icon :icon="isExpanded ? 'lucide:folder-open' : 'lucide:folder'"/>
</span>
<Icon v-else-if="iconByType[item.value.type]" :icon="iconByType[item.value.type]" class="group-[:hover]:text-accent-purple mx-2" @click="() => { handleSelect(); selected = isSelected ? undefined : item.value; }" />
<div class="pl-3 py-1 flex-1 truncate" :title="item.value.title" @click="() => { handleSelect(); selected = isSelected ? undefined : item.value; }">
{{ item.value.title }}
</div>
<div class="flex gap-2">
<span @click="item.value.private = !item.value.private">
<Icon v-if="item.value.private" icon="radix-icons:lock-closed" />
<Icon v-else class="text-light-50 dark:text-dark-50" icon="radix-icons:lock-open-2" />
</span>
<span @click="item.value.navigable = !item.value.navigable">
<Icon v-if="item.value.navigable" icon="radix-icons:eye-open" />
<Icon v-else class="text-light-50 dark:text-dark-50" icon="radix-icons:eye-none" />
</span>
</div>
<DraggableTree class="list-none select-none border border-light-35 dark:border-dark-35 text-light-100 dark:text-dark-100 p-2 xl:text-base text-sm overflow-auto w-[450px] max-h-full"
:items="navigation ?? undefined" :get-key="(item: Partial<ProjectItem>) => item.path ? getPath(item as ProjectItem) : ''" @updateTree="drop">
<template #default="{ handleToggle, handleSelect, isExpanded, isSelected, isDragging, isDraggedOver, item }">
<div class="flex flex-1 px-2" :class="{ 'opacity-50': isDragging }" :style="{ 'padding-left': `${item.level - 0.5}em` }">
<span class="py-2 px-2" @click="handleToggle" v-if="item.hasChildren" >
<Icon :icon="isExpanded ? 'lucide:folder-open' : 'lucide:folder'"/>
</span>
<div class="ps-2 py-1 flex-1 truncate" :class="{'!ps-4 border-s border-light-35 dark:border-dark-35': !item.hasChildren}" :title="item.value.title" @click="() => { handleSelect(); selected = isSelected ? undefined : item.value; }">
{{ item.value.title }}
</div>
</template>
<template #hint="{ instruction }">
<div v-if="instruction" class="absolute h-full w-full top-0 right-0 border-light-50 dark:border-dark-50" :style="{
width: `calc(100% - ${instruction.currentLevel - 1}em)`
}" :class="{
'!border-b-4': instruction?.type === 'reorder-below',
'!border-t-4': instruction?.type === 'reorder-above',
'!border-4': instruction?.type === 'make-child',
}"></div>
</template>
</DraggableTree>
</div>
</div>
</template>
<template #hint="{ instruction }">
<div v-if="instruction" class="absolute h-full w-full top-0 right-0 border-light-50 dark:border-dark-50" :style="{
width: `calc(100% - ${instruction.currentLevel - 1}em)`
}" :class="{
'!border-b-4': instruction?.type === 'reorder-below',
'!border-t-4': instruction?.type === 'reorder-above',
'!border-4': instruction?.type === 'make-child',
}"></div>
</template>
</DraggableTree>
<div class="flex flex-col flex-1">
<div class="flex self-end gap-4 px-4">
<DropdownMenu align="center" side="bottom" :options="[{
type: 'item',
label: 'Markdown',
kbd: 'Ctrl+N',
icon: 'radix-icons:file',
select: () => add('markdown'),
}, {
type: 'item',
label: 'Dossier',
kbd: 'Ctrl+Shift+N',
icon: 'lucide:folder',
select: () => add('folder'),
}, {
type: 'item',
label: 'Canvas',
icon: 'ph:graph-light',
select: () => add('canvas'),
}, {
type: 'item',
label: 'Fichier',
icon: 'radix-icons:file-text',
select: () => add('file'),
}]">
<Button>Nouveau</Button>
</DropdownMenu>
<Button @click="navigation = tree.remove(navigation, getPath(selected))" v-if="selected" class="border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red focus:shadow-light-red dark:focus:shadow-dark-red">Supprimer</Button>
<Tooltip message="Ctrl+S" side="bottom"><Button @click="() => save(true)" :loading="saveStatus === 'pending'" class="border-light-blue dark:border-dark-blue hover:border-light-blue dark:hover:border-dark-blue focus:shadow-light-blue dark:focus:shadow-dark-blue">Enregistrer</Button></Tooltip>
<Tooltip message="Ctrl+Shift+Z" side="bottom"><NuxtLink :href="{ name: 'explore' }"><Button>Annuler</Button></NuxtLink></Tooltip>
</div>
<div v-if="selected" class="flex-1 flex justify-start items-start">
<div class="flex flex-col flex-1 justify-start items-start">
<input type="text" v-model="selected.title" @change="(e) => {
if(selected && !selected.customPath)
{
selected.name = parsePath(selected.title);
rebuildPath(selected.children, getPath(selected));
}
}" placeholder="Titre" class="flex-1 mx-4 h-16 w-full caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50 appearance-none outline-none px-3 py-1 text-5xl font-thin bg-transparent" />
<div class="flex ms-6 flex-col justify-start items-start gap-2">
<div class="flex flex-col justify-start items-start">
<Switch label="Chemin personnalisé" v-model="selected.customPath" />
<span>
<pre v-if="selected.customPath" class="flex items-center">/{{ selected.parent !== '' ? selected.parent + '/' : '' }}<TextInput v-model="selected.name" @input="(e) => {
if(selected && selected.customPath)
{
selected.name = parsePath(selected.name);
rebuildPath(selected.children, getPath(selected));
}
}" class="mx-0"/></pre>
<pre v-else>/{{ getPath(selected) }}</pre>
</span>
</div>
<div class="flex items-center gap-2">
<HoverCard class="!py-2 !px-4"><Icon icon="radix-icons:question-mark-circled" /><template #content><span class="text-sm italic text-light-60 dark:text-dark-60">Un fichier privé n'est consultable que par le propriétaire du projet. Rendre un dossier privé cache automatiquement son contenu sans avoir à chaque fichier un par un.</span></template></HoverCard>
<Switch label="Privé" v-model="selected.private" />
</div>
<div class="flex items-center gap-2">
<HoverCard class="!py-2 !px-4"><Icon icon="radix-icons:question-mark-circled" /><template #content><span class="text-sm italic text-light-60 dark:text-dark-60">Un fichier navigable est disponible dans le menu de navigation à gauche. Les fichiers non navigable peuvent toujours être utilisés dans des liens.</span></template></HoverCard>
<Switch label="Navigable" v-model="selected.navigable" />
</div>
<input type="text" v-model="selected.title" placeholder="Titre" class="flex-1 mx-4 h-16 w-full caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50 appearance-none outline-none px-3 py-1 text-5xl font-thin bg-transparent" />
<span><pre class="ps-2 inline">{{ selected.parent }}/</pre><input v-model="selected.name" placeholder="Titre" class="font-mono border-b border-light-35 dark:border-dark-35 caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50 appearance-none outline-none bg-transparent" /></span>
<div class="flex ms-6 flex-col justify-start items-start">
<Tooltip message="Consultable uniquement par le propriétaire" side="right"><Switch label="Privé" v-model="selected.private" /></Tooltip>
<Tooltip message="Afficher dans le menu de navigation" side="right"><Switch label="Navigable" v-model="selected.navigable" /></Tooltip>
</div>
</div>
</div>
@ -107,42 +44,26 @@
</div>
</template>
<script setup lang="ts">
import { Icon } from '@iconify/vue/dist/iconify.js';
import type { Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/dist/types/tree-item';
import { parsePath } from '#shared/general.utils';
import type { ProjectItem } from '~/schemas/project';
import type { FileType } from '~/schemas/file';
import { iconByType } from '#shared/general.utils';
interface ProjectExtendedItem extends ProjectItem
{
customPath: boolean
children?: ProjectExtendedItem[]
}
interface ProjectExtended
{
items: ProjectExtendedItem[]
}
definePageMeta({
rights: ['admin', 'editor'],
});
const router = useRouter();
const route = router.currentRoute;
const path = computed(() => Array.isArray(route.value.params.path) ? route.value.params.path[0] : route.value.params.path);
const toaster = useToast();
const saveStatus = ref<'idle' | 'pending' | 'success' | 'error'>('idle');
const { data: project } = await useFetch(`/api/project`, {
transform: (project) =>{
if(project)
(project as ProjectExtended).items = transform(project.items)!;
return project as ProjectExtended;
}
});
const navigation = computed<ProjectExtendedItem[] | undefined>({
const { data: project } = await useFetch(`/api/project`);
const navigation = computed({
get: () => project.value?.items,
set: (value) => {
const proj = project.value;
@ -153,17 +74,15 @@ const navigation = computed<ProjectExtendedItem[] | undefined>({
project.value = proj;
}
});
const selected = ref<ProjectExtendedItem>();
const selected = ref<ProjectItem>();
useShortcuts({
meta_s: { usingInput: true, handler: () => save(false) },
meta_n: { usingInput: true, handler: () => add('markdown') },
meta_shift_n: { usingInput: true, handler: () => add('folder') },
meta_shift_z: { usingInput: true, handler: () => router.push({ name: 'explore' }) }
meta_shift_z: { usingInput: true, handler: () => router.push({ name: 'explore-path', params: { path: path.value }}) }
})
const tree = {
remove(data: ProjectExtendedItem[], id: string): ProjectExtendedItem[] {
remove(data: ProjectItem[], id: string): ProjectItem[] {
return data
.filter(item => getPath(item) !== id)
.map((item) => {
@ -176,7 +95,7 @@ const tree = {
return item;
});
},
insertBefore(data: ProjectExtendedItem[], targetId: string, newItem: ProjectExtendedItem): ProjectExtendedItem[] {
insertBefore(data: ProjectItem[], targetId: string, newItem: ProjectItem): ProjectItem[] {
return data.flatMap((item) => {
if (getPath(item) === targetId)
return [newItem, item];
@ -190,7 +109,7 @@ const tree = {
return item;
});
},
insertAfter(data: ProjectExtendedItem[], targetId: string, newItem: ProjectExtendedItem): ProjectExtendedItem[] {
insertAfter(data: ProjectItem[], targetId: string, newItem: ProjectItem): ProjectItem[] {
return data.flatMap((item) => {
if (getPath(item) === targetId)
return [item, newItem];
@ -205,7 +124,7 @@ const tree = {
return item;
});
},
insertChild(data: ProjectExtendedItem[], targetId: string, newItem: ProjectExtendedItem): ProjectExtendedItem[] {
insertChild(data: ProjectItem[], targetId: string, newItem: ProjectItem): ProjectItem[] {
return data.flatMap((item) => {
if (getPath(item) === targetId) {
// already a parent: add as first child
@ -226,7 +145,7 @@ const tree = {
};
});
},
find(data: ProjectExtendedItem[], itemId: string): ProjectExtendedItem | undefined {
find(data: ProjectItem[], itemId: string): ProjectItem | undefined {
for (const item of data) {
if (getPath(item) === itemId)
return item;
@ -238,27 +157,12 @@ const tree = {
}
}
},
search(data: ProjectExtendedItem[], prop: keyof ProjectExtendedItem, value: string): ProjectExtendedItem[] {
const arr = [];
for (const item of data)
{
if (item[prop]?.toString().toLowerCase()?.startsWith(value.toLowerCase()))
arr.push(item);
if (tree.hasChildren(item)) {
arr.push(...tree.search(item.children ?? [], prop, value));
}
}
return arr;
},
getPathToItem({
current,
targetId,
parentIds = [],
}: {
current: ProjectExtendedItem[]
current: ProjectItem[]
targetId: string
parentIds?: string[]
}): string[] | undefined {
@ -275,37 +179,12 @@ const tree = {
return nested;
}
},
hasChildren(item: ProjectExtendedItem): boolean {
hasChildren(item: ProjectItem): boolean {
return (item.children ?? []).length > 0;
},
}
function add(type: FileType): void
{
if(!navigation.value)
{
return;
}
const news = [...tree.search(navigation.value, 'title', 'Nouveau')].filter((e, i, a) => a.indexOf(e) === i);
const title = `Nouveau${news.length > 0 ? ' (' + news.length +')' : ''}`;
const item: ProjectExtendedItem = { navigable: true, private: false, parent: '', path: '', title: title, name: parsePath(title), type: type, order: 0, children: type === 'folder' ? [] : undefined, customPath: false };
if(!selected.value)
{
navigation.value = [...navigation.value, item];
}
else if(selected.value?.children)
{
item.parent = getPath(selected.value);
navigation.value = tree.insertChild(navigation.value, item.parent, item);
}
else
{
navigation.value = tree.insertAfter(navigation.value, getPath(selected.value), item);
}
}
function updateTree(instruction: Instruction, itemId: string, targetId: string) : ProjectExtendedItem[] | undefined {
function updateTree(instruction: Instruction, itemId: string, targetId: string) : ProjectItem[] | undefined {
if(!navigation.value)
return;
@ -359,16 +238,14 @@ function updateTree(instruction: Instruction, itemId: string, targetId: string)
return navigation.value;
}
function transform(items: ProjectItem[] | undefined): ProjectExtendedItem[] | undefined
{
return items?.map(e => ({...e, customPath: e.name !== parsePath(e.title), children: transform(e.children)}));
}
function drop(instruction: Instruction, itemId: string, targetId: string)
{
navigation.value = updateTree(instruction, itemId, targetId) ?? navigation.value ?? [];
}
function rebuildPath(tree: ProjectExtendedItem[] | null | undefined, parentPath: string)
function rebuildPath(tree: ProjectItem[] | null | undefined, parentPath: string)
{
debugger;
if(!tree)
return;
@ -392,7 +269,7 @@ async function save(redirect: boolean): Promise<void>
type: 'success', content: 'Contenu enregistré', timer: true, duration: 10000
});
if(redirect) router.push({ name: 'explore' });
if(redirect) router.push({ name: 'explore-path', params: { path: path.value } });
} catch(e: any) {
toaster.add({
type: 'error', content: e.message, timer: true, duration: 10000
@ -400,8 +277,8 @@ async function save(redirect: boolean): Promise<void>
saveStatus.value = 'error';
}
}
function getPath(item: ProjectExtendedItem): string
function getPath(item: ProjectItem): string
{
return [item.parent, parsePath(item.customPath ? item.name : item.title)].filter(e => !!e).join('/');
return [item.parent, parsePath(item?.name ?? item.title)].filter(e => !!e).join('/');
}
</script>

View File

@ -35,7 +35,7 @@
</div>
<div class="flex flex-col gap-2 justify-start">
<ProseH3>Utilisateur</ProseH3>
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" checked disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class="text-light-60 dark:text-dark-60 line-through">Validation du compte par mail<ProseTag>prioritaire</ProseTag></span></Label>
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Validation du compte par mail<ProseTag>prioritaire</ProseTag></span></Label>
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Modification de profil</span></Label>
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Image de profil</span></Label>
</div>

View File

@ -1,18 +0,0 @@
<template>
<Head>
<Title>d[any] - Validation de votre adresse mail</Title>
</Head>
<div class="flex flex-col justify-center items-center">
<ProseH2>Votre compte a été validé ! 🎉</ProseH2>
<div class="flex flex-row gap-8">
<Button class="bg-light-25 dark:bg-dark-25"><NuxtLink :to="{ name: 'user-login', replace: true }">Se connecter</NuxtLink></Button>
<Button class="bg-light-25 dark:bg-dark-25"><NuxtLink :to="{ name: 'index', replace: true }">Retourner à l'accueil</NuxtLink></Button>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
layout: 'login'
})
</script>

View File

@ -4,26 +4,13 @@ import { hasPermissions } from "#shared/auth.util";
definePageMeta({
guestsGoesTo: '/user/login',
})
const { user, clear } = useUserSession();
const toaster = useToast();
const loading = ref<boolean>(false);
let { user, clear } = useUserSession();
async function revalidateUser()
{
loading.value = true;
await $fetch(`/api/users/${user.value?.id}/revalidate`, {
method: 'get'
});
loading.value = false;
toaster.add({ closeable: false, duration: 10000, timer: true, content: 'Un mail vous a été envoyé.', type: 'info' });
}
async function deleteUser()
{
loading.value = true;
await $fetch(`/api/users/${user.value?.id}`, {
method: 'delete'
});
loading.value = false;
clear();
}
</script>
@ -49,14 +36,14 @@ async function deleteUser()
<template v-slot:content><span>Tant que votre adresse mail n'as pas été validée, vous n'avez que
des droits de lecture.</span></template>
</HoverCard>
<Button class="ms-4" @click="revalidateUser" :loading="loading">Renvoyez un mail</Button>
<Tooltip message="En cours de développement"><Button class="ms-4" disabled>Renvoyez un mail</Button></Tooltip>
</div>
</div>
<div class="flex flex-col self-center flex-1 gap-4">
<Button @click="clear">Se deconnecter</Button>
<Button @click="async () => await clear()">Se deconnecter</Button>
<Button disabled><Tooltip message="En cours de développement">Modifier mon profil</Tooltip></Button>
<AlertDialogRoot>
<AlertDialogTrigger asChild><Button :loading="loading"
<AlertDialogTrigger asChild><Button
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
mon compte</Button></AlertDialogTrigger>
<AlertDialogPortal>

View File

@ -11,13 +11,19 @@
<TextInput type="text" label="Nom d'utilisateur" autocomplete="username" v-model="state.username" class="w-full md:w-auto"/>
<TextInput type="email" label="Email" autocomplete="email" v-model="state.email" class="w-full md:w-auto"/>
<TextInput type="password" label="Mot de passe" autocomplete="new-password" v-model="state.password" class="w-full md:w-auto"/>
<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 class="flex 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="">Votre mot de passe doit respecter les critères de sécurité suivants
:</span>
<span class="ps-4" :class="{'text-light-red dark:text-dark-red': !checkedLength}">Entre 8 et 128
caractères</span>
<span class="ps-4" :class="{'text-light-red dark:text-dark-red': !checkedLowerUpper}">Au moins
une minuscule et une majuscule</span>
<span class="ps-4" :class="{'text-light-red dark:text-dark-red': !checkedDigit}">Au moins un
chiffre</span>
<span class="ps-4" :class="{'text-light-red dark:text-dark-red': !checkedSymbol}">Au moins un
caractère spécial parmi la liste suivante:
<pre class="text-wrap">! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ ] ^ _ ` { | } ~</pre>
</span>
</div>
<TextInput type="password" label="Confirmation du mot de passe" autocomplete="new-password" v-model="confirmPassword" class="w-full md:w-auto"/>
<Button class="border border-light-35 dark:border-dark-35 max-w-48 w-full order-9 col-span-2 md:col-span-1 m-auto" :loading="status === 'pending'">S'inscrire</Button>
@ -46,8 +52,7 @@ const { add: addToast, clear: clearToasts } = useToast();
const confirmPassword = ref("");
const checkedLength = computed(() => state.password.length >= 8 && state.password.length <= 128);
const checkedLower = computed(() => state.password.toUpperCase() !== state.password);
const checkedUpper = computed(() => state.password.toLowerCase() !== state.password);
const checkedLowerUpper = computed(() => state.password.toLowerCase() !== state.password && state.password.toUpperCase() !== state.password);
const checkedDigit = computed(() => /[0-9]/.test(state.password));
const checkedSymbol = computed(() => " !\"#$%&'()*+,-./:;<=>?@[]^_`{|}~".split("").some(e => state.password.includes(e)));

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@ -1,16 +1,14 @@
import { z } from "zod";
export const fileType = z.enum(['folder', 'file', 'markdown', 'canvas']);
export const schema = z.object({
path: z.string(),
owner: z.number().finite(),
title: z.string(),
type: fileType,
type: z.enum(['folder', 'file', 'markdown', 'canvas']),
content: z.string(),
navigable: z.boolean(),
private: z.boolean(),
order: z.number().finite(),
});
export type FileType = z.infer<typeof fileType>;
export type File = z.infer<typeof schema>;

View File

@ -1,11 +1,10 @@
import { z } from "zod";
import { fileType } from "./file";
export const single = z.object({
path: z.string(),
owner: z.number().finite(),
title: z.string(),
type: fileType,
type: z.enum(['folder', 'file', 'markdown', 'canvas']),
navigable: z.boolean(),
private: z.boolean(),
order: z.number().finite(),

View File

@ -1,12 +1,11 @@
import { z } from "zod";
import { fileType } from "./file";
const baseItem = z.object({
path: z.string(),
parent: z.string(),
name: z.string(),
name: z.string().optional(),
title: z.string(),
type: fileType,
type: z.enum(['folder', 'file', 'markdown', 'canvas']),
navigable: z.boolean(),
private: z.boolean(),
order: z.number().finite(),

View File

@ -1,13 +1,5 @@
import { hasPermissions } from "#shared/auth.util";
declare module 'nitropack'
{
interface TaskPayload
{
type: string
}
}
export default defineEventHandler(async (e) => {
const session = await getUserSession(e);
@ -17,31 +9,19 @@ export default defineEventHandler(async (e) => {
return;
}
const id = getRouterParam(e, 'id');
const payload: Record<string, any> = await readBody(e);
if(!id)
{
setResponseStatus(e, 400);
return;
}
payload.type = id;
payload.data = JSON.parse(payload.data);
const result = await runTask(id, {
payload: payload
});
const result = await runTask(id);
if(!result.result)
{
setResponseStatus(e, 500);
if(result.error && (result.error as Error).message)
throw result.error;
else if(result.error)
throw new Error(result.error);
else
throw new Error('Erreur inconnue');
throw result.error ?? new Error('Erreur inconnue');
}
return;
return
});

View File

@ -73,19 +73,6 @@ export default defineEventHandler(async (e): Promise<Return> => {
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);
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 };
}

View File

@ -15,11 +15,7 @@ export default defineEventHandler(async (e) => {
navigable: explorerContentTable.navigable,
private: explorerContentTable.private,
order: explorerContentTable.order,
}).from(explorerContentTable).all();
content.sort((a, b) => {
return a.path.split('/').length - b.path.split('/').length;
});
}).from(explorerContentTable).prepare().all();
if(content.length > 0)
{

View File

@ -26,10 +26,6 @@ export default defineEventHandler(async (e) => {
order: explorerContentTable.order,
}).from(explorerContentTable).prepare().all();
content.sort((a, b) => {
return a.path.split('/').length - b.path.split('/').length;
});
if(content.length > 0)
{
const project: Project = {

View File

@ -3,7 +3,6 @@ import useDatabase from '~/composables/useDatabase';
import { explorerContentTable } from '~/db/schema';
import { project, type ProjectItem } from '~/schemas/project';
import { parsePath } from "#shared/general.utils";
import { eq, getTableColumns } from "drizzle-orm";
export default defineEventHandler(async (e) => {
const { user } = await getUserSession(e);
@ -23,15 +22,6 @@ export default defineEventHandler(async (e) => {
const items = buildOrder(body.data.items);
const db = useDatabase();
const { content, ...cols } = getTableColumns(explorerContentTable);
const full = db.select(cols).from(explorerContentTable).all();
for(let i = full.length - 1; i >= 0; i--)
{
if(items.find(e => (e.path === '' ? [e.parent, parsePath(e.name === '' ? e.title : e.name)].filter(e => !!e).join('/') : e.path) === full[i].path))
full.splice(i, 1);
}
db.transaction((tx) => {
for(let i = 0; i < items.length; i++)
{
@ -45,10 +35,10 @@ export default defineEventHandler(async (e) => {
navigable: item.navigable,
private: item.private,
order: item.order,
content: null,
content: Buffer.from('', 'utf-8'),
}).onConflictDoUpdate({
set: {
path: [item.parent, parsePath(item.name === '' ? item.title : item.name)].filter(e => !!e).join('/'),
path: [item.parent, parsePath(item?.name ?? item.title)].filter(e => !!e).join('/'),
title: item.title,
type: item.type,
navigable: item.navigable,
@ -58,10 +48,6 @@ export default defineEventHandler(async (e) => {
target: explorerContentTable.path,
}).run();
}
for(let i = 0; i < full.length; i++)
{
tx.delete(explorerContentTable).where(eq(explorerContentTable.path, full[i].path)).run();
}
});
});

View File

@ -1,64 +0,0 @@
import { eq } from "drizzle-orm";
import useDatabase from "~/composables/useDatabase";
import { usersTable } from "~/db/schema";
export default defineEventHandler(async (e) => {
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 db = useDatabase();
const data = db.select({ state: usersTable.state }).from(usersTable).where(eq(usersTable.id, session.user.id)).get();
if(!data)
{
return createError({
statusCode: 401,
message: 'Unauthorized',
});
}
if(data.state === 1)
{
setResponseStatus(e, 200);
return;
}
runTask('mail', {
payload: {
type: 'mail',
to: [session.user.email],
template: 'registration', //@TODO
data: {
username: session.user.username,
timestamp: Date.now(),
id: session.user.id,
}
}
});
setResponseStatus(e, 200);
return;
})

View File

@ -1,16 +0,0 @@
<template>
<div style='margin-left: auto; margin-right: auto; width: 75%; font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; font-size: 1rem; line-height: 1.5rem; color: #171717;'>
<div style="margin-left: auto; margin-right: auto; text-align: center;">
<a href="https://obsidian.peaceultime.com">
<img src="https://obsidian.peaceultime.com/logo.light.png" alt="Logo" title="d[any] logo" width="64" height="64" style="display: block; height: 4rem; width: 4rem; margin-left: auto; margin-right: auto;" />
<span style="margin-inline-end: 1rem; font-size: 1.5rem; line-height: 2rem; font-weight: 700; font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;">d[any]</span>
</a>
</div>
<div style="padding: 1rem;">
<slot></slot>
</div>
</div>
<div style="background-color: #171717;">
<p style="padding-top: 1rem; padding-bottom: 1rem; text-align: center; font-size: 0.75rem; line-height: 1rem; color: #fff;">Copyright Peaceultime - d[any] - 2024</p>
</div>
</template>

View File

@ -1,26 +0,0 @@
<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;">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>
<div style="padding-top: 1rem; padding-bottom: 1rem; text-align: center;">
<a :href="`https://obsidian.peaceultime.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>
<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 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://obsidian.peaceultime.com/user/mailvalidation?u=${id}&t=${timestamp}&h=${hash}` }}</pre>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import Bun from 'bun';
const { id, username, timestamp } = defineProps<{
id: number
username: string
timestamp: number
}>();
const hash = computed(() => Bun.hash(id.toString(), timestamp));
</script>

View File

@ -1,20 +0,0 @@
<template>
<div class="">
<img />
<p>Bienvenue sur d[any], <span>{{ username }}</span>.</p>
</div>
<p class="">Nous vous invitons à valider votre compte en cliquant <a :href="`https://obsidian.peaceultime.com/user/mail-validation?u=${id}&t=${timestamp}&h=${hash(id.toString(), timestamp)}`"><Button>ici</Button></a> afin de profiter de toutes les fonctionnalités de d[any]</p>
<p class="">Vous pouvez egalement copier le lien suivant pour valider votre compte: {{ `https://obsidian.peaceultime.com/user/mail-validation?u=${id}&t=${timestamp}&h=${hash(id.toString(), timestamp)}` }}</p>
<span>Ce lien est valable 1 heure.</span>
<footer></footer>
</template>
<script setup lang="ts">
import { hash } from 'bun';
const { id, username, timestamp } = defineProps<{
id: number
username: string
timestamp: number
}>();
</script>

View File

@ -1,54 +0,0 @@
import { eq } from "drizzle-orm";
import { z } from "zod";
import useDatabase from "~/composables/useDatabase";
import { usersTable } from "~/db/schema";
const schema = z.object({
h: z.coerce.string(),
u: z.coerce.number(),
t: z.coerce.number(),
});
export default defineEventHandler(async (e) => {
const query = await getValidatedQuery(e, schema.safeParse);
if(!query.success)
throw query.error;
if(Bun.hash(query.data.u.toString(), query.data.t).toString() !== query.data.h)
{
return createError({
statusCode: 400,
message: 'Lien incorrect',
})
}
if(Date.now() > query.data.t + (60 * 60 * 1000))
{
return createError({
statusCode: 400,
message: 'Le lien a expiré',
})
}
const db = useDatabase();
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',
})
}
if(result?.state === 1)
{
return createError({
statusCode: 400,
message: 'Votre compte a déjà été validé',
})
}
db.update(usersTable).set({ state: 1 }).where(eq(usersTable.id, query.data.u)).run();
sendRedirect(e, '/user/mailvalidated');
})

View File

@ -1,113 +0,0 @@
import nodemailer from 'nodemailer';
import { createSSRApp, h } from 'vue';
import { renderToString } from 'vue/server-renderer';
import postcss from 'postcss';
import tailwindcss from 'tailwindcss';
import { join } from 'node:path';
import base from '../components/mail/base.vue';
import registration from '../components/mail/registration.vue';
import revalidation from '../components/mail/revalidation.vue';
const config = useRuntimeConfig();
const [domain, selector, dkim] = config.mail.dkim.split(":");
export const templates: Record<string, { component: any, subject: string }> = {
"registration": { component: registration, subject: 'Bienvenue sur d[any] 😎' },
"revalidate-mail": { component: revalidation, subject: 'd[any]: Valider votre email' },
};
import 'nitropack/types';
import type Mail from 'nodemailer/lib/mailer';
declare module 'nitropack/types'
{
interface TaskPayload
{
type: 'mail'
to: string[]
template: string
data: Record<string, any>
}
}
const transport = nodemailer.createTransport({
//@ts-ignore
pool: true,
host: config.mail.host,
port: config.mail.port,
secure: false,
auth: {
user: config.mail.user,
pass: config.mail.passwd,
},
requireTLS: true,
dkim: {
domainName: domain,
keySelector: selector,
privateKey: dkim,
},
});
export default defineTask({
meta: {
name: 'mail',
description: 'Send email',
},
async run(e) {
try {
if(e.payload.type !== 'mail')
{
throw new Error(`Données inconnues`);
}
const payload = e.payload;
const template = templates[payload.template];
if(!template)
{
throw new Error(`Modèle de mail ${payload.template} inconnu`);
}
console.time('Generating HTML');
const mail: Mail.Options = {
from: 'd[any] - Ne pas répondre <no-reply@peaceultime.com>',
to: payload.to,
html: await render(template.component, payload.data),
subject: template.subject,
textEncoding: 'quoted-printable',
};
console.timeEnd('Generating HTML');
if(mail.html === '')
return { result: false, error: new Error("Invalid content") };
console.time('Sending Mail');
const status = await transport.sendMail(mail);
console.timeEnd('Sending Mail');
if(status.rejected.length > 0)
{
return { result: false, error: status.response, details: status.rejectedErrors };
}
return { result: true };
}
catch(e)
{
return { result: false, error: e };
}
},
})
async function render(component: any, data: Record<string, any>): Promise<string>
{
const app = createSSRApp({
render(){
return h(base, null, { default: () => h(component, data, { default: () => null }) });
}
});
const html = await renderToString(app);
return (`<html><body><div>${html}</div></body></html>`);
}

View File

@ -1,8 +1,7 @@
import useDatabase from "~/composables/useDatabase";
import { extname, basename } from 'node:path';
import type { File, FileType, Tag } from '~/types/api';
import type { CanvasColor, CanvasContent } from "~/types/canvas";
import type { FileType } from '~/types/api';
import { explorerContentTable } from "~/db/schema";
import { eq, ne } from "drizzle-orm";
const typeMapping: Record<string, FileType> = {
".md": "markdown",
@ -27,88 +26,14 @@ export default defineTask({
}
}) as any;
const files: typeof explorerContentTable.$inferInsert = await Promise.all(tree.tree.filter((e: any) => !e.path.startsWith(".")).map(async (e: any) => {
if(e.type === 'tree')
{
const title = basename(e.path);
const order = /(\d+)\. ?(.+)/gsmi.exec(title);
const path = (e.path as string).split('/').map(f => { const check = /(\d+)\. ?(.+)/gsmi.exec(f); return check && check[2] ? check[2] : f }).join('/');
return {
path: path.toLowerCase().replaceAll(" ", "-").normalize("NFD").replace(/[\u0300-\u036f]/g, ""),
order: order && order[1] ? order[1] : 0,
title: order && order[2] ? order[2] : title,
type: 'folder',
content: null,
owner: '1',
navigable: true,
private: e.path.startsWith('98.Privé'),
}
}
const extension = extname(e.path);
const title = basename(e.path, extension);
const order = /(\d+)\. ?(.+)/gsmi.exec(title);
const path = (e.path as string).split('/').map(f => { const check = /(\d+)\. ?(.+)/gsmi.exec(f); return check && check[2] ? check[2] : f }).join('/');
const content = (await $fetch(`https://git.peaceultime.com/api/v1/repos/peaceultime/system-aspect/raw/${encodeURIComponent(e.path)}`));
return {
path: (extension === '.md' ? path.replace(extension, '') : path).toLowerCase().replaceAll(" ", "-").normalize("NFD").replace(/[\u0300-\u036f]/g, ""),
order: order && order[1] ? order[1] : 0,
title: order && order[2] ? order[2] : title,
type: (typeMapping[extension] ?? 'file'),
content: reshapeContent(content as string, typeMapping[extension] ?? 'File'),
owner: '1',
navigable: true,
private: e.path.startsWith('98.Privé')
}
}));
const db = useDatabase();
db.delete(explorerContentTable).run();
db.insert(explorerContentTable).values(files).run();
useStorage('cache').clear();
const files = db.select().from(explorerContentTable).where(ne(explorerContentTable.type, 'folder')).all();
return { result: true };
}
catch(e)
{
console.error(e);
return { result: false, error: e };
}
},
})
function reshapeContent(content: string, type: FileType): string | null
{
switch(type)
{
case "markdown":
case "file":
return content;
case "canvas":
const data = JSON.parse(content) as CanvasContent;
data.edges.forEach(e => e.color = typeof e.color === 'string' ? getColor(e.color) : undefined);
data.nodes.forEach(e => e.color = typeof e.color === 'string' ? getColor(e.color) : undefined);
return JSON.stringify(data);
default:
case 'folder':
return null;
}
}
function getColor(color: string): CanvasColor
{
const colors: Record<string, string> = {
'1': 'red',
'2': 'orange',
'3': 'yellow',
'4': 'green',
'5': 'cyan',
'6': 'purple',
};
if(colors.hasOwnProperty(color))
return { class: colors[color] };
else
return { hex: color };
}
})

View File

@ -1,7 +1,8 @@
import useDatabase from "~/composables/useDatabase";
import type { FileType } from '~/types/api';
import { extname, basename } from 'node:path';
import type { File, FileType, Tag } from '~/types/api';
import type { CanvasColor, CanvasContent } from "~/types/canvas";
import { explorerContentTable } from "~/db/schema";
import { eq, ne } from "drizzle-orm";
const typeMapping: Record<string, FileType> = {
".md": "markdown",
@ -26,8 +27,47 @@ export default defineTask({
}
}) as any;
const files: typeof explorerContentTable.$inferInsert = await Promise.all(tree.tree.filter((e: any) => !e.path.startsWith(".")).map(async (e: any) => {
if(e.type === 'tree')
{
const title = basename(e.path);
const order = /(\d+)\. ?(.+)/gsmi.exec(title);
const path = (e.path as string).split('/').map(f => { const check = /(\d+)\. ?(.+)/gsmi.exec(f); return check && check[2] ? check[2] : f }).join('/');
return {
path: path.toLowerCase().replaceAll(" ", "-").normalize("NFD").replace(/[\u0300-\u036f]/g, ""),
//order: order && order[1] ? order[1] : 50,
title: order && order[2] ? order[2] : title,
type: 'folder',
content: null,
owner: '1',
navigable: true,
private: e.path.startsWith('98.Privé'),
}
}
const extension = extname(e.path);
const title = basename(e.path, extension);
const order = /(\d+)\. ?(.+)/gsmi.exec(title);
const path = (e.path as string).split('/').map(f => { const check = /(\d+)\. ?(.+)/gsmi.exec(f); return check && check[2] ? check[2] : f }).join('/');
const content = (await $fetch(`https://git.peaceultime.com/api/v1/repos/peaceultime/system-aspect/raw/${encodeURIComponent(e.path)}`));
return {
path: (extension === '.md' ? path.replace(extension, '') : path).toLowerCase().replaceAll(" ", "-").normalize("NFD").replace(/[\u0300-\u036f]/g, ""),
//order: order && order[1] ? order[1] : 50,
title: order && order[2] ? order[2] : title,
type: (typeMapping[extension] ?? 'file'),
content: reshapeContent(content as string, typeMapping[extension] ?? 'File'),
owner: '1',
navigable: true,
private: e.path.startsWith('98.Privé')
}
}));
const db = useDatabase();
const files = db.select().from(explorerContentTable).where(ne(explorerContentTable.type, 'folder')).all();
db.delete(explorerContentTable).run();
db.insert(explorerContentTable).values(files).run();
useStorage('cache').clear();
return { result: true };
}
@ -36,4 +76,37 @@ export default defineTask({
return { result: false, error: e };
}
},
})
})
function reshapeContent(content: string, type: FileType): string | null
{
switch(type)
{
case "markdown":
case "file":
return content;
case "canvas":
const data = JSON.parse(content) as CanvasContent;
data.edges.forEach(e => e.color = typeof e.color === 'string' ? getColor(e.color) : undefined);
data.nodes.forEach(e => e.color = typeof e.color === 'string' ? getColor(e.color) : undefined);
return JSON.stringify(data);
default:
case 'folder':
return null;
}
}
function getColor(color: string): CanvasColor
{
const colors: Record<string, string> = {
'1': 'red',
'2': 'orange',
'3': 'yellow',
'4': 'green',
'5': 'cyan',
'6': 'purple',
};
if(colors.hasOwnProperty(color))
return { class: colors[color] };
else
return { hex: color };
}

View File

@ -1,12 +1,10 @@
import type { FileType } from "~/schemas/file";
export function unifySlug(slug: string | string[]): string
{
return (Array.isArray(slug) ? slug.join('/') : slug);
}
export function parsePath(path: string): string
{
return path.toLowerCase().replaceAll(" ", "-").normalize("NFD").replaceAll(/[\u0300-\u036f]/g, "").replaceAll('(', '').replaceAll(')', '');
return path.toLowerCase().replaceAll(" ", "-").normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}
export function parseId(id: string | undefined): string |undefined
{
@ -45,11 +43,4 @@ export function clamp(x: number, min: number, max: number): number {
if (x < min)
return min;
return x;
}
export const iconByType: Record<FileType, string> = {
'folder': 'lucide:folder',
'canvas': 'ph:graph-light',
'file': 'radix-icons:file',
'markdown': 'radix-icons:file',
}