Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nuxt kratos selfservice example. #95

Closed
wants to merge 2 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions nuxt-kratos-selfservice/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist

# Node dependencies
node_modules

# Logs
logs
*.log

# Misc
.DS_Store
.fleet
.idea

# Local env files
.env
.env.*
!.env.example
39 changes: 39 additions & 0 deletions nuxt-kratos-selfservice/.kratos/identity.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Person",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"title": "E-Mail",
"minLength": 3,
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
},
"verification": {
"via": "email"
},
"recovery": {
"via": "email"
}
}
}
},
"required": [
"email"
],
"additionalProperties": false
}
},
"required": [
"traits"
]
}
100 changes: 100 additions & 0 deletions nuxt-kratos-selfservice/.kratos/kratos.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@

dsn: memory

serve:
public:
base_url: http://127.0.0.1:4433/
cors:
enabled: true
allowed_origins:
- http://127.0.0.1:3000
allowed_methods:
- POST
- GET
- PUT
- PATCH
- DELETE
allowed_headers:
- Authorization
- Content-Type
exposed_headers:
- Content-Type
allow_credentials: true
admin:
base_url: http://127.0.0.1:4434/

selfservice:
default_browser_return_url: http://127.0.0.1:3000/
allowed_return_urls:
- http://127.0.0.1:3000

methods:
password:
enabled: true
config:
haveibeenpwned_enabled: false
min_password_length: 8

totp:
enabled: true
lookup_secret:
enabled: true

flows:
error:
ui_url: http://127.0.0.1:3000/error

settings:
ui_url: http://127.0.0.1:3000/settings
privileged_session_max_age: 15m

recovery:
enabled: true
ui_url: http://127.0.0.1:3000/recovery

verification:
enabled: true
ui_url: http://127.0.0.1:3000/verification
after:
default_browser_return_url: http://127.0.0.1:3000/

logout:
after:
default_browser_return_url: http://127.0.0.1:3000/login

login:
ui_url: http://127.0.0.1:3000/login

registration:
ui_url: http://127.0.0.1:3000/signup
after:
password:
hooks:
- hook: session

log:
level: debug
format: text
leak_sensitive_values: true

secrets:
cookie:
- PLEASE-CHANGE-ME-I-AM-VERY-INSECURE

session:
cookie:
same_site: Lax

hashers:
argon2:
parallelism: 1
memory: 128MB
iterations: 2
salt_length: 16
key_length: 16

identity:
default_schema_id: default
schemas:
- id: default
url: file:///etc/config/kratos/identity.schema.json
1 change: 1 addition & 0 deletions nuxt-kratos-selfservice/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v20.12.2
38 changes: 38 additions & 0 deletions nuxt-kratos-selfservice/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Nuxt 3 Kratox Selfservice example

## Relevant files

The nuxt integration is very simple and is not based on any modules.
The files used to handle authentication and session checks are

- `composables/useAuth.ts`: Contains the functions to login, logout and check if the user is authenticated.
- `middleware/auth.global.ts`: Checks for a valid kratos session and redirects to the login page if not found.
- `pages/signup.vue` & `pages/login.vue`: The signup and login pages implements with `useAuth`
- `plugins/kratos.ts`: Provide kratos client's to the nuxt app.

## Setup

Make sure to install the dependencies:

```bash
# npm
npm install

# pnpm
pnpm install

# yarn
yarn install

# bun
bun install
```

## Development Server


```bash
docker compose up -d
```

