I’m aware that this might be basic knowledge for some forum-users. But still, I would like to share my approach for those who bump into the same question. And also to hear from the advanced users if this is a safe approach(?) (Not quit sure if it’s even worthly enough to reach the infamous Cookbook…)
First off you need to install both Nuxt-kql & kirby-headless-starter.
I didn’t have to tweak the headless Kirby Instance. The authentication is done by doing a post-request from Nuxt (localhost:3000) to Kirby’s (localhost:8000) enpoint api/auth/login
.
For this I installed Nuxt.js | Pinia to act as a store to handle the received response from the post-request.
/store/auth.ts
:
import { defineStore } from "pinia";
interface UserPayloadInterface {
username: string;
password: string;
}
export const useAuthStore = defineStore("auth", {
state: () => ({
authenticated: false,
loading: false,
}),
actions: {
authenticateUser({ username, password }: UserPayloadInterface) {
const loginApi = "http://localhost:8000/api/auth/login";
const csrf = "<?= csrf() ?>";
const credentials = username + ":" + password;
// authenticate user with kirby headless api
async function authenticate() {
const response = await fetch(loginApi, {
method: "post",
// for reference: "/vendor/getkirby/cms/config/api/routes/auth.php" && https://getkirby.com/docs/reference/objects/cms/auth/login
headers: new Headers({ "Content-Type": "application/json", "X-CSRF": csrf, Authorization: "Basic " + btoa(credentials) }),
body: JSON.stringify({ email: username, password: password, long: false }),
});
if (!response.ok) {
const message = `An error has occured: ${response.status}`;
throw new Error(message);
}
const data = await response.json();
return data;
}
// then set token cookie
authenticate().then((data) => {
const token = useCookie("token"); // useCookie new hook in nuxt 3
token.value = data.user.id; // set token to cookie
if (!data.ok) {
useRouter().push("/dashboard"); // redirect to dashboard page
this.authenticated = true; // set authenticated state value to true
}
});
},
logUserOut() {
const token = useCookie("token"); // useCookie new hook in nuxt 3
this.authenticated = false; // set authenticated state value to false
token.value = null; // clear the token cookie
useRouter().push("/login"); // redirect to login page
},
},
});
/middleware/auth.ts
:
import { storeToRefs } from "pinia";
import { useAuthStore } from "~/store/auth";
export default defineNuxtRouteMiddleware((to) => {
const { authenticated } = storeToRefs(useAuthStore()); // make authenticated state reactive
const token = useCookie("token"); // get token from cookies
if (token.value) {
// check if value exists
authenticated.value = true; // update the state to authenticated
}
// if token exists and url is /login redirect to dashboard
if (token.value && to?.name === "login") {
return navigateTo("/dashboard");
}
// if token doesn't exist redirect to log in
if (!token.value && to?.name !== "login") {
abortNavigation();
return navigateTo("/login");
}
});
pages/login.vue
:
<script lang="ts" setup>
import { storeToRefs } from "pinia"; // import storeToRefs helper hook from pinia
import { useAuthStore } from "~/store/auth"; // import the auth store we just created
const { authenticateUser } = useAuthStore(); // use authenticateUser action from auth store
const { authenticated } = storeToRefs(useAuthStore()); // make authenticated state reactive with storeToRefs
const user = ref({
username: "",
password: "",
});
const router = useRouter();
// bind the login function to the login button click event and prevent default form submission
const login = async () => {
await authenticateUser(user.value); // call authenticateUser and pass the user object
// redirect to homepage if user is authenticated
if (authenticated) {
router.push("/dashboard");
}
};
definePageMeta({
middleware: "auth", // this should match the name of the file inside the middleware directory
});
</script>
pages/dashboard.vue
:
<script setup lang="ts">
import { useAuthStore } from "~/store/auth"; // import the auth store we just created
const { logUserOut } = useAuthStore(); // use logUserOut action from auth store
const token = useCookie("token"); // useCookie new hook in nuxt 3
definePageMeta({
middleware: "auth", // this should match the name of the file inside the middleware directory
});
</script>
This middleware /middleware/auth.ts
I’ve added to my dashboard-page and login-page. It automatically routes the user to the login-page if it can’t find the token in the Pinia store. If the token exists and the url is /login
it will redirect to /dashboard
.
The /store/auth.ts
handles the authentication post request. It has an action to login and logout.
The login action does the fetching and uses the necessary header to authenticate:
"Content-Type": "application/json", "X-CSRF": <csrf>, Authorization: "Basic " + btoa(<credentials>)
Next I’ve the user-id from the received response-data as a cookie-token:
// then set token cookie
authenticate().then((data) => {
const token = useCookie("token"); // useCookie new hook in nuxt 3
token.value = data.user.id; // set token to cookie
if (!data.ok) {
useRouter().push("/dashboard"); // redirect to login page
this.authenticated = true; // set authenticated state value to true
}
});
final question: is this a safe approach? Can it do any harm if I take the user-id as a token? Because with that I can populate the dashboard with the user’s specific data.