Markdown editor in progress + Login and session process completed
This commit is contained in:
parent
aba56bb034
commit
2e92c389a2
6
app.vue
6
app.vue
|
|
@ -13,7 +13,7 @@ const { id: project, home, get } = useProject();
|
|||
|
||||
if(project.value !== 0 && home.value === null)
|
||||
{
|
||||
const { data: useless } = await useAsyncData(`project:get:${project}`, get);
|
||||
await useAsyncData(`project:get:${project}`, get);
|
||||
}
|
||||
|
||||
const toggled = ref(false);
|
||||
|
|
@ -50,8 +50,8 @@ onMounted(() => {
|
|||
<NuxtLink class="arrow-group-item" aria-label="Projets" :to="{ path: '/explorer', force: true }">Liste des projets</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<NuxtLink class="site-nav-bar-text mobile-hidden" aria-label="Outils" :to="{ path: '/tools', force: true }"
|
||||
:class="{'mod-active': $route.path.startsWith('/tools')}">Outils</NuxtLink>
|
||||
<NuxtLink class="site-nav-bar-text mobile-hidden" aria-label="Editeur" :to="{ path: '/editing', force: true }"
|
||||
:class="{'mod-active': $route.path.startsWith('/editing')}">Editeur</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mobile-bigger">
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ html.light-mode .light-block {
|
|||
padding: 4px 1em;
|
||||
}
|
||||
|
||||
.input-group .input-error {
|
||||
.input-error {
|
||||
padding: .5em 1em 4px;
|
||||
color: var(--text-error);
|
||||
user-select: text;
|
||||
|
|
|
|||
|
|
@ -342,7 +342,7 @@
|
|||
--inline-title-margin-bottom: 0.5em;
|
||||
/* Inputs */
|
||||
--input-height: 30px;
|
||||
--input-radius: 5px;
|
||||
--input-radius: 0px;
|
||||
--input-font-weight: var(--font-normal);
|
||||
--input-border-width: 1px;
|
||||
/* Italic */
|
||||
|
|
|
|||
|
|
@ -0,0 +1,105 @@
|
|||
<style>
|
||||
.editor
|
||||
{
|
||||
white-space: pre-line;
|
||||
overflow: auto;
|
||||
outline: none;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="editor" contenteditable>
|
||||
<template
|
||||
v-if="model && model.length > 0">
|
||||
<MarkdownRenderer
|
||||
v-if="node"
|
||||
:key="key"
|
||||
:node="node"
|
||||
:proses="{
|
||||
'a': LiveA,
|
||||
'h1': LiveH1,
|
||||
'h2': LiveH2,
|
||||
'h3': LiveH3,
|
||||
'h4': LiveH4,
|
||||
'h5': LiveH5,
|
||||
'h6': LiveH6,
|
||||
'blockquote': LiveBlockquote,
|
||||
}"
|
||||
></MarkdownRenderer>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LiveA from "~/components/prose/live/LiveA.vue";
|
||||
import LiveH1 from "~/components/prose/live/LiveH1.vue";
|
||||
import LiveH2 from "~/components/prose/live/LiveH2.vue";
|
||||
import LiveH3 from "~/components/prose/live/LiveH3.vue";
|
||||
import LiveH4 from "~/components/prose/live/LiveH4.vue";
|
||||
import LiveH5 from "~/components/prose/live/LiveH5.vue";
|
||||
import LiveH6 from "~/components/prose/live/LiveH6.vue";
|
||||
import LiveBlockquote from "~/components/prose/ProseBlockquote.vue";
|
||||
|
||||
import { hash } from 'ohash'
|
||||
import { watch, computed } from 'vue'
|
||||
import type { Root } from 'hast';
|
||||
import { diffLines as diff } from 'diff';
|
||||
|
||||
const model = defineModel<string>();
|
||||
|
||||
const parser = useMarkdown();
|
||||
const key = computed(() => hash(model.value));
|
||||
|
||||
const node = ref<Root>();
|
||||
|
||||
watch(model, async (value, old) => {
|
||||
if(value && old)
|
||||
{
|
||||
if(node.value)
|
||||
{
|
||||
let content = "", line = 0, pos = -1, len = 0, child;
|
||||
const d = diff(old, value);
|
||||
const children = node.value?.children.filter(e => e.hasOwnProperty('position'));
|
||||
|
||||
for(let i = 0; i < d.length; i++)
|
||||
{
|
||||
if(d[i].added) //Nouvelle ligne
|
||||
{
|
||||
const next = d.length > i ? d[i + 1] : undefined;
|
||||
if(pos === -1 && (!next || !next.removed)) //Nouvelle ligne
|
||||
{
|
||||
child = children.filter(e => e.position?.start.line <= line && e.position?.end.line >= line); //Je cherche tout les blocs qui était inclus dans les lignes éditées.
|
||||
if(child.length > 0)
|
||||
{
|
||||
pos = child[0].position?.start.offset ?? 0; //Je pars du premier caractère du bloc
|
||||
len += (child[child.length - 1].position?.end.offset ?? 0) + 1; //Je m'arrete au dernier caractère du bloc + le \n
|
||||
}
|
||||
}
|
||||
len += d[i].value.length; // J'ajoute le nouveau nombre de caractère
|
||||
}
|
||||
else if(d[i].removed) //Ancienne ligne
|
||||
{
|
||||
child = children.filter(e => e.position?.start.line <= line + 1 && e.position?.end.line >= line + (d[i].count ?? 1)); //Je cherche tout les blocs qui était inclus dans les lignes éditées.
|
||||
if(child.length > 0)
|
||||
{
|
||||
pos = child[0].position?.start.offset ?? 0; //Je pars du premier caractère du bloc
|
||||
len += child[child.length - 1].position?.end.offset ?? 0 + 1; //Je m'arrete au dernier caractère du bloc
|
||||
len -= d[i].value.length; //Je supprime l'ancien nombre de caractère
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
line += d[i].count ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
node.value = parser(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
node.value = parser(value);
|
||||
}
|
||||
}
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
|
|
@ -8,6 +8,8 @@ const props = defineProps<Prop>();
|
|||
const model = defineModel<string>();
|
||||
|
||||
const err = ref<string | boolean | undefined>(props.error);
|
||||
|
||||
watchEffect(() => err.value = props.error);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -13,15 +13,17 @@ const { data: navigation, execute, status, error } = await useFetch(() => `/api/
|
|||
immediate: false,
|
||||
});
|
||||
|
||||
if(route.params.projectId && project.value !== 0)
|
||||
{
|
||||
showing.value = true;
|
||||
execute();
|
||||
}
|
||||
else
|
||||
{
|
||||
showing.value = false;
|
||||
}
|
||||
watch(route, () => {
|
||||
if(route.params.projectId && project.value !== 0)
|
||||
{
|
||||
showing.value = true;
|
||||
execute();
|
||||
}
|
||||
else
|
||||
{
|
||||
showing.value = false;
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -1,89 +1,26 @@
|
|||
<template>
|
||||
<slot
|
||||
:data="data?.data"
|
||||
:body="data?.body"
|
||||
:toc="data?.toc"
|
||||
:excerpt="data?.excerpt"
|
||||
:error="error"
|
||||
>
|
||||
<MDCRenderer
|
||||
v-if="body"
|
||||
:tag="tag"
|
||||
:class="props.class"
|
||||
:body="body"
|
||||
:data="data?.data"
|
||||
:unwrap="props.unwrap"
|
||||
:components="{
|
||||
a: ProseA,
|
||||
h1: ProseH1,
|
||||
h2: ProseH2,
|
||||
h3: ProseH3,
|
||||
h4: ProseH4,
|
||||
h5: ProseH5,
|
||||
h6: ProseH6,
|
||||
blockquote: ProseBlockquote,
|
||||
}"
|
||||
/>
|
||||
</slot>
|
||||
<template
|
||||
v-if="model && model.length > 0">
|
||||
<MarkdownRenderer :key="key" v-if="node" :node="node"></MarkdownRenderer>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ProseA from "~/components/content/prose/ProseA.vue";
|
||||
import ProseH1 from "~/components/content/prose/ProseH1.vue";
|
||||
import ProseH2 from "~/components/content/prose/ProseH2.vue";
|
||||
import ProseH3 from "~/components/content/prose/ProseH3.vue";
|
||||
import ProseH4 from "~/components/content/prose/ProseH4.vue";
|
||||
import ProseH5 from "~/components/content/prose/ProseH5.vue";
|
||||
import ProseH6 from "~/components/content/prose/ProseH6.vue";
|
||||
import ProseBlockquote from "~/components/content/prose/ProseBlockquote.vue";
|
||||
|
||||
import { hash } from 'ohash'
|
||||
import { useAsyncData } from 'nuxt/app'
|
||||
import { watch, computed } from 'vue'
|
||||
import type { Root } from 'hast';
|
||||
|
||||
const props = defineProps({
|
||||
tag: {
|
||||
type: [String, Boolean],
|
||||
default: 'div'
|
||||
},
|
||||
content: {
|
||||
type: [String, Object],
|
||||
required: true
|
||||
},
|
||||
excerpt: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
class: {
|
||||
type: [String, Array, Object],
|
||||
default: ''
|
||||
},
|
||||
unwrap: {
|
||||
type: [Boolean, String],
|
||||
default: false
|
||||
}
|
||||
})
|
||||
const model = defineModel<number>({
|
||||
default: 0,
|
||||
});
|
||||
const model = defineModel<string>();
|
||||
|
||||
const parser = useMarkdown();
|
||||
const key = computed(() => hash(props.content))
|
||||
const key = computed(() => hash(model.value));
|
||||
|
||||
const { data, refresh, error } = await useAsyncData(key.value, async () => {
|
||||
const timer = performance.now();
|
||||
if (typeof props.content !== 'string') {
|
||||
model.value = performance.now() - timer;
|
||||
return props.content
|
||||
const node = ref<Root>();
|
||||
|
||||
watch(model, async () => {
|
||||
if(model.value && model.value)
|
||||
{
|
||||
node.value = parser(model.value);
|
||||
}
|
||||
const result = await parser(props.content);
|
||||
model.value = performance.now() - timer;
|
||||
return result;
|
||||
})
|
||||
|
||||
const body = computed(() => props.excerpt ? data.value?.excerpt : data.value?.body)
|
||||
|
||||
watch(() => props.content, () => {
|
||||
refresh()
|
||||
})
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
<script lang="ts">
|
||||
import type { Node, Text, Element, Comment, Root } from 'hast';
|
||||
import { Text as HText, Comment as HComment } from 'vue';
|
||||
|
||||
import ProseP from '~/components/prose/ProseP.vue';
|
||||
import ProseA from '~/components/prose/ProseA.vue';
|
||||
import ProseBlockquote from '~/components/prose/ProseBlockquote.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 ProseStrong from '~/components/prose/ProseStrong.vue';
|
||||
import ProseTable from '~/components/prose/ProseTable.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,
|
||||
"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,
|
||||
"strong": ProseStrong,
|
||||
"table": ProseTable,
|
||||
"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: () => ({})
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
node: {
|
||||
handler: function(val, old) {
|
||||
|
||||
},
|
||||
deep: true,
|
||||
}
|
||||
},
|
||||
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;
|
||||
|
||||
const div = h('div', null, (node as Root).children.map(e => renderNode(e, tags)).filter(e => !!e));
|
||||
return div;
|
||||
}
|
||||
});
|
||||
|
||||
function renderNode(node: Node, tags: Record<string, any>): VNode | undefined
|
||||
{
|
||||
if(node.type === 'text')
|
||||
{
|
||||
const text = node as Text;
|
||||
if(text.value.length > 0 && text.value !== '\n')
|
||||
return h(HText, (node as Text).value);
|
||||
}
|
||||
else if(node.type === 'comment')
|
||||
{
|
||||
const comment = node as Comment;
|
||||
if(comment.value.length > 0 && comment.value !== '\n')
|
||||
return h(HComment, (node as Comment).value);
|
||||
}
|
||||
else if(node.type === 'element')
|
||||
{
|
||||
const element = node as Element;
|
||||
|
||||
return h(tags[element.tagName], { ...element.properties, class: element.properties.className }, element.children.map(e => renderNode(e, tags)).filter(e => !!e));
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
</script>
|
||||
|
|
@ -43,7 +43,7 @@ if(props.node.color !== undefined)
|
|||
<div v-if="node.text?.length > 0" class="markdown-embed-content node-insert-event" style="">
|
||||
<div class="markdown-preview-view markdown-rendered node-insert-event show-indentation-guide allow-fold-headings allow-fold-lists">
|
||||
<div class="markdown-preview-sizer markdown-preview-section">
|
||||
<Markdown :content="node.text"/>
|
||||
<Markdown v-model="node.text"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import type { Navigation } from '~/server/api/project/[projectId]/navigation.get';
|
||||
import type { Navigation } from '~/types/api';
|
||||
|
||||
interface Props {
|
||||
link: Navigation;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
<div class="markdown-preview-view markdown-rendered node-insert-event hide-title">
|
||||
<div class="markdown-preview-sizer markdown-preview-section" style="padding-bottom: 0px;">
|
||||
<h1 v-if="page[0]?.title">{{ page[0]?.title }}</h1>
|
||||
<Markdown :content="page[0].content" />
|
||||
<Markdown v-model="page[0].content" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<blockquote>
|
||||
<slot />
|
||||
</blockquote>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<code><slot /></code>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<em>
|
||||
<slot />
|
||||
</em>
|
||||
</template>
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
<template>
|
||||
<h1 :id="id"><slot></slot></h1>
|
||||
<h1 :id="id">
|
||||
<slot />
|
||||
</h1>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ id?: string }>()
|
||||
|
||||
const generate = computed(() => props.id)
|
||||
</script>
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
<template>
|
||||
<h2 :id="id"><slot></slot></h2>
|
||||
<h2 :id="id">
|
||||
<slot />
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ id?: string }>()
|
||||
|
||||
const generate = computed(() => props.id)
|
||||
</script>
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
<template>
|
||||
<h3 :id="id"><slot></slot></h3>
|
||||
<h3 :id="id">
|
||||
<slot />
|
||||
</h3>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ id?: string }>()
|
||||
|
||||
const generate = computed(() => props.id)
|
||||
</script>
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
<template>
|
||||
<h4 :id="id"><slot></slot></h4>
|
||||
<h4 :id="id">
|
||||
<slot />
|
||||
</h4>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ id?: string }>()
|
||||
|
||||
const generate = computed(() => props.id)
|
||||
</script>
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
<template>
|
||||
<h5 :id="id"><slot></slot></h5>
|
||||
<h5 :id="id">
|
||||
<slot />
|
||||
</h5>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ id?: string }>()
|
||||
|
||||
const generate = computed(() => props.id)
|
||||
</script>
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
<template>
|
||||
<h6 :id="id"><slot></slot></h6>
|
||||
<h6 :id="id">
|
||||
<slot />
|
||||
</h6>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ id?: string }>()
|
||||
|
||||
const generate = computed(() => props.id)
|
||||
</script>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<hr>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<template>
|
||||
<img
|
||||
:src="refinedSrc"
|
||||
:alt="alt"
|
||||
:width="width"
|
||||
:height="height"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { withTrailingSlash, withLeadingSlash, joinURL } from 'ufo'
|
||||
import { useRuntimeConfig, computed, resolveComponent } from '#imports'
|
||||
|
||||
const props = defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
alt: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: undefined
|
||||
},
|
||||
height: {
|
||||
type: [String, Number],
|
||||
default: undefined
|
||||
}
|
||||
})
|
||||
|
||||
const refinedSrc = computed(() => {
|
||||
if (props.src?.startsWith('/') && !props.src.startsWith('//')) {
|
||||
const _base = withLeadingSlash(withTrailingSlash(useRuntimeConfig().app.baseURL))
|
||||
if (_base !== '/' && !props.src.startsWith(_base)) {
|
||||
return joinURL(_base, props.src)
|
||||
}
|
||||
}
|
||||
return props.src
|
||||
})
|
||||
</script>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<li><slot /></li>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<ol>
|
||||
<slot />
|
||||
</ol>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<p><slot /></p>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<template>
|
||||
<pre :class="$props.class"><slot /></pre>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
code: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
filename: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
highlights: {
|
||||
type: Array as () => number[],
|
||||
default: () => []
|
||||
},
|
||||
meta: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
class: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
pre code .line{display:block}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<template>
|
||||
<div v-if="isDev">
|
||||
Rendering the <code>script</code> element is dangerous and is disabled by default. Consider implementing your own <code>ProseScript</code> element to have control over script rendering.
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
const isDev = import.meta.dev
|
||||
</script>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<strong>
|
||||
<slot />
|
||||
</strong>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<table>
|
||||
<slot />
|
||||
</table>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<tbody>
|
||||
<slot />
|
||||
</tbody>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<td>
|
||||
<slot />
|
||||
</td>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<th>
|
||||
<slot />
|
||||
</th>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<thead>
|
||||
<slot />
|
||||
</thead>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<tr>
|
||||
<slot />
|
||||
</tr>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<ul>
|
||||
<slot />
|
||||
</ul>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<a>
|
||||
<slot></slot>
|
||||
</a>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<template>
|
||||
<span v-if="focused">> </span>
|
||||
<blockquote ref="el" @focusin="focused = true" @focusout="focused = false">
|
||||
<slot />
|
||||
</blockquote>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const focused = ref(false);
|
||||
|
||||
watch(focused, console.log);
|
||||
</script>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<span v-show="false"># </span><h1><slot></slot></h1>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<template>
|
||||
<span v-show="focused">## </span><h2><slot></slot></h2>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
focused: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<span v-show="false">### </span><h3><slot></slot></h3>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<span v-show="false">#### </span><h4><slot></slot></h4>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<span v-show="false">##### </span><h5><slot></slot></h5>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<span v-show="false">###### </span><h6><slot></slot></h6>
|
||||
</template>
|
||||
|
|
@ -1,24 +1,23 @@
|
|||
import { createMarkdownParser } from "@nuxtjs/mdc/runtime/parser/index";
|
||||
import RemarkBreaks from "remark-breaks";
|
||||
import RemarkOfm from "remark-ofm";
|
||||
import { unified, type Processor } from "unified";
|
||||
import type { Root } from 'hast';
|
||||
import RemarkParse from "remark-parse";
|
||||
|
||||
export default function useMarkdown(): Awaited<ReturnType<typeof createMarkdownParser>>
|
||||
import RemarkRehype from 'remark-rehype';
|
||||
import RemarkOfm from 'remark-ofm';
|
||||
import RemarkGfm from 'remark-gfm';
|
||||
|
||||
export default function useMarkdown(): (md: string) => Root
|
||||
{
|
||||
let parser: Awaited<ReturnType<typeof createMarkdownParser>>
|
||||
let processor: Processor;
|
||||
|
||||
const parse = async (markdown: string) => {
|
||||
if (!parser)
|
||||
const parse = (markdown: string) => {
|
||||
if (!processor)
|
||||
{
|
||||
parser = await createMarkdownParser({
|
||||
remark: {
|
||||
plugins: {
|
||||
'remark-breaks': { instance: RemarkBreaks },
|
||||
'remark-ofm': { instance: RemarkOfm }
|
||||
}
|
||||
},
|
||||
});
|
||||
processor = unified().use([RemarkParse, RemarkGfm, RemarkOfm, RemarkRehype]);
|
||||
}
|
||||
return parser(markdown);
|
||||
|
||||
const processed = processor.runSync(processor.parse(markdown)) as Root;
|
||||
return processed;
|
||||
}
|
||||
|
||||
return parse;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
import type { UserSession, UserSessionComposable } from '~/types/auth'
|
||||
|
||||
const useSessionState = () => useState<UserSession>('nuxt-session', () => ({}))
|
||||
const useAuthReadyState = () => useState('nuxt-auth-ready', () => false)
|
||||
|
||||
/**
|
||||
* Composable to get back the user session and utils around it.
|
||||
* @see https://github.com/atinux/nuxt-auth-utils
|
||||
*/
|
||||
export function useUserSession(): UserSessionComposable {
|
||||
const sessionState = useSessionState()
|
||||
const authReadyState = useAuthReadyState()
|
||||
return {
|
||||
ready: computed(() => authReadyState.value),
|
||||
loggedIn: computed(() => Boolean(sessionState.value.user)),
|
||||
user: computed(() => sessionState.value.user || null),
|
||||
session: sessionState,
|
||||
fetch,
|
||||
clear,
|
||||
}
|
||||
}
|
||||
|
||||
async function fetch() {
|
||||
const authReadyState = useAuthReadyState()
|
||||
useSessionState().value = await useRequestFetch()('/api/auth/session', {
|
||||
headers: {
|
||||
Accept: 'text/json',
|
||||
},
|
||||
retry: false,
|
||||
}).catch(() => ({}))
|
||||
if (!authReadyState.value) {
|
||||
authReadyState.value = true
|
||||
}
|
||||
}
|
||||
|
||||
async function clear() {
|
||||
await $fetch('/api/auth/session', { method: 'DELETE' })
|
||||
useSessionState().value = {}
|
||||
useRouter().go(0);
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
const { loggedIn, ready, fetch } = useUserSession();
|
||||
const meta = to.meta;
|
||||
|
||||
if(!ready)
|
||||
await fetch();
|
||||
|
||||
if(!!meta.guestsGoesTo && !loggedIn.value)
|
||||
{
|
||||
return navigateTo(meta.guestsGoesTo);
|
||||
}
|
||||
else if(meta.requireAuth && !loggedIn.value)
|
||||
{
|
||||
return abortNavigation();
|
||||
}
|
||||
else if(!!meta.usersGoesTo && loggedIn.value)
|
||||
{
|
||||
return navigateTo(meta.usersGoesTo);
|
||||
}
|
||||
|
||||
return;
|
||||
});
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
|
||||
export default defineNuxtConfig({
|
||||
modules: ["@nuxtjs/color-mode", "@nuxtjs/mdc"],
|
||||
modules: ["@nuxtjs/color-mode", "nuxt-security"],
|
||||
css: ['~/assets/common.css', '~/assets/global.css'],
|
||||
runtimeConfig: {
|
||||
dbFile: '',
|
||||
sessionPassword: '699c46bd-9aaa-4364-ad01-510ee4fe7013'
|
||||
session: {
|
||||
password: '699c46bd-9aaa-4364-ad01-510ee4fe7013'
|
||||
}
|
||||
},
|
||||
components: [
|
||||
{
|
||||
|
|
|
|||
10
package.json
10
package.json
|
|
@ -1,17 +1,19 @@
|
|||
{
|
||||
"devDependencies": {
|
||||
"@nuxtjs/color-mode": "^3.4.2",
|
||||
"@nuxtjs/mdc": "^0.8.3",
|
||||
"@types/bun": "^1.1.6",
|
||||
"nuxt": "^3.12.4",
|
||||
"vue": "^3.4.35",
|
||||
"vue-router": "^4.4.2",
|
||||
"@types/diff": "^5.2.1",
|
||||
"hast-util-to-html": "^9.0.1",
|
||||
"nuxt": "^3.12.4",
|
||||
"nuxt-security": "^2.0.0-rc.9",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-ofm": "link:remark-ofm",
|
||||
"vue": "^3.4.37",
|
||||
"vue-router": "^4.4.3",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"diff": "^5.2.0",
|
||||
"lodash.capitalize": "^4.2.1"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +1,220 @@
|
|||
<style>
|
||||
.editor-container
|
||||
{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
.editor
|
||||
{
|
||||
width: 45%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="column">
|
||||
<textarea v-model="input"></textarea>
|
||||
<pre>{{ timing }}ms</pre>
|
||||
</div>
|
||||
<div class="column">
|
||||
<Head>
|
||||
<Title>Live Editing</Title>
|
||||
</Head>
|
||||
<div class="editor-container">
|
||||
<Suspense>
|
||||
<template #fallback>
|
||||
<div class="loading"></div>
|
||||
</template>
|
||||
<Markdown v-if="input.length > 0" :content="input" v-model="timing"/>
|
||||
<EditableMarkdown class="editor-preview" v-if="input.length > 0" v-model="input"></EditableMarkdown>
|
||||
</Suspense>
|
||||
<textarea class="editor" v-model="input"></textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const input = ref(""), timing = ref(0);
|
||||
const input = ref(`## Liste de sorts provisoire
|
||||
%% Equilibrage: Les sorts de dégâts plus cher ne doivent pas forcément proposer plus de dés de dégâts mais offrir plus d'options et avoir des dé de dégâts plus haut, pour synergiser avec les buffs de l'arbre de magie. %%
|
||||
|
||||
**Notation:**
|
||||
- Nom #element (coût, durée d'incantation, portée, prérequis d'incantation)
|
||||
> Effet
|
||||
### Rang 1
|
||||
- Trait de feu #element/feu (4 mana, tour, 12 cases, V/Ge/Gl)
|
||||
>Tire un faisceau de flamme, infligeant 2d8 dégâts de [[Les types de dégâts#Feu|feu]].
|
||||
|
||||
- Echauffement #element/feu (2 mana, tour, V/Gl)
|
||||
>Chauffe à blanc une arme ou un projectile. Jusqu'au début de votre prochain tour, les coups portés avec l'objet infligent 1d6 dégâts supplémentaire. Les dégâts de l'arme deviennent des dégâts de [[Les types de dégâts#Feu|feu]].
|
||||
|
||||
- #element/feu (mana, tour)
|
||||
>
|
||||
|
||||
- Corps ardent #element/feu(6 mana, tour, V/Ge/Gl/C)
|
||||
>Pendant 5 tours, toute personne terminant son tour à une case de vous subit 1d10 dégâts de [[Les types de dégâts#Feu|feu]].
|
||||
|
||||
- #element/feu(mana, tour)
|
||||
>
|
||||
|
||||
- Protection supérieure #element/glace (2 mana, réaction, V/Gl)
|
||||
>L'armure subit l'intégralité des dégâts sur le prochain coup.
|
||||
|
||||
- Lames de glace #element/glace (4 mana, tour, 12 cases)
|
||||
>Tire 2 projectiles infligeant 1d8 dégâts de [[Les types de dégâts#Glace|glace]]. Augmenter les dés de dégâts offre un projectile supplémentaire à la place. Chaque projectile demande un jet d'attaque séparé et peut viser une cible différente.
|
||||
|
||||
- #element/glace(mana, tour)
|
||||
>
|
||||
|
||||
- #element/glace(mana, tour)
|
||||
>
|
||||
|
||||
- #element/glace(mana, tour)
|
||||
>
|
||||
|
||||
- Chaine de foudre #element/foudre (4 mana, tour, 12 cases)
|
||||
>Frappe une cible visible puis rebondit sur jusqu'à 2 autres cibles à 1 case de la première. Inflige 1d8 dégâts de [[Les types de dégâts#Foudre|foudre]].
|
||||
|
||||
- Vitesse lumière #element/foudre (3 mana, tour)
|
||||
>Se téléporte à 6 cases tant que vous pouvez voir et courir vers la destination.
|
||||
|
||||
- Décharge de foudre #element/foudre(3 mana, tour)
|
||||
>Tire une décharge foudroyante d'énergie, infligeant 4d4[[Glossaire#Jet explosif|!]] dégâts de [[Les types de dégâts#Foudre|foudre]].
|
||||
|
||||
- Faisceau fulgurant #element/foudre(4 mana, tour)
|
||||
>Lance un faisceau électrique qui peut contourner les obstacles pour toucher une cible à couvert. Inflige 3d4[[Glossaire#Jet explosif|!]] dégâts de [[Les types de dégâts#Foudre|foudre]].
|
||||
>*Vous pouvez viser une case que vous ne voyez pas, mais le MJ ne doit pas vous informer si l'attaque pourrait toucher une cible.*
|
||||
|
||||
- #element/foudre(mana, tour)
|
||||
>
|
||||
|
||||
- #element/terre(2 mana, tour)
|
||||
>Un pilier de matière est extirpé du sol pour aller frapper la cible, qui est alors déplacée d'une case. Si la cible est propulsée contre un mur, elle subit alors 3d12 dégâts [[Les types de dégâts#Contondant|contondant]].
|
||||
|
||||
- #element/terre(3 mana, tour)
|
||||
>Propulse un projectile de matière sur la cible, infligeant 1d12 dégâts [[Les types de dégâts#Contondant|contondant]] et appliquant un [[Les effets#L'étourdissement|étourdissement]] (2/12).
|
||||
|
||||
- Bouclier tortue #element/terre(3 mana, tour)
|
||||
>Vous gagnez un bonus de 2 en blocage, mais subissez également un malus de 2 en esquive et perdez 2 cases de vitesse de course durant 1 min.
|
||||
|
||||
- #element/terre(2 mana, réaction)
|
||||
> Vous gagnez une résistance aux dégâts [[Les types de dégâts#Les dégâts physiques|physiques]] jusqu'à la fin de votre prochain tour.
|
||||
|
||||
- #element/terre(mana, tour)
|
||||
>
|
||||
|
||||
- Enchantement mineur #element/arcane(2 mana, tour, V/Gl)
|
||||
> Condense de l'énergie magique dans une arme ou un projectile. Vous faites une attaque immédiatement après avoir lancé ce sort sans dépenser d'action, infligeant 1d8 dégâts supplémentaire. Les dégâts de l'arme deviennent [[Les types de dégâts#Neutre|magique]].
|
||||
|
||||
- Rupture de force #element/arcane(3 mana, tour, V/Ge/Gl)
|
||||
> Vous condensez une puissante énergie magique qui est propulsée directement sur votre cible. Vous lancez 2d20 et prenez le plus haut résultat pour infliger des dégâts [[Les types de dégâts#Neutre|magique]]. *Avoir un #avantage aux dégâts permet de lancer un autre d20.* *Augmenter les dégâts de ce sort permet d'infliger 5 dégâts [[Les types de dégâts#Neutre|magique]] supplémentaire.*
|
||||
|
||||
- #element/arcane(mana, tour)
|
||||
>
|
||||
|
||||
- #element/arcane(mana, tour)
|
||||
>
|
||||
|
||||
- #element/arcane(mana, tour)
|
||||
>
|
||||
|
||||
- Foulée aérienne #element/air(3 mana, tour, 12 cases)
|
||||
>La vitesse de course de votre cible augmente de 2 cases pendant 1 minute. Vous gagnez également un bonus de +1 à l'esquive.
|
||||
|
||||
- Pression forcée #element/air(5 mana, tour, 18 cases)
|
||||
>Crée une imposante colonne d'air descendent de 3 cases de rayon sur 12 cases de haut. Les créatures à l'intérieur ont un malus de 1 à l'esquive. Les créatures volantes chutent de 3 cases par tour.
|
||||
|
||||
- #element/air(mana, tour)
|
||||
>
|
||||
|
||||
- #element/air(mana, tour)
|
||||
>
|
||||
|
||||
- #element/air(mana, tour)
|
||||
>
|
||||
|
||||
- Conservation #element/nature (2 mana, 1 minute)
|
||||
>Permet à jusqu'à 5 herbes ou préparations médicinales de se conserver 1 jour de plus. *Ne peux être utilisé qu'une seule fois par herbe/préparation.*
|
||||
|
||||
- #element/nature(mana, tour)
|
||||
>
|
||||
|
||||
- #element/nature(mana, tour)
|
||||
>
|
||||
|
||||
- #element/nature(mana, tour)
|
||||
>
|
||||
|
||||
- #element/nature(mana, tour)
|
||||
>
|
||||
|
||||
- Absorption radieuse #element/lumiere (3 mana, tour)
|
||||
> Absorbe la lumière d'une zone de 4 cases de rayon, la faisant apparaitre comme plus sombre. #todo
|
||||
|
||||
- #element/lumiere (mana, tour)
|
||||
>
|
||||
|
||||
- #element/lumiere (mana, tour)
|
||||
>
|
||||
|
||||
- #element/lumiere (mana, tour)
|
||||
>
|
||||
|
||||
- #element/lumiere (mana, tour)
|
||||
>
|
||||
|
||||
- #element/psy(6 mana, tour)
|
||||
>Envenime l'esprit de la cible, brouillant sa perception de la réalité et lui faisant voir des images subliminales de chaos. Applique un effet de [[Les effets#La peur|peur]] (4/12).
|
||||
### Rang 2
|
||||
- Trait de feu 2 #element/feu (5 mana, tour, 15 cases, V/Ge/Gl)
|
||||
>Tire un faisceau de flamme, infligeant 3d8 de dégâts de feu.
|
||||
|
||||
- Lames de glace 2 #element/glace (5 mana, tour, 15 cases)
|
||||
>Tire 3 projectiles à 1d8 de glace. Augmenter les dés de dégâts offre un projectile supplémentaire à la place. Chaque projectile demande un jet d'attaque séparé et peut viser une cible différente.
|
||||
|
||||
- Chaine de foudre 2 #element/foudre (5 mana, tour, 15 cases)
|
||||
>Frappe une cible visible puis rebondit sur jusqu'à 3 autres cibles à 2 cases de la première. 1d8+3 de foudre.
|
||||
|
||||
- Décharge de foudre 2 #element/foudre(3 mana, tour)
|
||||
>Tire une décharge foudroyante d'énergie, infligeant 6d4[[Glossaire#Jet explosif|!]] de dégâts de foudre.
|
||||
|
||||
- Conservation 2 #element/nature (4 mana, 1 minute)
|
||||
>Permet à jusqu'à 8 herbes ou préparations médicinales de se conserver 3 jours de plus. *Ne peux être utilisé qu'une seule fois par herbe/préparation.*
|
||||
|
||||
- Boule de feu #element/feu (8 mana, tour, 12 cases)
|
||||
>Projette une imposante boule de flamme explosant au contact d'une surface, infligeant ainsi 4d10 de feu sur 3 cases de rayon.
|
||||
|
||||
- Détonation #element/feu (4 mana, tour, 8 cases)
|
||||
>Pointe un lieu visible. Une explosion de flamme jaillit subitement, infligeant 2d10 de feu sur 2 cases de rayon.
|
||||
|
||||
- Lance de givre #element/glace(4 mana, tour)
|
||||
>Une lame de glace vient grandir le long de votre arme. Augmente votre portée d'une case. L'arme inflige des dégâts tranchants. Dure 1 min, casse après 8 coups réussis.
|
||||
|
||||
- Téléportation #element/foudre (4 mana, tour)
|
||||
>Se téléporte à un point visible à 9 cases max.
|
||||
|
||||
- Apaisement #element/psy (3 mana, tour)
|
||||
>En touchant la cible, vous pouvez faire un jet d'intelligence. Guérit l'influence, le charme et la peur, mais augmente les chances de ces effet de 1 niveau pendant 3 tours.
|
||||
|
||||
- Painshock #element/psy (6 mana, tour)
|
||||
>*Ne fonctionne que si la cible touchée à subit des dégâts depuis votre dernier tour.* Vous touchez une plaie et intensifiez la douleur à l'extrême. Applique un effet d'[[Les effets#L'étourdissement|étourdissement]]. La difficulté est égale à 2/12 + 1 niveau pour chaque 10% de vie max retiré.
|
||||
|
||||
- Perturbateur #element/psy (4 mana, réaction, 9 cases, V/Ge)
|
||||
>Lorsqu'un lanceur de sort termine son incantation, vous pouvez perturber les flux magiques pour lui imposer un malus de 3 au jet.
|
||||
### Rang 3
|
||||
- Rejet pur #divin (spécial, tour, 3 cases, Ge)
|
||||
>Vous propulsez une énergie magique pure condensée sur votre adversaire avec une puissance absolue. Vous infligez 1d6!+4 dégâts [[Les types de dégâts#Neutre|magique]] tous les 3 mana dépensé. Vous pouvez dépenser jusqu'à 30 mana. Après avoir lancé ce sort, vous subissez un malus de 4 au lancer de sort pendant 1 tour.
|
||||
### Sorts unique
|
||||
Les sorts uniques sont des sorts obtenus uniquement avec des objets magiques ou en progressant dans l'arbre d'entrainement. Il n'existe **aucun** autre moyen d'obtenir des sorts.
|
||||
|
||||
- Dévastation #element/feu + #element/glace + #element/foudre (10 mana, tour, 12 cases)
|
||||
>Inflige 10+3d10 dégâts. Vous pouvez choisir le type de dégâts entre feu, glace et foudre. Ignore les résistances et réduit les immunités en résistance. ^484fc3
|
||||
|
||||
- Soin #element/nature (8 mana, tour, toucher)
|
||||
>Soigne 10+1d10 PV et guérit l'[[Les effets#L'étourdissement|étourdissement]], le [[Les effets#Le saignement|saignement]] et les [[Les effets#L'empoisonnement|poisons]]. ^068b55
|
||||
|
||||
- Contresort #element/arcane (4 mana, réaction, 12 cases)
|
||||
>Perturbe les flux magique pour interrompre une canalisation en cours. Vous pouvez augmenter le coût du sort pour augmenter les chances de réussite. La difficulté est égale à 6 - le cout du sort à interrompre + le cout du contresort. ^a8f46f
|
||||
|
||||
- Focalisation destructrice #element/arcane (12 mana, tour)
|
||||
>Vous focalisez les énergies magiques sur vous, rendant l'utilisation de sort plus complexe pour les autres. La densité d'énergie anormale vous fait subir 5 points de dégâts par tour. Pendant une minute, toute personne à 18 cases de vous essayant de lancer un sort ou de [[1.Règles/6.Les Aspects/index#Transformations|se transformer]] subit un malus de 4. ^73b8bd
|
||||
|
||||
- Domination mentale #element/psy (10 mana, tour, toucher)
|
||||
>Applique un effet de [[Les effets#La possession|possession]] (6/12). ^5b38b6
|
||||
### Sorts spéciaux
|
||||
Les sorts spéciaux sont une liste de sorts que les joueurs peuvent obtenir durant certaines aventures. Selon les cas, un joueur peut demander au maitre du jeu de commencer avec un sort spécial si ça correspond à son passé. Les sorts spéciaux peuvent aussi être des sorts que les PNJ ont et qu'ils peuvent apprendre aux joueurs.`);
|
||||
</script>
|
||||
|
|
@ -24,7 +24,7 @@ await set(parseInt(route.params.projectId as string));
|
|||
<div class="markdown-preview-view markdown-rendered node-insert-event hide-title">
|
||||
<div class="markdown-preview-sizer markdown-preview-section" style="padding-bottom: 0px;">
|
||||
<h1>{{ content[0].title }}</h1>
|
||||
<Markdown :content="content[0].content" />
|
||||
<Markdown v-model="content[0].content" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<script lang="ts">
|
||||
<!--<script lang="ts">
|
||||
let icon: HTMLDivElement | null;
|
||||
function toggleLeftPanel(_: Event) {
|
||||
icon!.parentElement!.parentElement!.parentElement!.parentElement!.classList.toggle('is-left-column-open');
|
||||
|
|
@ -84,4 +84,6 @@ onMounted(() => {
|
|||
</div>
|
||||
</template>
|
||||
</ContentList>
|
||||
</template>
|
||||
</template>-->
|
||||
|
||||
<template></template>
|
||||
|
|
@ -18,7 +18,7 @@ Le logo a été créé grâce aux icones de [Game Icons](https://game-icons.net)
|
|||
<div class="publish-renderer">
|
||||
<div class="markdown-preview-view markdown-rendered node-insert-event hide-title">
|
||||
<div class="markdown-preview-sizer markdown-preview-section" style="padding-bottom: 0px;">
|
||||
<Markdown :content="data" />
|
||||
<Markdown v-model="data" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,46 +1,74 @@
|
|||
<script setup lang="ts">
|
||||
import { hydrate } from 'vue';
|
||||
import { ZodError } from 'zod';
|
||||
import { schema, type Login } from '~/schemas/login';
|
||||
|
||||
definePageMeta({
|
||||
auth: {
|
||||
disconnectedOnly: true,
|
||||
connectedRedirect: '/user/profile'
|
||||
}
|
||||
usersGoesTo: '/user/profile'
|
||||
});
|
||||
|
||||
const state = reactive<Login>({
|
||||
username: '',
|
||||
usernameOrEmail: '',
|
||||
password: ''
|
||||
});
|
||||
|
||||
const { status, login } = useAuth();
|
||||
const { data: result, status, error, refresh } = await useFetch('/api/auth/login', {
|
||||
body: state,
|
||||
immediate: false,
|
||||
method: 'POST',
|
||||
watch: false,
|
||||
ignoreResponseError: true,
|
||||
})
|
||||
|
||||
const usernameError = ref("");
|
||||
const passwordError = ref("");
|
||||
const generalError = ref("");
|
||||
|
||||
async function submit()
|
||||
{
|
||||
if(state.password === "")
|
||||
return;
|
||||
|
||||
const data = schema.safeParse(state);
|
||||
|
||||
if(data.success && state.password !== "")
|
||||
if(data.success)
|
||||
{
|
||||
let errors = await login(data.data.username, data.data.password);
|
||||
await refresh()
|
||||
|
||||
if(status.value === AuthStatus.connected)
|
||||
const login = result.value;
|
||||
if(!login || !login.success)
|
||||
{
|
||||
await navigateTo('/user/profile', { replace: true });
|
||||
handleErrors(login?.error ?? error.value!);
|
||||
}
|
||||
else
|
||||
else if(status.value === 'success' && login.success)
|
||||
{
|
||||
errors = errors?.issues ?? errors;
|
||||
usernameError.value = errors?.find((e: any) => e.path.includes("username"))?.message ?? "";
|
||||
passwordError.value = errors?.find((e: any) => e.path.includes("password"))?.message ?? "";
|
||||
console.log(await navigateTo('/user/profile'));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
usernameError.value = data.error?.issues.find(e => e.path.includes("username"))?.message ?? "";
|
||||
passwordError.value = data.error?.issues.find(e => e.path.includes("password"))?.message ?? "";
|
||||
handleErrors(data.error);
|
||||
}
|
||||
}
|
||||
function handleErrors(error: Error | ZodError)
|
||||
{
|
||||
if(error.hasOwnProperty('issues'))
|
||||
{
|
||||
for(const err of (error as ZodError).issues)
|
||||
{
|
||||
if(err.path.includes('username'))
|
||||
{
|
||||
usernameError.value = err.message;
|
||||
}
|
||||
if(err.path.includes('password'))
|
||||
{
|
||||
passwordError.value = err.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
generalError.value = error?.message ?? 'Erreur inconnue.';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -51,20 +79,20 @@ async function submit()
|
|||
</Head>
|
||||
<div class="site-body-center-column">
|
||||
<div class="render-container flex align-center justify-center">
|
||||
<form v-if="status === AuthStatus.disconnected" @submit.prevent="submit" class="input-form input-form-wide">
|
||||
<form @submit.prevent="submit" class="input-form input-form-wide">
|
||||
<h1>Connexion</h1>
|
||||
<Input type="text" autocomplete="username" v-model="state.username"
|
||||
<Input type="text" autocomplete="username" v-model="state.usernameOrEmail"
|
||||
placeholder="" title="Nom d'utilisateur ou adresse mail" :error="usernameError" />
|
||||
<Input type="password" autocomplete="current-password" v-model="state.password"
|
||||
placeholder="" title="Mot de passe"
|
||||
:error="passwordError" />
|
||||
<button>Se connecter</button>
|
||||
<span v-if="generalError" class="input-error">{{ generalError }}</span>
|
||||
<button>
|
||||
<div class="loading" v-if="status === 'pending'"></div>
|
||||
<template v-else>Se connecter</template>
|
||||
</button>
|
||||
<NuxtLink :to="{ path: `/user/register`, force: true }">Pas de compte ?</NuxtLink>
|
||||
</form>
|
||||
<div v-else-if="status === AuthStatus.loading" class="input-form"><div class="loading"></div></div>
|
||||
<div v-else class="not-found-container">
|
||||
<div class="not-found-title">👀 Vous n'avez rien à faire ici. 👀</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,13 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
auth: {
|
||||
disconnectedOnly: false,
|
||||
disconnectedRedirect: '/user/login'
|
||||
}
|
||||
guestsGoesTo: '/user/login'
|
||||
});
|
||||
|
||||
const { data } = useAuth();
|
||||
|
||||
const { user, clear } = useUserSession();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -18,7 +14,8 @@ const { data } = useAuth();
|
|||
<div class="render-container">
|
||||
<div class="not-found-container">
|
||||
<ThemeIcon icon="logo" :width=128 :height=128 />
|
||||
<div class="not-found-title">Work in prorgess</div>
|
||||
<div class="not-found-title">Bonjour {{ user?.username }} :)</div>
|
||||
<button @click="clear">Se deconnecter</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import { ZodError } from 'zod';
|
||||
import { schema, type Registration } from '~/schemas/registration';
|
||||
|
||||
definePageMeta({
|
||||
auth: {
|
||||
disconnectedOnly: true,
|
||||
connectedRedirect: '/user/profile'
|
||||
}
|
||||
usersGoesTo: '/user/profile'
|
||||
});
|
||||
|
||||
const state = reactive<Registration>({
|
||||
|
|
@ -16,8 +14,6 @@ const state = reactive<Registration>({
|
|||
|
||||
const confirmPassword = ref("");
|
||||
|
||||
const { status, register } = useAuth();
|
||||
|
||||
const checkedLength = computed(() => state.password.length >= 8 && state.password.length <= 128);
|
||||
const checkedLowerUpper = computed(() => state.password.toLowerCase() !== state.password && state.password.toUpperCase() !== state.password);
|
||||
const checkedDigit = computed(() => /[0-9]/.test(state.password));
|
||||
|
|
@ -25,30 +21,61 @@ const checkedSymbol = computed(() => " !\"#$%&'()*+,-./:;<=>?@[]^_`{|}~".split("
|
|||
|
||||
const usernameError = ref("");
|
||||
const emailError = ref("");
|
||||
const generalError = ref("");
|
||||
|
||||
const { data: result, status, error, execute } = await useFetch('/api/auth/register', {
|
||||
body: state,
|
||||
immediate: false,
|
||||
method: 'POST',
|
||||
watch: false,
|
||||
ignoreResponseError: true,
|
||||
})
|
||||
|
||||
async function submit()
|
||||
{
|
||||
if(state.password === "" || state.password !== confirmPassword.value)
|
||||
return;
|
||||
|
||||
const data = schema.safeParse(state);
|
||||
|
||||
if(data.success && state.password !== "" && confirmPassword.value === state.password)
|
||||
if(data.success)
|
||||
{
|
||||
let errors = await register(data.data.username, data.data.email, data.data.password, {});
|
||||
await execute()
|
||||
|
||||
if(status.value === AuthStatus.connected)
|
||||
const login = result.value;
|
||||
if(!login || !login.success)
|
||||
{
|
||||
await navigateTo('/');
|
||||
handleErrors(login?.error ?? error.value!);
|
||||
}
|
||||
else
|
||||
else if(status.value === 'success' && login.success)
|
||||
{
|
||||
errors = errors?.issues ?? errors;
|
||||
usernameError.value = errors?.find((e: any) => e.path.includes("username"))?.message ?? "";
|
||||
emailError.value = errors?.find((e: any) => e.path.includes("email"))?.message ?? "";
|
||||
console.log(await navigateTo('/user/profile'));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
usernameError.value = data.error?.issues.find(e => e.path.includes("username"))?.message ?? "";
|
||||
emailError.value = data.error?.issues.find(e => e.path.includes("email"))?.message ?? "";
|
||||
handleErrors(data.error);
|
||||
}
|
||||
}
|
||||
function handleErrors(error: Error | ZodError)
|
||||
{
|
||||
if(error.hasOwnProperty('issues'))
|
||||
{
|
||||
for(const err of (error as ZodError).issues)
|
||||
{
|
||||
if(err.path.includes('username'))
|
||||
{
|
||||
usernameError.value = err.message;
|
||||
}
|
||||
if(err.path.includes('email'))
|
||||
{
|
||||
emailError.value = err.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
generalError.value = error?.message ?? 'Erreur inconnue.';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -60,7 +87,7 @@ async function submit()
|
|||
</Head>
|
||||
<div class="site-body-center-column">
|
||||
<div class="render-container flex align-center justify-center">
|
||||
<form v-if="status === AuthStatus.disconnected" @submit.prevent="submit" class="input-form input-form-wide">
|
||||
<form @submit.prevent="submit" class="input-form input-form-wide">
|
||||
<h1>Inscription</h1>
|
||||
<Input type="text" autocomplete="username" v-model="state.username"
|
||||
placeholder="Entrez un nom d'utiliateur" title="Nom d'utilisateur" :error="usernameError" />
|
||||
|
|
@ -85,14 +112,12 @@ async function submit()
|
|||
</div>
|
||||
<Input type="password" v-model="confirmPassword" placeholder="Confirmer le mot de passe"
|
||||
title="Confirmer le mot de passe"
|
||||
autocomplete="new-password"
|
||||
:error="confirmPassword === '' || confirmPassword === state.password ? '' : 'Les mots de passe saisies ne sont pas identique'" />
|
||||
<button>S'inscrire</button>
|
||||
<span v-if="generalError" class="input-error">{{ generalError }}</span>
|
||||
<button><div v-if="status === 'pending'" class="loading"></div><template v-else>S'inscrire</template></button>
|
||||
<NuxtLink :to="{ path: `/user/login`, force: true }">Se connecter</NuxtLink>
|
||||
</form>
|
||||
<div v-else-if="status === AuthStatus.loading" class="input-form"><div class="loading"></div></div>
|
||||
<div v-else class="not-found-container">
|
||||
<div class="not-found-title">👀 Vous n'avez rien à faire ici. 👀</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
export default defineNuxtPlugin(async (nuxtApp) => {
|
||||
if (!nuxtApp.payload.serverRendered || Boolean(nuxtApp.payload.prerenderedAt) || Boolean(nuxtApp.payload.isCached)) {
|
||||
nuxtApp.hook('app:mounted', async () => {
|
||||
await useUserSession().fetch()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
export default defineNuxtPlugin({
|
||||
name: 'session-fetch-plugin',
|
||||
enforce: 'pre',
|
||||
async setup(nuxtApp) {
|
||||
// Flag if request is cached
|
||||
nuxtApp.payload.isCached = Boolean(useRequestEvent()?.context.cache)
|
||||
if (nuxtApp.payload.serverRendered && !nuxtApp.payload.prerenderedAt && !nuxtApp.payload.isCached) {
|
||||
await useUserSession().fetch()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const schema = z.object({
|
||||
username: z.string({ required_error: "Nom d'utilisateur obligatoire" }),
|
||||
usernameOrEmail: z.string({ required_error: "Nom d'utilisateur ou email obligatoire" }),
|
||||
password: z.string({ required_error: "Mot de passe obligatoire" }),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,17 @@ function securePassword(password: string, ctx: z.RefinementCtx): void {
|
|||
}
|
||||
|
||||
export const schema = z.object({
|
||||
username: z.string({ required_error: "Nom d'utilisateur obligatoire" }).min(3, "Votre nom d'utilisateur doit contenir au moins 3 caractères").max(32, "Votre nom d'utilisateur doit contenir au plus 32 caractères"),
|
||||
username: z.string({ required_error: "Nom d'utilisateur obligatoire" }).min(3, "Votre nom d'utilisateur doit contenir au moins 3 caractères").max(32, "Votre nom d'utilisateur doit contenir au plus 32 caractères").superRefine((user, ctx) => {
|
||||
const test = z.string().email().safeParse(user);
|
||||
if(test.success)
|
||||
{
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.invalid_string,
|
||||
validation: 'email',
|
||||
message: "Votre nom d'utilisateur ne peut pas être une addresse mail",
|
||||
});
|
||||
}
|
||||
}),
|
||||
email: z.string({ required_error: "Email obligatoire" }).email("Adresse mail invalide"),
|
||||
password: z.string({ required_error: "Mot de passe obligatoire" }).min(8, "Votre mot de passe doit contenir au moins 8 caractères").max(128, "Votre mot de passe doit contenir au moins 8 caractères").superRefine(securePassword),
|
||||
data: z.object({
|
||||
|
|
|
|||
|
|
@ -1,96 +1,89 @@
|
|||
import useDatabase from '~/composables/useDatabase';
|
||||
import { schema } from '~/schemas/login';
|
||||
import { User, UserExtendedData, UserRawData, UserSession, UserSessionRequired } from '~/types/auth';
|
||||
import type { Database } from "bun:sqlite";
|
||||
import { ZodError } from 'zod';
|
||||
import { checkSession, logSession } from '~/server/utils/user';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const { sessionPassword } = useRuntimeConfig();
|
||||
const session = await useSession(e, {
|
||||
password: sessionPassword,
|
||||
});
|
||||
interface SuccessHandler
|
||||
{
|
||||
success: true;
|
||||
session: UserSession;
|
||||
}
|
||||
interface ErrorHandler
|
||||
{
|
||||
success: false;
|
||||
error: Error | ZodError<{
|
||||
usernameOrEmail: string;
|
||||
password: string;
|
||||
}>;
|
||||
}
|
||||
type Return = SuccessHandler | ErrorHandler;
|
||||
|
||||
export default defineEventHandler(async (e): Promise<Return> => {
|
||||
try
|
||||
{
|
||||
const session = await getUserSession(e);
|
||||
const db = useDatabase();
|
||||
|
||||
console.log(session.id);
|
||||
const checkedSession = await checkSession(e, session);
|
||||
|
||||
if(session.id && session.data.id)
|
||||
{
|
||||
const checkSession = db.query("SELECT user_id FROM user_sessions WHERE id = ?1");
|
||||
const sessionId = checkSession.get(session.id) as any;
|
||||
if(checkedSession !== undefined)
|
||||
return checkedSession;
|
||||
|
||||
console.log(sessionId);
|
||||
|
||||
if(sessionId && sessionId.user_id === session.data.id)
|
||||
{
|
||||
return { success: true, id: session.data.id, sessionId: session.id, data: session.data };
|
||||
}
|
||||
else
|
||||
{
|
||||
session.clear();
|
||||
|
||||
setResponseStatus(e, 406);
|
||||
return { success: false, error: { path: ['global'], message: 'Vous êtes déjà connecté' } };
|
||||
}
|
||||
|
||||
}
|
||||
const body = await readValidatedBody(e, schema.safeParse);
|
||||
|
||||
if (!body.success)
|
||||
{
|
||||
session.clear();
|
||||
await clearUserSession(e);
|
||||
|
||||
setResponseStatus(e, 406);
|
||||
return { success: false, error: body.error };
|
||||
}
|
||||
|
||||
|
||||
const hash = await Bun.password.hash(body.data.password);
|
||||
const checkID = db.query(`SELECT id FROM users WHERE (username = ?1 or email = ?1)`);
|
||||
const id = checkID.get(body.data.username) as any;
|
||||
const checkID = db.query(`SELECT id, hash FROM users WHERE (username = ?1 or email = ?1)`);
|
||||
const id = checkID.get(body.data.usernameOrEmail) as { id: number, hash: string };
|
||||
|
||||
if(!id || !id.id)
|
||||
if(!id || !id.id || !id.hash)
|
||||
{
|
||||
session.clear();
|
||||
await clearUserSession(e);
|
||||
|
||||
setResponseStatus(e, 401);
|
||||
return { success: false, error: { path: ['username'], message: 'Identifiant inconnu' } };
|
||||
return { success: false, error: new ZodError([{ code: 'custom', path: ['username'], message: 'Identifiant inconnu' }]) };
|
||||
}
|
||||
|
||||
const checkHash = db.query(`SELECT COUNT(*) as count FROM users WHERE id = ?1 and hash = ?2`);
|
||||
const validation = checkHash.get(id.id, hash) as any;
|
||||
const valid = await Bun.password.verify(body.data.password, id.hash);
|
||||
|
||||
if(validation && validation.count && validation.count !== 1)
|
||||
if(!valid)
|
||||
{
|
||||
session.clear();
|
||||
await clearUserSession(e);
|
||||
|
||||
setResponseStatus(e, 401);
|
||||
return { success: false, error: { path: ['password'], message: 'Mot de passe incorrect' } };
|
||||
return { success: false, error: new ZodError([{ code: 'custom', path: ['password'], message: 'Mot de passe incorrect' }]) };
|
||||
}
|
||||
|
||||
const loggingIn = db.query(`INSERT INTO user_sessions(id, user_id, ip, agent, lastRefresh) VALUES(?1, ?2, ?3, ?4, ?5)`);
|
||||
loggingIn.get(session.id, id.id, getRequestIP(e), getRequestHeader(e, 'User-Agent'), Date.now());
|
||||
|
||||
await session.update(getData(db, id.id));
|
||||
logSession(e, await setUserSession(e, { user: getData(db, id.id) }) as UserSessionRequired);
|
||||
|
||||
setResponseStatus(e, 201);
|
||||
return { success: true, id: id.id, sessionId: session.id, data: session.data };
|
||||
return { success: true, session };
|
||||
}
|
||||
catch(e)
|
||||
catch(err: any)
|
||||
{
|
||||
session.clear();
|
||||
await clearUserSession(e);
|
||||
|
||||
console.error(e);
|
||||
return { success: false, error: e };
|
||||
console.error(err);
|
||||
return { success: false, error: err as Error };
|
||||
}
|
||||
});
|
||||
|
||||
function getData(db: Database, id: string): any
|
||||
function getData(db: Database, id: number): User
|
||||
{
|
||||
const userQuery = db.query(`SELECT * FROM users WHERE id = ?1`);
|
||||
const user = userQuery.get(id);
|
||||
const userQuery = db.query(`SELECT id, username, email, state FROM users WHERE id = ?1`);
|
||||
const user = userQuery.get(id) as UserRawData;
|
||||
|
||||
const userDataQuery = db.query(`SELECT * FROM users_data WHERE user_id = ?1`);
|
||||
const userData = userDataQuery.get(id);
|
||||
const userData = userDataQuery.get(id) as UserExtendedData;
|
||||
|
||||
return { ...user, ...userData };
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export default defineEventHandler(async (e) => {
|
||||
});
|
||||
|
|
@ -1,41 +1,67 @@
|
|||
import { ZodError, ZodIssue } from 'zod';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { schema } from '~/schemas/registration';
|
||||
import { checkSession, logSession } from '~/server/utils/user';
|
||||
import { UserSession, UserSessionRequired } from '~/types/auth';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
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 { sessionPassword } = useRuntimeConfig();
|
||||
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 db = useDatabase();
|
||||
|
||||
const usernameQuery = db.query(`SELECT COUNT(*) as count FROM users WHERE username = ?1`);
|
||||
const checkUsername = usernameQuery.get(body.data.username) as any;
|
||||
|
||||
const emailQuery = db.query(`SELECT COUNT(*) as count FROM users WHERE email = ?1`);
|
||||
const checkEmail = emailQuery.get(body.data.email) as any;
|
||||
|
||||
const errors = [];
|
||||
const errors: ZodIssue[] = [];
|
||||
if(checkUsername.count !== 0)
|
||||
errors.push({ path: ['username'], message: "Ce nom d'utilisateur est déjà utilisé" });
|
||||
errors.push({ code: 'custom', path: ['username'], message: "Ce nom d'utilisateur est déjà utilisé" });
|
||||
if(checkEmail.count !== 0)
|
||||
errors.push({ path: ['email'], message: "Cette adresse mail est déjà utilisée" });
|
||||
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: errors };
|
||||
return { success: false, error: new ZodError(errors) };
|
||||
}
|
||||
else
|
||||
{
|
||||
const hash = await Bun.password.hash(body.data.password);
|
||||
const registration = db.query(`INSERT INTO users(username, email, hash, email_valid) VALUES(?1, ?2, ?3, 0)`);
|
||||
const registration = db.query(`INSERT INTO users(username, email, hash, state) VALUES(?1, ?2, ?3, 0)`);
|
||||
registration.get(body.data.username, body.data.email, hash) as any;
|
||||
|
||||
const userIdQuery = db.query(`SELECT id FROM users WHERE username = ?1`);
|
||||
|
|
@ -44,19 +70,17 @@ export default defineEventHandler(async (e) => {
|
|||
const registeringData = db.query(`INSERT INTO users_data(user_id) VALUES(?1)`);
|
||||
registeringData.get(id);
|
||||
|
||||
const session = await useSession(e, {
|
||||
password: sessionPassword,
|
||||
});
|
||||
|
||||
const loggingIn = db.query(`INSERT INTO user_sessions(id, user_id, ip, agent, lastRefresh) VALUES(?1, ?2, ?3, ?4, ?5)`);
|
||||
loggingIn.get(session.id, id, getRequestIP(e), getRequestHeader(e, 'User-Agent'), Date.now());
|
||||
logSession(e, await setUserSession(e, { user: { id: id, username: body.data.username, email: body.data.email, state: 0 } }) as UserSessionRequired);
|
||||
|
||||
setResponseStatus(e, 201);
|
||||
return { success: true, id: id, sessionId: session.id };
|
||||
return { success: true, session };
|
||||
}
|
||||
}
|
||||
catch(e)
|
||||
catch(err: any)
|
||||
{
|
||||
return { success: false, error: e };
|
||||
await clearUserSession(e);
|
||||
|
||||
console.error(err);
|
||||
return { success: false, error: err as Error };
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { eventHandler } from 'h3';
|
||||
import { clearUserSession } from '~/server/utils/session';
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
await clearUserSession(event);
|
||||
|
||||
return { loggedOut: true };
|
||||
})
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { eventHandler } from 'h3'
|
||||
import { getUserSession, sessionHooks } from '~/server/utils/session'
|
||||
import type { UserSessionRequired } from '~/types/auth'
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const session = await getUserSession(event)
|
||||
|
||||
if (session.user) {
|
||||
await sessionHooks.callHookParallel('fetch', session as UserSessionRequired, event)
|
||||
}
|
||||
|
||||
return session
|
||||
})
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,32 @@
|
|||
import useDatabase from "~/composables/useDatabase";
|
||||
|
||||
const monthAsMs = 1000 * 60 * 60 * 24 * 30;
|
||||
|
||||
export default defineNitroPlugin(() => {
|
||||
const db = useDatabase();
|
||||
|
||||
sessionHooks.hook('fetch', async (session, event) => {
|
||||
const query = db.prepare('SELECT lastRefresh FROM user_sessions WHERE id = ?1 AND user_id = ?2');
|
||||
const result = query.get(session.id, session.user.id) as Record<string, any>;
|
||||
|
||||
if(!result)
|
||||
{
|
||||
throw createError({ statusCode: 401, message: 'Unauthorized' });
|
||||
}
|
||||
else if(result && result.lastRefresh && result.lastRefresh < Date.now() - monthAsMs)
|
||||
{
|
||||
throw createError({ statusCode: 401, message: 'Session has expired' });
|
||||
}
|
||||
else
|
||||
{
|
||||
db.prepare('UPDATE user_sessions SET lastRefresh = ?1 WHERE id = ?2 AND user_id = ?3').run(Date.now(), session.id, session.user.id);
|
||||
}
|
||||
});
|
||||
sessionHooks.hook('clear', async (session, event) => {
|
||||
if(session.id && session.user)
|
||||
{
|
||||
const query = db.prepare('DELETE FROM user_sessions WHERE id = ?1 AND user_id = ?2');
|
||||
query.run(session.id, session.user.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
import type { H3Event, SessionConfig } from 'h3'
|
||||
import { useSession, createError } from 'h3'
|
||||
import { defu } from 'defu'
|
||||
import { createHooks } from 'hookable'
|
||||
import { useRuntimeConfig } from '#imports'
|
||||
import type { UserSession, UserSessionRequired } from '~/types/auth'
|
||||
|
||||
export interface SessionHooks {
|
||||
/**
|
||||
* Called when fetching the session from the API
|
||||
* - Add extra properties to the session
|
||||
* - Throw an error if the session could not be verified (with a database for example)
|
||||
*/
|
||||
fetch: (session: UserSessionRequired, event: H3Event) => void | Promise<void>
|
||||
/**
|
||||
* Called before clearing the session
|
||||
*/
|
||||
clear: (session: UserSession, event: H3Event) => void | Promise<void>
|
||||
}
|
||||
|
||||
export const sessionHooks = createHooks<SessionHooks>()
|
||||
|
||||
/**
|
||||
* Get the user session from the current request
|
||||
* @param event The Request (h3) event
|
||||
* @returns The user session
|
||||
*/
|
||||
export async function getUserSession(event: H3Event) {
|
||||
const session = await _useSession(event);
|
||||
|
||||
if(!session.data || !session.data.id)
|
||||
{
|
||||
await session.update(defu({ id: session.id }, session.data));
|
||||
}
|
||||
|
||||
return session.data;
|
||||
}
|
||||
/**
|
||||
* Set a user session
|
||||
* @param event The Request (h3) event
|
||||
* @param data User session data, please only store public information since it can be decoded with API calls
|
||||
* @see https://github.com/atinux/nuxt-auth-utils
|
||||
*/
|
||||
export async function setUserSession(event: H3Event, data: UserSession) {
|
||||
const session = await _useSession(event)
|
||||
|
||||
await session.update(defu(data, session.data))
|
||||
|
||||
return session.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace a user session
|
||||
* @param event The Request (h3) event
|
||||
* @param data User session data, please only store public information since it can be decoded with API calls
|
||||
*/
|
||||
export async function replaceUserSession(event: H3Event, data: UserSession) {
|
||||
const session = await _useSession(event)
|
||||
|
||||
await session.clear()
|
||||
await session.update(data)
|
||||
|
||||
return session.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the user session and removing the session cookie
|
||||
* @param event The Request (h3) event
|
||||
* @returns true if the session was cleared
|
||||
*/
|
||||
export async function clearUserSession(event: H3Event) {
|
||||
const session = await _useSession(event)
|
||||
|
||||
await sessionHooks.callHookParallel('clear', session.data, event)
|
||||
await session.clear()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Require a user session, throw a 401 error if the user is not logged in
|
||||
* @param event
|
||||
* @param opts Options to customize the error message and status code
|
||||
* @param opts.statusCode The status code to use for the error (defaults to 401)
|
||||
* @param opts.message The message to use for the error (defaults to Unauthorized)
|
||||
* @see https://github.com/atinux/nuxt-auth-utils
|
||||
*/
|
||||
export async function requireUserSession(event: H3Event, opts: { statusCode?: number, message?: string } = {}): Promise<UserSessionRequired> {
|
||||
const userSession = await getUserSession(event)
|
||||
|
||||
if (!userSession.user) {
|
||||
throw createError({
|
||||
statusCode: opts.statusCode || 401,
|
||||
message: opts.message || 'Unauthorized',
|
||||
})
|
||||
}
|
||||
|
||||
return userSession as UserSessionRequired
|
||||
}
|
||||
|
||||
let sessionConfig: SessionConfig
|
||||
|
||||
function _useSession(event: H3Event) {
|
||||
if (!sessionConfig) {
|
||||
const runtimeConfig = useRuntimeConfig(event)
|
||||
|
||||
sessionConfig = runtimeConfig.session;
|
||||
}
|
||||
return useSession<UserSession>(event, sessionConfig)
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import useDatabase from "~/composables/useDatabase";
|
||||
import { Return } from "~/types/api";
|
||||
import type { UserSession, UserSessionRequired } from "~/types/auth";
|
||||
|
||||
export async function checkSession(e: H3Event<EventRequestHandler>, session: UserSession): Promise<Return | undefined>
|
||||
{
|
||||
const db = useDatabase();
|
||||
|
||||
if(session.id && session.user?.id)
|
||||
{
|
||||
const checkSession = db.query("SELECT user_id FROM user_sessions WHERE id = ?1");
|
||||
const sessionId = checkSession.get(session.id) as any;
|
||||
|
||||
if(sessionId && sessionId.user_id === session.user?.id)
|
||||
{
|
||||
return { success: true, session };
|
||||
}
|
||||
else
|
||||
{
|
||||
await clearUserSession(e);
|
||||
|
||||
setResponseStatus(e, 406);
|
||||
return { success: false, error: new Error('Vous êtes déjà connecté') };
|
||||
}
|
||||
}
|
||||
}
|
||||
export async function logSession(e: H3Event<EventRequestHandler>, session: UserSessionRequired)
|
||||
{
|
||||
const db = useDatabase();
|
||||
|
||||
const loggingIn = db.query(`INSERT INTO user_sessions(id, user_id, ip, agent, lastRefresh) VALUES(?1, ?2, ?3, ?4, ?5)`);
|
||||
loggingIn.get(session.id, session.user.id, getRequestIP(e) ?? null, getRequestHeader(e, 'User-Agent') ?? null, Date.now());
|
||||
}
|
||||
|
|
@ -1,3 +1,19 @@
|
|||
export interface SuccessHandler
|
||||
{
|
||||
success: true;
|
||||
session: UserSession;
|
||||
}
|
||||
export interface ErrorHandler
|
||||
{
|
||||
success: false;
|
||||
error: Error | ZodError<{
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
}>;
|
||||
}
|
||||
export type Return = SuccessHandler | ErrorHandler;
|
||||
|
||||
export interface Project {
|
||||
id: number;
|
||||
name: string;
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import type { ComputedRef, Ref } from 'vue'
|
||||
|
||||
import 'vue-router';
|
||||
declare module 'vue-router'
|
||||
{
|
||||
interface RouteMeta
|
||||
{
|
||||
requiresAuth?: boolean;
|
||||
guestsGoesTo?: string;
|
||||
usersGoesTo?: string;
|
||||
}
|
||||
}
|
||||
|
||||
import 'nuxt';
|
||||
declare module 'nuxt'
|
||||
{
|
||||
interface RuntimeConfig
|
||||
{
|
||||
session: SessionConfig;
|
||||
}
|
||||
}
|
||||
|
||||
export interface UserRawData {
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
state: number;
|
||||
}
|
||||
|
||||
export interface UserExtendedData {
|
||||
|
||||
}
|
||||
|
||||
export type User = UserRawData & UserExtendedData;
|
||||
|
||||
export interface UserSession {
|
||||
user?: User;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface UserSessionRequired extends UserSession {
|
||||
user: User;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface UserSessionComposable {
|
||||
/**
|
||||
* Computed indicating if the auth session is ready
|
||||
*/
|
||||
ready: ComputedRef<boolean>
|
||||
/**
|
||||
* Computed indicating if the user is logged in.
|
||||
*/
|
||||
loggedIn: ComputedRef<boolean>
|
||||
/**
|
||||
* The user object if logged in, null otherwise.
|
||||
*/
|
||||
user: ComputedRef<User | null>
|
||||
/**
|
||||
* The session object.
|
||||
*/
|
||||
session: Ref<UserSession>
|
||||
/**
|
||||
* Fetch the user session from the server.
|
||||
*/
|
||||
fetch: () => Promise<void>
|
||||
/**
|
||||
* Clear the user session and remove the session cookie.
|
||||
*/
|
||||
clear: () => Promise<void>
|
||||
}
|
||||
Loading…
Reference in New Issue