Then the frontend will be available on http://127.0.0.1:3000. Make sure to use the IP address not `localhost` as it will lead to problems with cookies.
5 changes: 5 additions & 0 deletions nuxt-kratos-selfservice/app.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
209 changes: 209 additions & 0 deletions nuxt-kratos-selfservice/components/KratosForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
<template>
<form @submit.prevent="handleSubmit" class="kratos-form">
<template v-for="node in flow.ui.nodes" :key="node.attributes.id">
<div v-if="isVisibleField(node)" class="form-group">
<label :for="node.attributes.id" class="form-label">
{{ node.meta.label?.text }}
</label>
<input
v-if="['text', 'password', 'email'].includes(node.attributes.type)"
:id="node.attributes.id"
:name="node.attributes.name"
v-model="formData[node.attributes.name]"
:type="node.attributes.type"
:placeholder="node.meta.label?.text"
class="form-input"
:class="{ 'input-error': hasError(node) }"
:required="node.attributes.required"
/>
<select
v-else-if="node.attributes.type === 'select'"
:id="node.attributes.id"
:name="node.attributes.name"
v-model="formData[node.attributes.name]"
class="form-select"
:class="{ 'input-error': hasError(node) }"
:required="node.attributes.required"
>
<option value="" disabled selected>
{{ node.meta.label?.text }}
</option>
<option
v-for="option in node.attributes.options"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</option>
</select>
<small v-if="hasError(node)" class="error-message">
{{ getErrorMessage(node) }}
</small>
</div>
<input
v-else-if="isHiddenField(node)"
:id="node.attributes.id"
:name="node.attributes.name"
:value="node.attributes.value"
type="hidden"
/>
</template>

<div v-if="!hasPasswordField" class="form-group">
<label for="password" class="form-label">Password</label>
<input
id="password"
v-model="formData.password"
type="password"
class="form-input"
required
/>
</div>

<slot name="additional-fields"></slot>

<button
type="submit"
class="submit-button"
:disabled="isLoading || !isFormValid"
>
<span v-if="submitButtonIcon" class="button-icon">{{
submitButtonIcon
}}</span>
{{ submitButtonLabel }}
</button>
</form>
</template>

<script lang="ts" setup>
import { computed, watch } from "vue";
import type { UiNode, UiFlow } from "@ory/kratos-client";
const props = defineProps<{
flow: UiFlow;
formData: Record<string, string>;
isLoading: boolean;
submitButtonLabel: string;
submitButtonIcon?: string;
}>();
const emit = defineEmits(["submit", "update:formData"]);
const isVisibleField = (node: UiNode) => {
return node.type === "input" && node.attributes.type !== "hidden";
};
const isHiddenField = (node: UiNode) => {
return node.type === "input" && node.attributes.type === "hidden";
};
const hasError = (node: UiNode) => {
return node.messages && node.messages.length > 0;
};
const getErrorMessage = (node: UiNode) => {
return node.messages && node.messages.length > 0 ? node.messages[0].text : "";
};
const hasPasswordField = computed(() => {
return props.flow.ui.nodes.some(
(node) =>
node.attributes.name === "password" || node.attributes.type === "password"
);
});
const isFormValid = computed(() => {
if (!props.flow) return false;
return props.flow.ui.nodes.every((node) => {
if (isVisibleField(node) && node.attributes.required) {
return !!props.formData[node.attributes.name];
}
return true;
});
});
watch(
() => props.flow,
(newFlow) => {
if (newFlow) {
newFlow.ui.nodes.forEach((node) => {
if (node.attributes.name && !props.formData[node.attributes.name]) {
props.formData[node.attributes.name] = node.attributes.value || "";
}
});
emit("update:formData", props.formData);
}
},
{ immediate: true, deep: true }
);
const handleSubmit = () => {
const submissionData = { ...props.formData };
props.flow.ui.nodes.forEach((node) => {
if (isHiddenField(node)) {
submissionData[node.attributes.name] = node.attributes.value;
}
});
emit("submit", submissionData);
};
</script>

