Magic-Link — Examples
React frontend
tsx
import { useState } from 'react';
const API = import.meta.env.VITE_API_URL || 'http://localhost:1337';
export function MagicLinkLogin() {
const [email, setEmail] = useState('');
const [sent, setSent] = useState(false);
const [error, setError] = useState<string | null>(null);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError(null);
try {
const res = await fetch(`${API}/api/magic-link/send`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
if (!res.ok) throw new Error((await res.json()).error);
setSent(true);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
}
}
if (sent) {
return <p>Check your inbox for a login link ✨</p>;
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
/>
<button type="submit">Send magic link</button>
{error && <p role="alert">{error}</p>}
</form>
);
}
// On the /auth/verify route:
export async function verifyToken(token: string) {
const res = await fetch(`${API}/api/magic-link/verify?token=${token}`);
const data = await res.json();
if (data.jwt) {
localStorage.setItem('jwt', data.jwt);
return data.user;
}
if (data.requiresOtp) {
// Show OTP input UI
return { requiresOtp: true, magicLinkId: data.magicLinkId };
}
throw new Error(data.error);
}Vue 3 frontend
vue
<script setup lang="ts">
import { ref } from 'vue';
const email = ref('');
const sent = ref(false);
const error = ref<string | null>(null);
async function send() {
error.value = null;
const res = await fetch('/api/magic-link/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email.value }),
});
if (res.ok) sent.value = true;
else error.value = (await res.json()).error;
}
</script>
<template>
<form v-if="!sent" @submit.prevent="send">
<input v-model="email" type="email" required placeholder="you@example.com" />
<button type="submit">Send magic link</button>
<p v-if="error" role="alert">{{ error }}</p>
</form>
<p v-else>Check your inbox for a login link ✨</p>
</template>Curl scripting
bash
#!/bin/bash
# Quick CLI login helper
API=${STRAPI_API:-http://localhost:1337}
EMAIL=$1
if [ -z "$EMAIL" ]; then
echo "Usage: $0 email@example.com"; exit 1
fi
curl -sS -X POST "$API/api/magic-link/send" \
-H "Content-Type: application/json" \
-d "{\"email\":\"$EMAIL\"}" | jqCustom middleware — protect routes
typescript
// src/middlewares/require-auth.ts
export default (config, { strapi }) => async (ctx, next) => {
const token = ctx.request.header.authorization?.replace('Bearer ', '');
if (!token) {
return ctx.unauthorized('JWT required');
}
try {
const { id } = await strapi.plugins['users-permissions'].services.jwt.verify(token);
ctx.state.user = await strapi.query('plugin::users-permissions.user').findOne({ where: { id } });
await next();
} catch {
return ctx.unauthorized('Invalid JWT');
}
};Use in route config:
typescript
// src/api/post/routes/post.ts
export default {
routes: [{
method: 'GET',
path: '/posts/me',
handler: 'post.findMine',
config: {
middlewares: ['global::require-auth'],
},
}],
};Custom email template (with Magic-Mail)
If you install Magic-Mail, open Admin → Magic-Mail → Templates → Create:
html
<h1>Sign in to {{ appName }}</h1>
<p>Hi {{ user.firstName }},</p>
<p>Click the button below to sign in. This link works for 15 minutes.</p>
<a href="{{ magicLink }}" style="...">Sign in</a>
<p>If you did not request this, ignore this email.</p>Reference in Magic-Link settings:
Template ID for magic-link email: 5Refresh token rotation with Magic-Sessionmanager
typescript
// Login response already contains jwt
// Magic-Sessionmanager automatically issues a refresh token
// On JWT expiry:
async function refreshJwt() {
const res = await fetch('/api/session/refresh', {
method: 'POST',
credentials: 'include', // cookies carry refresh token
});
if (res.ok) {
const { jwt } = await res.json();
return jwt;
}
// Refresh failed — user must log in again
throw new Error('Session expired');
}Auto user creation with custom defaults
typescript
// config/plugins.ts
'magic-link': {
enabled: true,
config: {
autoCreateUsers: true,
defaultRole: 'subscriber',
onUserCreate: async (user, { strapi }) => {
// Send welcome email, initialize profile, etc.
await strapi.db.query('api::profile.profile').create({
data: { user: user.id, tier: 'free' },
});
},
},
},Next: Troubleshooting →