Skip to content

Commit f114c5d

Browse files
committed
first commit
0 parents  commit f114c5d

36 files changed

+14342
-0
lines changed

.env.example

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
NUXT_AUTH_PASSWORD=secretsecretsecretsecretsecretsecretsecret

.gitignore

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
node_modules
2+
*.log*
3+
.nuxt
4+
.nitro
5+
.cache
6+
.output
7+
.env
8+
dist
9+
.data

.npmrc

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
shamefully-hoist=true
2+
strict-peer-dependencies=false

README.md

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# ZenStack Nuxt Tutorial Project
2+
3+
This is a sample project for demonstrating how to use ZenStack in a Next.js project.
4+
5+
See documentation [here](https://zenstack.dev/docs/get-started/nuxt).
6+
7+
To run the project:
8+
9+
```
10+
npm run build
11+
npm run dev
12+
```
13+
14+
For the Next.js 13 new "app" directory, please checkout [this project](https://github.com/zenstackhq/docs-tutorial-nextjs-app-dir).

app.vue

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<script setup lang="ts">
2+
import { provideHooksContext } from './lib/hooks';
3+
4+
// Provide tanstack-query context
5+
// Use an absolute endpoint so server-side fetch works too
6+
provideHooksContext({
7+
endpoint: 'http://localhost:3000/api/model',
8+
});
9+
</script>
10+
11+
<template>
12+
<NuxtExampleLayout repo="nuxt/examples">
13+
<AppNav />
14+
<div>
15+
<NuxtPage />
16+
</div>
17+
</NuxtExampleLayout>
18+
</template>

auth/composables/auth.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
export const useAuth = () => useNuxtApp().$auth
2+
3+
export const authLogin = async (email: string, password: string) => {
4+
await $fetch("/api/auth/login", {
5+
method: "POST",
6+
body: {
7+
email: email,
8+
password: password,
9+
},
10+
});
11+
useAuth().redirectTo.value = null;
12+
await useAuth().updateSession();
13+
await navigateTo(useAuth().redirectTo.value || "/");
14+
};
15+
16+
export const authRegister = async (email: string, password: string) => {
17+
await $fetch("/api/auth/register", {
18+
method: "POST",
19+
body: {
20+
email: email,
21+
password: password,
22+
},
23+
});
24+
return await authLogin(email, password);
25+
};
26+
27+
export const authLogout = async () => {
28+
await $fetch("/api/auth/logout", {
29+
method: "POST",
30+
});
31+
await useAuth().updateSession();
32+
};

auth/nuxt.config.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// https://nuxt.com/docs/api/configuration/nuxt-config
2+
3+
// This code is demo only.
4+
if (!process.env.NUXT_AUTH_PASSWORD) {
5+
console.warn('Security warning: NUXT_AUTH_PASSWORD is not set. Using an example value. Please set it otherwise your session is unsecure!');
6+
process.env.NUXT_AUTH_PASSWORD = 'secretsecretsecretsecretsecretsecretsecret'
7+
}
8+
9+
export default defineNuxtConfig({
10+
runtimeConfig: {
11+
auth: {
12+
name: "nuxt-session",
13+
password: process.env.NUXT_AUTH_PASSWORD || "",
14+
},
15+
},
16+
nitro: {
17+
storage: {
18+
".data:auth": { driver: "fs", base: "./.data/auth" },
19+
},
20+
},
21+
});

auth/plugins/0.auth.ts

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import type { AuthSession } from "~~/auth/server/utils/session";
2+
3+
export default defineNuxtPlugin(async (nuxtApp) => {
4+
// Skip plugin when rendering error page
5+
if (nuxtApp.payload.error) {
6+
return {};
7+
}
8+
9+
const { data: session, refresh: updateSession }
10+
= await useFetch<AuthSession>('/api/auth/session');
11+
12+
const loggedIn: any = computed(() => !!session.value?.email);
13+
14+
// Create a ref to know where to redirect the user when logged in
15+
const redirectTo = useState("authRedirect")
16+
17+
/**
18+
* Add global route middleware to protect pages using:
19+
*
20+
* definePageMeta({
21+
* auth: true
22+
* })
23+
*/
24+
//
25+
26+
addRouteMiddleware(
27+
"auth",
28+
(to) => {
29+
if (to.meta.auth && !loggedIn.value) {
30+
redirectTo.value = to.path
31+
return "/login";
32+
}
33+
},
34+
{ global: true }
35+
);
36+
37+
const currentRoute = useRoute();
38+
39+
if (process.client) {
40+
watch(loggedIn, async (loggedIn) => {
41+
if (!loggedIn && currentRoute.meta.auth) {
42+
redirectTo.value = currentRoute.path
43+
await navigateTo("/login");
44+
}
45+
});
46+
}
47+
48+
if (loggedIn.value && currentRoute.path === "/login") {
49+
await navigateTo(redirectTo.value || "/");
50+
}
51+
52+
return {
53+
provide: {
54+
auth: {
55+
loggedIn,
56+
session,
57+
redirectTo,
58+
updateSession,
59+
},
60+
},
61+
};
62+
});

auth/server/api/auth/login.post.ts

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
export default eventHandler(async (event) => {
2+
const session = await useAuthSession(event);
3+
const { email, password } = await readBody(event);
4+
const user = await findUserByEmail(email);
5+
if (!user) {
6+
throw createError({
7+
message: 'Email not found! Please register.',
8+
statusCode: 401,
9+
});
10+
}
11+
12+
if (!user.password || user.password !== (await hash(password))) {
13+
throw createError({
14+
message: 'Incorrect password!',
15+
statusCode: 401,
16+
});
17+
}
18+
await session.update({
19+
id: user.id,
20+
name: user.name,
21+
email: user.email,
22+
});
23+
return session;
24+
});

auth/server/api/auth/logout.post.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default eventHandler(async (event) => {
2+
const session = await useAuthSession(event);
3+
await session.clear();
4+
return {
5+
message: "Successfully logged out!",
6+
};
7+
});

auth/server/api/auth/register.post.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export default eventHandler(async (event) => {
2+
const { email, password } = await readBody(event);
3+
await createUser({
4+
email,
5+
name: email.split('@')[0],
6+
password: await hash(password)
7+
});
8+
return {
9+
message: "Successfully registered!",
10+
};
11+
});

auth/server/api/auth/session.get.ts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export default eventHandler(async (event) => {
2+
const session = await useAuthSession(event);
3+
return session.data;
4+
});

auth/server/utils/db.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { User } from '@prisma/client';
2+
import { prisma } from '~/server/prisma';
3+
4+
export async function findUserByEmail(email: string) {
5+
return prisma.user.findUnique({ where: { email } });
6+
}
7+
8+
export async function createUser(user: Omit<User, 'id'>) {
9+
return prisma.user.create({
10+
data: user,
11+
});
12+
}

auth/server/utils/session.ts

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { H3Event, SessionConfig } from "h3";
2+
import crypto from "uncrypto";
3+
4+
const sessionConfig: SessionConfig = useRuntimeConfig().auth || {};
5+
6+
export type AuthSession = {
7+
id: string;
8+
name: string;
9+
email: string;
10+
};
11+
12+
export const useAuthSession = async (event: H3Event) => {
13+
const session = await useSession<AuthSession>(event, sessionConfig);
14+
return session
15+
};
16+
17+
export const requireAuthSession = async (event: H3Event) => {
18+
const session = await useAuthSession(event);
19+
if (!session.data.email) {
20+
throw createError({
21+
message: "Not Authorized",
22+
statusCode: 401,
23+
});
24+
}
25+
return session;
26+
}
27+
28+
export async function hash(str: string) {
29+
const msgUint8 = new TextEncoder().encode(str);
30+
const hashBuffer = await crypto.subtle.digest("SHA-512", msgUint8);
31+
const hashArray = Array.from(new Uint8Array(hashBuffer));
32+
const hashHex = hashArray
33+
.map((b) => b.toString(16).padStart(2, "0"))
34+
.join("");
35+
return hashHex;
36+
}

components/AppNav.vue

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<script setup lang="ts"></script>
2+
3+
<template>
4+
<nav>
5+
<NuxtLink to="/">Home Page</NuxtLink> |
6+
<NuxtLink to="/secret">Secret Page</NuxtLink> |
7+
<NuxtLink to="/login" v-if="!$auth.loggedIn.value">Login</NuxtLink>
8+
<NuxtLink to="/profile" v-else>Profile</NuxtLink>
9+
</nav>
10+
<hr />
11+
</template>
12+
13+
<style scoped>
14+
.router-link-exact-active {
15+
color: royalblue;
16+
}
17+
</style>

components/Post.vue

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<script setup lang="ts">
2+
import { useDeletePost, useUpdatePost } from '~/lib/hooks';
3+
4+
const props = defineProps({
5+
id: String,
6+
title: String,
7+
author: {
8+
type: Object,
9+
default: () => ({ email: '' }),
10+
},
11+
published: Boolean,
12+
});
13+
14+
const updatePost = useUpdatePost();
15+
const deletePost = useDeletePost();
16+
17+
const onTogglePublish = async () => {
18+
try {
19+
await updatePost.mutateAsync({
20+
where: { id: props.id },
21+
data: { published: !props.published },
22+
});
23+
} catch (err: any) {
24+
alert(err.info?.message ?? err);
25+
}
26+
};
27+
28+
const onDelete = async () => {
29+
try {
30+
await deletePost.mutateAsync({ where: { id: props.id } });
31+
} catch (err: any) {
32+
alert(err.info?.message ?? err);
33+
}
34+
};
35+
</script>
36+
37+
<template>
38+
<div class="flex justify-center">
39+
<div class="min-w-80">
40+
<span class="mr-4 text-lg font-semibold">{{ title }}</span
41+
><span>by {{ author.email }}</span>
42+
</div>
43+
<div class="ml-8 space-x-2">
44+
<NButton @click="onTogglePublish">{{
45+
published ? 'Unpublish' : 'Publish'
46+
}}</NButton>
47+
<NButton @click="onDelete">Delete</NButton>
48+
</div>
49+
</div>
50+
</template>

lib/hooks/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './user';
2+
export * from './post';
3+
export { VueQueryContextKey, provideHooksContext } from '@zenstackhq/tanstack-query/runtime/vue';

0 commit comments

Comments
 (0)