<style scoped>
.kratos-form {
max-width: 400px;
margin: 0 auto;
}
.form-group {
margin-bottom: 1rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.form-input,
.form-select {
width: 100%;
padding: 0.5rem;
border: 1px solid var(--color-border, #ccc);
border-radius: 4px;
font-size: 1rem;
}
.input-error {
border-color: var(--color-danger, #ff4d4f);
}
.error-message {
display: block;
color: var(--color-danger, #ff4d4f);
font-size: 0.875rem;
margin-top: 0.25rem;
}
.submit-button {
width: 100%;
padding: 0.75rem;
background-color: var(--color-primary, #1890ff);
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
}
.submit-button:disabled {
background-color: var(--color-disabled, #d9d9d9);
cursor: not-allowed;
}
.button-icon {
margin-right: 0.5rem;
}
</style>
187 changes: 187 additions & 0 deletions nuxt-kratos-selfservice/composables/useAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { ref, readonly, onMounted } from "vue";
import type {
Session,
LoginFlow,
RegistrationFlow,
UpdateLoginFlowBody,
UpdateRegistrationFlowBody,
} from "@ory/kratos-client";

interface AuthError {
message: string;
details?: Record<string, any>;
}

interface User {
id: string;
email: string;
}

export const useAuth = () => {
const { $kratos } = useNuxtApp();

const user = ref<User | null>(null);
const isAuthenticated = ref(false);
const isLoading = ref(true);
const error = ref<AuthError | null>(null);
const loginFlow = ref<LoginFlow | null>(null);
const registrationFlow = ref<RegistrationFlow | null>(null);

const setUser = (session: Session) => {
if (!session.identity) {
user.value = null;
isAuthenticated.value = false;
return;
}

user.value = {
id: session.identity.id,
email: session.identity.traits.email,
};
isAuthenticated.value = true;
};

const handleError = (err: unknown): AuthError => {
if (err instanceof Error) {
return { message: err.message };
}
if (typeof err === "object" && err !== null && "response" in err) {
const errorResponse = (err as any).response?.data;
if (errorResponse && typeof errorResponse === "object") {
return {
message: errorResponse.error?.message || "An unknown error occurred",
details: errorResponse.error?.details,
};
}
}
return { message: "An unknown error occurred" };
};

const checkAuth = async () => {
isLoading.value = true;
error.value = null;
try {
const { data: session } = await $kratos.toSession();
setUser(session);
} catch (err) {
error.value = handleError(err);
isAuthenticated.value = false;
user.value = null;
} finally {
isLoading.value = false;
}
};

const initializeLoginFlow = async () => {
isLoading.value = true;
error.value = null;
try {
const { data } = await $kratos.createBrowserLoginFlow();
loginFlow.value = data;
} catch (err) {
error.value = handleError(err);
} finally {
isLoading.value = false;
}
};

const login = async (updateLoginFlowBody: UpdateLoginFlowBody) => {
isLoading.value = true;
error.value = null;
try {
if (!loginFlow.value) {
throw new Error("Login flow not initialized");
}
const { data: successfulNativeLogin } = await $kratos.updateLoginFlow({
flow: loginFlow.value.id,
updateLoginFlowBody,
});
setUser(successfulNativeLogin.session);
return true;
} catch (err) {
error.value = handleError(err);
return false;
} finally {
isLoading.value = false;
}
};

const logout = async () => {
isLoading.value = true;
error.value = null;
try {
await $kratos.createBrowserLogoutFlow();
isAuthenticated.value = false;
user.value = null;
return navigateTo("/login");
} catch (err) {
error.value = handleError(err);
} finally {
isLoading.value = false;
}
};

const initializeRegistrationFlow = async () => {
isLoading.value = true;
error.value = null;
try {
const { data } = await $kratos.createBrowserRegistrationFlow();
registrationFlow.value = data;
} catch (err) {
error.value = handleError(err);
} finally {
isLoading.value = false;
}
};

const register = async (
updateRegistrationFlowBody: UpdateRegistrationFlowBody
) => {
isLoading.value = true;
error.value = null;
try {
if (!registrationFlow.value) {
throw new Error("Registration flow not initialized");
}
const { data: successfulNativeRegistration } =
await $kratos.updateRegistrationFlow({
flow: registrationFlow.value.id,
updateRegistrationFlowBody,
});

if (!successfulNativeRegistration.session) {
error.value = {
message: "Registration successful but no session created",
};
return false;
}
setUser(successfulNativeRegistration.session);
return true;
} catch (err) {
error.value = handleError(err);
return false;
} finally {
isLoading.value = false;
}
};

const clearError = () => {
error.value = null;
};

return {
user: readonly(user),
isAuthenticated: readonly(isAuthenticated),
isLoading: readonly(isLoading),
error: readonly(error),
loginFlow: readonly(loginFlow),
registrationFlow: readonly(registrationFlow),
checkAuth,
initializeLoginFlow,
login,
logout,
initializeRegistrationFlow,
register,
clearError,
};
};
6 changes: 6 additions & 0 deletions nuxt-kratos-selfservice/dev.dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM node:20.12.2-slim

WORKDIR /app
EXPOSE 3000

CMD ["npm", "run", "dev"]
54 changes: 54 additions & 0 deletions nuxt-kratos-selfservice/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
version: '3.8'

services:
nuxt-app:
build:
context: .
dockerfile: dev.dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=development
volumes:
- .:/app
command: npm run dev
depends_on:
- kratos
networks:
- app-network

kratos:
image: oryd/kratos:v1
ports:
- "4433:4433"
- "4434:4434"
environment:
- DSN=sqlite:///var/lib/sqlite/db.sqlite?_fk=true
- LOG_LEAK_SENSITIVE_VALUES=true
volumes:
- ./.kratos:/etc/config/kratos
- kratos_data:/var/lib/sqlite
command: serve -c /etc/config/kratos/kratos.yml --dev
networks:
- app-network

kratos-migrate:
image: oryd/kratos:v1
environment:
- DSN=sqlite:///var/lib/sqlite/db.sqlite?_fk=true
volumes:
- ./.kratos:/etc/config/kratos
- kratos_data:/var/lib/sqlite
command: migrate sql -e --yes
depends_on:
- kratos
networks:
- app-network

networks:
app-network:
driver: bridge

volumes:
mongodb_data:
kratos_data:
3 changes: 3 additions & 0 deletions nuxt-kratos-selfservice/layouts/default.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<slot></slot>
</template>
50 changes: 50 additions & 0 deletions nuxt-kratos-selfservice/middleware/auth.global.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { serverCheckAuth } from "~/server/utils/checkAuth";
import { useAuth } from "~/composables/useAuth";
import type { RouteLocationNormalizedGeneric } from "vue-router";

export default defineNuxtRouteMiddleware(async (to) => {
if (import.meta.server) {
if (!to.path.startsWith("/api")) {
return;
}

const event = useRequestEvent();
if (!event) {
return;
}
const session = await serverCheckAuth(event);

return checkRoute(session, to);
}

if (import.meta.client) {
const { checkAuth, isAuthenticated, isLoading } = useAuth();

await checkAuth();

if (isLoading.value) {
return;
}

return checkRoute(isAuthenticated.value, to);
}
});

const checkRoute = (
session: boolean,
route: RouteLocationNormalizedGeneric
) => {
if (
!session &&
route.meta.requiresAuth !== false &&
route.meta.guestOnly !== true
) {
return navigateTo("/login");
}

if (session && route.meta.guestOnly) {
return navigateTo("/");
}

return;
};
15 changes: 15 additions & 0 deletions nuxt-kratos-selfservice/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default defineNuxtConfig({
compatibilityDate: "2024-04-03",
devtools: { enabled: true },
plugins: ["@/plugins/kratos.ts"],
runtimeConfig: {
ORY_SDK_URL: process.env.NUXT_ORY_SDK_URL || "http://kratos:4433",
public: {
ORY_SDK_URL:
process.env.NUXT_PUBLIC_ORY_SDK_URL || "http://127.0.0.1:4433",
},
},
server: {
host: "0.0.0.0",
},
});
9,124 changes: 9,124 additions & 0 deletions nuxt-kratos-selfservice/package-lock.json

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions nuxt-kratos-selfservice/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@ory/kratos-client": "^1.2.1",
"nuxt": "^3.13.0",
"vue": "latest",
"vue-router": "latest"
}
}
6 changes: 6 additions & 0 deletions nuxt-kratos-selfservice/pages/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<template>
<div>
<NuxtRouteAnnouncer />
<NuxtWelcome />
</div>
</template>
169 changes: 169 additions & 0 deletions nuxt-kratos-selfservice/pages/login.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<template>
<div class="login-container">
<div class="login-box">
<div class="login-header">
<h1>Welcome Back</h1>
<p>
Don't have an account?
<a @click="navigateTo('/signup')" class="link">Create today!</a>
</p>
</div>

<KratosForm
v-if="loginFlow"
:flow="loginFlow"
v-model:formData="formData"
:isLoading="isLoading"
submitButtonLabel="Sign In"
submitButtonIcon=""
@submit="handleSubmit"
>
<template #additional-fields>
<div class="additional-fields">
<div class="remember-me">
<input type="checkbox" id="rememberme" v-model="rememberMe">
<label for="rememberme">Remember me</label>
</div>
<a
class="forgot-password link"
@click="navigateTo('/recovery')"
>
Forgot password?
</a>
</div>
</template>
</KratosForm>

<button
class="signup-button"
:disabled="isLoading"
@click="navigateTo('/signup')"
>
Sign Up
</button>
</div>
</div>
</template>

<script setup lang="ts">
import { ref, onMounted, watch } from "vue";
import { useAuth } from "~/composables/useAuth";
import KratosForm from "~/components/KratosForm.vue";
definePageMeta({
guestOnly: true,
});
const { loginFlow, isLoading, error, clearError, initializeLoginFlow, login } = useAuth();
const formData = ref<Record<string, string>>({});
const rememberMe = ref(true);
onMounted(async () => {
await initializeLoginFlow();
});
watch(error, (newError) => {
if (newError) {
alert(newError.message); // Replace with your preferred error handling method
clearError();
}
});
const handleSubmit = async () => {
if (isLoading.value) return;
const success = await login(formData.value);
if (success) {
alert("Login successful!"); // Replace with your preferred success handling method
navigateTo("/");
}
};
</script>

<style scoped>
.login-container {
background-color: var(--color-background);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 1rem;
}
.login-box {
background-color: var(--color-background-soft);
padding: 2rem;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 400px;
}
.login-header {
text-align: center;
margin-bottom: 2rem;
}
.login-header h1 {
font-size: 1.5rem;
color: var(--color-heading);
margin-bottom: 0.5rem;
}
.login-header p {
color: var(--color-text);
}
.link {
color: var(--color-primary);
cursor: pointer;
text-decoration: none;
}
.link:hover {
text-decoration: underline;
}
.additional-fields {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.remember-me {
display: flex;
align-items: center;
}
.remember-me input {
margin-right: 0.5rem;
}
.forgot-password {
font-size: 0.9rem;
}
.signup-button {
width: 100%;
padding: 0.75rem;
background-color: var(--color-primary);
color: var(--color-background);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
margin-top: 1rem;
transition: background-color 0.3s ease;
}
.signup-button:hover {
background-color: var(--color-primary-dark);
}
.signup-button:disabled {
background-color: var(--color-border);
cursor: not-allowed;
}
</style>
228 changes: 228 additions & 0 deletions nuxt-kratos-selfservice/pages/signup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
<template>
<div class="signup-container">
<div class="signup-box">
<div class="signup-header">
<h1>Create an Account</h1>
<p>
Already have an account?
<a @click="navigateTo('/login')" class="link">Sign in!</a>
</p>
</div>

<KratosForm
v-if="flow"
:flow="flow"
v-model:formData="formData"
:isLoading="isLoading"
submitButtonLabel="Register"
submitButtonIcon=""
@submit="handleSubmit"
>
<template #additional-fields>
<div class="form-group">
<label for="password_repeat">Repeat Password</label>
<input
id="password_repeat"
v-model="passwordRepeat"
type="password"
placeholder="Repeat your password"
:class="{ 'input-error': passwordMismatch }"
required
/>
<small v-if="passwordMismatch" class="error-message">
Passwords do not match
</small>
</div>
</template>
</KratosForm>

<button
class="signin-button"
:disabled="isLoading"
@click="navigateTo('/login')"
>
Sign In
</button>
</div>
</div>
</template>

<script lang="ts" setup>
import {
Configuration,
FrontendApi,
type RegistrationFlow,
} from "@ory/kratos-client";
import { ref, onMounted, computed } from "vue";
import KratosForm from "~/components/KratosForm.vue";
definePageMeta({
guestOnly: true,
});
const config = useRuntimeConfig();
const ory = new FrontendApi(
new Configuration({
basePath: config.public.ORY_SDK_URL,
baseOptions: {
withCredentials: true,
},
})
);
const flow = ref<RegistrationFlow | null>(null);
const formData = ref<Record<string, string>>({});
const passwordRepeat = ref("");
const isLoading = ref(false);
const passwordMismatch = computed(() => {
return formData.value["password"] !== passwordRepeat.value;
});
onMounted(() => {
ory
.createBrowserRegistrationFlow()
.then(({ data }) => {
flow.value = data;
flow.value.ui.nodes.forEach((node) => {
if ("name" in node.attributes) {
formData.value[node.attributes.name] = "";
}
});
})
.catch((err) => {
alert("Failed to initialize registration flow. Please try again.");
});
});
const handleSubmit = () => {
if (isLoading.value || !flow.value || passwordMismatch.value) return;
isLoading.value = true;
const submitData = {
...formData.value,
...Object.fromEntries(
flow.value.ui.nodes
.filter(
(node) => node.type === "input" && node.attributes.type === "hidden"
)
.map((node) => [node.attributes.name, node.attributes.value])
),
};
ory
.updateRegistrationFlow({
flow: flow.value.id,
updateRegistrationFlowBody: { ...submitData, method: "password" },
})
.then(({ data }) => {
console.log("success", data);
alert("You are signed up!");
navigateTo("/login");
})
.catch((error) => {
console.log("error", error);
alert("An error occurred during registration. Please try again.");
})
.finally(() => {
isLoading.value = false;
});
};
</script>

<style scoped>
.signup-container {
background-color: var(--color-background);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 1rem;
}
.signup-box {
background-color: var(--color-background-soft);
padding: 2rem;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 400px;
}
.signup-header {
text-align: center;
margin-bottom: 2rem;
}
.signup-header h1 {
font-size: 1.5rem;
color: var(--color-heading);
margin-bottom: 0.5rem;
}
.signup-header p {
color: var(--color-text);
}
.link {
color: var(--color-primary);
cursor: pointer;
text-decoration: none;
}
.link:hover {
text-decoration: underline;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
color: var(--color-text);
}
.form-group input {
width: 100%;
padding: 0.5rem;
border: 1px solid var(--color-border);
border-radius: 4px;
font-size: 1rem;
}
.input-error {
border-color: var(--color-danger);
}
.error-message {
color: var(--color-danger);
font-size: 0.875rem;
margin-top: 0.25rem;
}
.signin-button {
width: 100%;
padding: 0.75rem;
background-color: var(--color-primary);
color: var(--color-background);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
margin-top: 1rem;
transition: background-color 0.3s ease;
}
.signin-button:hover {
background-color: var(--color-primary-dark);
}
.signin-button:disabled {
background-color: var(--color-border);
cursor: not-allowed;
}
</style>
24 changes: 24 additions & 0 deletions nuxt-kratos-selfservice/plugins/kratos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { defineNuxtPlugin } from "#app";
import { Configuration, FrontendApi } from "@ory/kratos-client";

const createClient = (url: string) => {
return new FrontendApi(
new Configuration({
basePath: url,
baseOptions: {
withCredentials: true,
},
})
);
};

export default defineNuxtPlugin((nuxtApp) => {
const config = useRuntimeConfig();
const kratos = createClient(config.public.ORY_SDK_URL);

return {
provide: {
kratos,
},
};
});
Binary file added nuxt-kratos-selfservice/public/favicon.ico
Binary file not shown.
1 change: 1 addition & 0 deletions nuxt-kratos-selfservice/public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

3 changes: 3 additions & 0 deletions nuxt-kratos-selfservice/server/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}
33 changes: 33 additions & 0 deletions nuxt-kratos-selfservice/server/utils/checkAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { getCookie } from "h3";
import { useRuntimeConfig } from "#imports";
import type { H3Event } from "h3";
import { Configuration, FrontendApi } from "@ory/kratos-client";

const createKratosClient = (basePath: string) =>
new FrontendApi(
new Configuration({
basePath,
})
);

export async function serverCheckAuth(event: H3Event) {
const config = useRuntimeConfig();
const cookie = getCookie(event, "ory_kratos_session");
const client = createKratosClient(config.ORY_SDK_URL);

return client
.toSession({
xSessionToken: cookie,
})
.then(({ data, status }) => {
if (status !== 200) {
console.error(`HTTP error! status: ${status}`);
return false;
}
return !!data.active && !!data.id && !!data.identity;
})
.catch((error) => {
console.error("Error checking authentication:", error);
return false;
});
}
4 changes: 4 additions & 0 deletions nuxt-kratos-selfservice/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}