strapi-plugin-magic-link-v5 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/.eslintignore +1 -0
  2. package/.github/workflows/semantic-release.yml +27 -0
  3. package/README.md +318 -0
  4. package/admin/jsconfig.json +10 -0
  5. package/admin/src/components/Initializer/index.jsx +20 -0
  6. package/admin/src/components/Initializer.jsx +18 -0
  7. package/admin/src/components/LazyComponentLoader.jsx +27 -0
  8. package/admin/src/components/PluginIcon/index.jsx +6 -0
  9. package/admin/src/components/PluginIcon.jsx +5 -0
  10. package/admin/src/index.js +101 -0
  11. package/admin/src/pages/App/index.jsx +50 -0
  12. package/admin/src/pages/App.jsx +15 -0
  13. package/admin/src/pages/HomePage/index.js +2 -0
  14. package/admin/src/pages/HomePage/index.jsx +228 -0
  15. package/admin/src/pages/HomePage.jsx +655 -0
  16. package/admin/src/pages/Settings/index.jsx +1289 -0
  17. package/admin/src/pages/Settings/utils/api.js +13 -0
  18. package/admin/src/pages/Settings/utils/index.js +5 -0
  19. package/admin/src/pages/Settings/utils/layout.js +100 -0
  20. package/admin/src/pages/Settings/utils/schema.js +18 -0
  21. package/admin/src/pages/Tokens/index.jsx +2250 -0
  22. package/admin/src/permissions.js +7 -0
  23. package/admin/src/pluginId.js +3 -0
  24. package/admin/src/routes.js +40 -0
  25. package/admin/src/translations/de.json +188 -0
  26. package/admin/src/translations/en.json +189 -0
  27. package/admin/src/utils/getRequestURL.js +5 -0
  28. package/admin/src/utils/getTrad.js +17 -0
  29. package/admin/src/utils/getTranslation.js +3 -0
  30. package/admin/src/utils/index.js +4 -0
  31. package/build.js +75 -0
  32. package/package.json +59 -0
  33. package/server/bootstrap.js +127 -0
  34. package/server/controllers/settings.js +122 -0
  35. package/server/jsconfig.json +10 -0
  36. package/server/services/store.js +35 -0
  37. package/server/src/bootstrap.js +110 -0
  38. package/server/src/config/index.js +6 -0
  39. package/server/src/content-types/index.js +7 -0
  40. package/server/src/content-types/token/index.js +5 -0
  41. package/server/src/content-types/token/schema.json +47 -0
  42. package/server/src/controllers/auth.js +211 -0
  43. package/server/src/controllers/controller.js +213 -0
  44. package/server/src/controllers/index.js +16 -0
  45. package/server/src/controllers/jwt.js +261 -0
  46. package/server/src/controllers/tokens.js +654 -0
  47. package/server/src/destroy.js +5 -0
  48. package/server/src/index.js +33 -0
  49. package/server/src/middlewares/index.js +3 -0
  50. package/server/src/policies/index.js +3 -0
  51. package/server/src/register.js +5 -0
  52. package/server/src/routes/admin.js +160 -0
  53. package/server/src/routes/content-api.js +27 -0
  54. package/server/src/routes/index.js +9 -0
  55. package/server/src/services/index.js +11 -0
  56. package/server/src/services/magic-link.js +356 -0
  57. package/server/src/services/service.js +13 -0
  58. package/server/utils/index.js +14 -0
  59. package/strapi-admin.js +82 -0
  60. package/strapi-server.js +4 -0
  61. package/vite.config.js +36 -0
package/.eslintignore ADDED
@@ -0,0 +1 @@
1
+ dist
@@ -0,0 +1,27 @@
1
+ name: Semantic Release
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ release:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: write
13
+ packages: write
14
+ issues: write
15
+ steps:
16
+ - uses: actions/checkout@v3
17
+ - uses: actions/setup-node@v3
18
+ with:
19
+ node-version: 20
20
+ registry-url: https://registry.npmjs.org/
21
+ - run: yarn install
22
+ - run: yarn build
23
+ - run: yarn semantic-release
24
+ env:
25
+ GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
26
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
27
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
package/README.md ADDED
@@ -0,0 +1,318 @@
1
+ # Magic Link - Passwordless Authentication for Strapi
2
+
3
+ A secure passwordless authentication solution for Strapi, allowing users to log in via email links without requiring passwords.
4
+
5
+ ## Core Features
6
+
7
+ - **Passwordless Authentication**: Login via secure email links
8
+ - **Token Management**: Admin dashboard for managing and monitoring tokens
9
+ - **JWT Session Tracking**: Monitor and manage active sessions
10
+ - **Security Features**: IP banning, token expiration controls
11
+ - **Admin Interface**: Statistics dashboard and configuration options
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ # Using npm
17
+ npm install magic-link
18
+
19
+ # Using yarn
20
+ yarn add magic-link
21
+ ```
22
+
23
+ After installation, restart your Strapi server and the plugin will be available in the admin panel.
24
+
25
+ ## How It Works
26
+
27
+ ### Email User with Login Link
28
+
29
+ 1. **Request Process**:
30
+
31
+ - User requests a login link by entering their email
32
+ - System generates a secure token and sends an email
33
+ - Email contains a magic link with the token
34
+
35
+ 2. **Token Details**:
36
+ - Cryptographically secure random tokens
37
+ - Configurable expiration time
38
+ - Option for one-time use or reusable tokens
39
+ - Tracks IP address and user agent information
40
+
41
+ ### Login with Token
42
+
43
+ 1. **Authentication Process**:
44
+
45
+ - User clicks the link in their email
46
+ - System verifies the token is valid and not expired
47
+ - User is automatically authenticated
48
+ - JWT token is generated for the session
49
+
50
+ 2. **Security Measures**:
51
+ - IP address validation (optional)
52
+ - Token expiration
53
+ - One-time use tokens (configurable)
54
+ - Automatic blocking after failed attempts
55
+
56
+ ## Configuration
57
+
58
+ Configure the plugin through **Settings > Magic Link** in the Strapi admin panel:
59
+
60
+ ### General Settings
61
+
62
+ - **Enable Magic Link**: Turn the feature on/off
63
+ - **Create New Users**: Automatically create users if they don't exist
64
+ - **Token Stays Valid**: Configure one-time use or reusable tokens
65
+ - **Expiration Period**: Set how long tokens remain valid
66
+ - **Token Length**: Configure the security level of tokens
67
+
68
+ ### Authentication Settings
69
+
70
+ - **Default Role**: Select which user role is assigned to new users
71
+ - **JWT Token Expiration**: Define how long JWT tokens remain valid (e.g., 30d, 24h)
72
+ - **Store Login Info**: Enable tracking of user agents and IP addresses
73
+ - **Remember Me**: Allow users to stay logged in for extended periods
74
+
75
+ ### Email Settings
76
+
77
+ - **Sender Information**: Configure the email sender details (name, email)
78
+ - **Reply-To Address**: Set a reply-to email address for support inquiries
79
+ - **Email Subject**: Customize the subject line of the magic link emails
80
+ - **Email Templates**: Customize HTML and text templates
81
+ - **Email Designer Integration**: Use with Email Designer 5 if installed
82
+
83
+ ## Dashboard & Admin Interface
84
+
85
+ Magic Link provides a comprehensive admin interface with several key sections:
86
+
87
+ ### Dashboard Overview
88
+
89
+ - **Security Score**: Dynamic score (0-100) showing your current security configuration
90
+ - **Active Tokens**: Count and management of currently active tokens
91
+ - **Token Usage**: Metrics on token creation and usage patterns
92
+ - **Users Using Magic Link**: Number of unique users authenticating via magic links
93
+ - **Tokens About to Expire**: Warning system for tokens expiring soon
94
+
95
+ ### Token Management
96
+
97
+ - **Token List View**: Sortable and filterable list of all tokens
98
+ - **Token Status Indicators**: Visual indicators showing token status (active, expired, used)
99
+ - **Token Details**: Inspect complete token information including:
100
+ - Creation and expiration dates
101
+ - IP address and user agent information
102
+ - Usage history and context data
103
+ - **Bulk Actions**: Select multiple tokens for batch operations
104
+ - **Search & Filter**: Find tokens by email, status, or creation date
105
+ - **Token Operations**:
106
+ - Block/deactivate tokens
107
+ - Extend token expiration
108
+ - Delete tokens
109
+
110
+ ### JWT Session Management
111
+
112
+ - **Active Sessions**: Monitor all active JWT sessions across your application
113
+ - **Session Revocation**: Ability to revoke any active session immediately
114
+ - **Session Details**: View complete session information including:
115
+ - User details
116
+ - Creation and expiration time
117
+ - Last activity
118
+ - IP address and device information
119
+
120
+ ### Security Features
121
+
122
+ - **IP Ban Management**: View and manage banned IP addresses
123
+ - **IP Ban Controls**: Ban suspicious IPs and view associated tokens
124
+ - **Security Audit**: Track login attempts and security events
125
+ - **System Status**: Monitor the health of the plugin and its dependencies
126
+
127
+ ### Token Creation Interface
128
+
129
+ - **Manual Token Creation**: Generate tokens for specific users
130
+ - **Email Control**: Option to send or not send the email with the token
131
+ - **Context Injection**: Add custom JSON context data to tokens
132
+ - **Email Validation**: Verify email existence before token creation
133
+
134
+ ## Admin Features
135
+
136
+ ### Token Management
137
+
138
+ - View all active/inactive tokens
139
+ - Block or activate individual tokens
140
+ - Delete expired tokens
141
+ - See token usage statistics
142
+
143
+ ### Security Dashboard
144
+
145
+ - Security score based on your configuration
146
+ - IP banning for suspicious activity
147
+ - JWT session monitoring and management
148
+ - Token expiration warnings
149
+
150
+ ## API Endpoints
151
+
152
+ - `POST /api/magic-link/send` - Generate and send a magic link
153
+ - `GET /api/magic-link/login?loginToken=xxx` - Authenticate with token
154
+ - `GET /api/magic-link/tokens` - List tokens (admin only)
155
+ - `GET /api/magic-link/jwt-sessions` - List active sessions (admin only)
156
+ - `POST /api/magic-link/tokens/:id/block` - Block a specific token
157
+ - `POST /api/magic-link/ban-ip` - Ban an IP address
158
+
159
+ ## Frontend Implementation
160
+
161
+ ```javascript
162
+ // Request a magic link
163
+ const requestLogin = async (email) => {
164
+ try {
165
+ await axios.post("/api/magic-link/send", { email });
166
+ // Show success message
167
+ } catch (error) {
168
+ // Handle error
169
+ }
170
+ };
171
+
172
+ // Verify token on the callback page
173
+ const verifyToken = async () => {
174
+ const token = new URLSearchParams(window.location.search).get("loginToken");
175
+ if (token) {
176
+ try {
177
+ const response = await axios.get(
178
+ `/api/magic-link/login?loginToken=${token}`
179
+ );
180
+ // Store JWT and redirect user
181
+ localStorage.setItem("token", response.data.jwt);
182
+ window.location.href = "/dashboard";
183
+ } catch (error) {
184
+ // Handle invalid token
185
+ }
186
+ }
187
+ };
188
+ ```
189
+
190
+ ## Context Data
191
+
192
+ You can include additional context when sending a magic link:
193
+
194
+ ```javascript
195
+ await axios.post("/api/magic-link/send", {
196
+ email: "user@example.com",
197
+ context: {
198
+ redirectUrl: "/dashboard",
199
+ source: "registration",
200
+ },
201
+ });
202
+ ```
203
+
204
+ ## Security Best Practices
205
+
206
+ - Set reasonable token expiration times
207
+ - Use one-time tokens for sensitive operations
208
+ - Regularly monitor the security dashboard
209
+ - Ban suspicious IP addresses promptly
210
+
211
+ ## Troubleshooting
212
+
213
+ | Issue | Solution |
214
+ | ---------------------- | ---------------------------------------------- |
215
+ | Emails not sending | Check your Strapi email provider configuration |
216
+ | Token validation fails | Verify token expiration settings |
217
+ | User not found errors | Check "Create New Users" setting |
218
+
219
+ ## License
220
+
221
+ [MIT](LICENSE)
222
+
223
+ ## Development & Contributing
224
+
225
+ ### Release Process
226
+
227
+ This project uses [semantic-release](https://github.com/semantic-release/semantic-release) to automate version management and package publishing.
228
+
229
+ To create a new release:
230
+
231
+ ```bash
232
+ npx semantic-release
233
+ ```
234
+
235
+ This will automatically:
236
+
237
+ 1. Analyze commit messages since the last release
238
+ 2. Determine the appropriate version bump (major, minor, or patch)
239
+ 3. Generate release notes
240
+ 4. Update the version in package.json
241
+ 5. Create a new Git tag
242
+ 6. Publish the package to npm
243
+
244
+ ### Commit Guidelines
245
+
246
+ To ensure semantic-release can properly determine the next version number, please follow these commit message conventions:
247
+
248
+ #### Commit Message Format
249
+
250
+ Each commit message consists of a **header**, an optional **body**, and an optional **footer**:
251
+
252
+ ```
253
+ <type>(<scope>): <subject>
254
+ <BLANK LINE>
255
+ <body>
256
+ <BLANK LINE>
257
+ <footer>
258
+ ```
259
+
260
+ The **header** is mandatory and must conform to the following format:
261
+
262
+ - **type**: What type of change this commit is making. Must be one of:
263
+
264
+ - `feat`: A new feature (triggers a minor release)
265
+ - `fix`: A bug fix (triggers a patch release)
266
+ - `docs`: Documentation changes only
267
+ - `style`: Changes that don't affect code functionality (formatting, etc.)
268
+ - `refactor`: Code changes that neither fix a bug nor add a feature
269
+ - `perf`: Performance improvements
270
+ - `test`: Adding or updating tests
271
+ - `chore`: Changes to build process or auxiliary tools
272
+ - `ci`: Changes to CI configuration files and scripts
273
+
274
+ - **scope**: Optional, can be anything specifying the place of the commit change (e.g., `admin`, `api`, `auth`)
275
+
276
+ - **subject**: Brief description of the change
277
+
278
+ #### Breaking Changes
279
+
280
+ For breaking changes, add `BREAKING CHANGE:` in the footer of the commit message or append a `!` after the type/scope:
281
+
282
+ ```
283
+ feat(api)!: completely restructure API endpoints
284
+
285
+ BREAKING CHANGE: The API endpoints have been completely restructured.
286
+ ```
287
+
288
+ This will trigger a major version bump.
289
+
290
+ #### Examples
291
+
292
+ ```
293
+ feat(auth): add support for multiple roles
294
+
295
+ fix(email): correct template rendering issue
296
+
297
+ docs: update API documentation
298
+
299
+ chore(deps): update dependencies
300
+
301
+ fix!: critical security vulnerability in token validation
302
+ ```
303
+
304
+ ### Branching Strategy
305
+
306
+ - `main`: Production-ready code
307
+ - `develop`: Integration branch for features
308
+ - `feature/*`: New features
309
+ - `fix/*`: Bug fixes
310
+ - `docs/*`: Documentation changes
311
+
312
+ When creating PRs, always merge feature branches into `develop` first. The `develop` branch is periodically merged into `main` for releases.
313
+
314
+ ---
315
+
316
+ ## Support
317
+
318
+ If you encounter issues or have questions, please check the admin documentation or open an issue.
@@ -0,0 +1,10 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es6",
4
+ "jsx": "react",
5
+ "module": "esnext",
6
+ "allowSyntheticDefaultImports": true,
7
+ "esModuleInterop": true
8
+ },
9
+ "include": ["./src/**/*.js", "./src/**/*.jsx"]
10
+ }
@@ -0,0 +1,20 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import pluginId from '../../pluginId';
4
+
5
+ const Initializer = ({ setPlugin }) => {
6
+ const ref = useRef();
7
+ ref.current = setPlugin;
8
+
9
+ useEffect(() => {
10
+ ref.current(pluginId);
11
+ }, []);
12
+
13
+ return null;
14
+ };
15
+
16
+ Initializer.propTypes = {
17
+ setPlugin: PropTypes.func.isRequired,
18
+ };
19
+
20
+ export default Initializer;
@@ -0,0 +1,18 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ import { PLUGIN_ID } from '../pluginId';
4
+
5
+ /**
6
+ * @type {import('react').FC<{ setPlugin: (id: string) => void }>}
7
+ */
8
+ const Initializer = ({ setPlugin }) => {
9
+ const ref = useRef(setPlugin);
10
+
11
+ useEffect(() => {
12
+ ref.current(PLUGIN_ID);
13
+ }, []);
14
+
15
+ return null;
16
+ };
17
+
18
+ export { Initializer };
@@ -0,0 +1,27 @@
1
+ import React from 'react';
2
+
3
+ // This helper ensures the component module structure that Strapi v5 expects
4
+ // for lazy-loaded components
5
+ const LazyComponentLoader = (importComponent) => {
6
+ return React.lazy(() => {
7
+ return importComponent().then(module => {
8
+ // If the module has a default export, use it
9
+ if (module.default) {
10
+ return { default: module.default };
11
+ }
12
+
13
+ // Otherwise, use the named export if available
14
+ const firstExport = Object.keys(module)[0];
15
+ if (firstExport && module[firstExport]) {
16
+ return { default: module[firstExport] };
17
+ }
18
+
19
+ // If no suitable export is found, return a fallback component
20
+ return {
21
+ default: () => <div>Component loading error</div>
22
+ };
23
+ });
24
+ });
25
+ };
26
+
27
+ export default LazyComponentLoader;
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import { Key } from '@strapi/icons';
3
+
4
+ export const PluginIcon = () => <Key />;
5
+
6
+ export default PluginIcon;
@@ -0,0 +1,5 @@
1
+ import { PuzzlePiece } from '@strapi/icons';
2
+
3
+ const PluginIcon = () => <PuzzlePiece />;
4
+
5
+ export { PluginIcon };
@@ -0,0 +1,101 @@
1
+ import { prefixPluginTranslations } from '@strapi/strapi/admin';
2
+ import pluginPkg from '../../package.json';
3
+ import pluginId from './pluginId';
4
+ import Initializer from './components/Initializer';
5
+ import PluginIcon from './components/PluginIcon';
6
+ import pluginPermissions from './permissions';
7
+ import getTrad from './utils/getTrad';
8
+
9
+ const name = pluginPkg.strapi.name;
10
+
11
+ export default {
12
+ register(app) {
13
+ app.addMenuLink({
14
+ to: `/plugins/${pluginId}`,
15
+ icon: PluginIcon,
16
+ intlLabel: {
17
+ id: `${pluginId}.plugin.name`,
18
+ defaultMessage: name,
19
+ },
20
+ Component: () => import('./pages/HomePage').then(module => ({
21
+ default: module.default
22
+ }))
23
+ });
24
+
25
+ app.addMenuLink({
26
+ to: `/plugins/${pluginId}/tokens`,
27
+ icon: PluginIcon,
28
+ intlLabel: {
29
+ id: getTrad('tokens.title'),
30
+ defaultMessage: 'Magic Link Tokens',
31
+ },
32
+ Component: () => import('./pages/Tokens').then(module => ({
33
+ default: module.default
34
+ }))
35
+ });
36
+
37
+ app.createSettingSection(
38
+ {
39
+ id: pluginId,
40
+ intlLabel: {
41
+ id: getTrad('Header.Settings'),
42
+ defaultMessage: 'Magic Link',
43
+ },
44
+ },
45
+ [
46
+ {
47
+ intlLabel: {
48
+ id: getTrad('Form.title.Settings'),
49
+ defaultMessage: 'Settings',
50
+ },
51
+ id: 'magic-link-settings',
52
+ to: `/settings/${pluginId}`,
53
+ Component: () => import('./pages/Settings').then(module => ({
54
+ default: module.default
55
+ })),
56
+ permissions: pluginPermissions.readSettings,
57
+ },
58
+ ]
59
+ );
60
+
61
+ app.registerPlugin({
62
+ id: pluginId,
63
+ initializer: Initializer,
64
+ isReady: false,
65
+ name,
66
+ });
67
+ },
68
+
69
+ bootstrap() {
70
+ // Nothing to do here
71
+ },
72
+
73
+ async registerTrads({ locales }) {
74
+ const importedTrads = await Promise.all(
75
+ locales.map(locale => {
76
+ try {
77
+ return import(`./translations/${locale}.json`)
78
+ .then(({ default: data }) => {
79
+ return {
80
+ data: prefixPluginTranslations(data, pluginId),
81
+ locale,
82
+ };
83
+ })
84
+ .catch(() => {
85
+ return {
86
+ data: {},
87
+ locale,
88
+ };
89
+ });
90
+ } catch (error) {
91
+ return {
92
+ data: {},
93
+ locale,
94
+ };
95
+ }
96
+ })
97
+ );
98
+
99
+ return Promise.resolve(importedTrads);
100
+ },
101
+ };
@@ -0,0 +1,50 @@
1
+ import React from 'react';
2
+ import { Main, Box, Typography } from '@strapi/design-system';
3
+ import pluginId from '../../pluginId';
4
+ import HomePage from '../HomePage';
5
+ import TokensPage from '../Tokens';
6
+
7
+ const App = () => {
8
+ console.log("Magic Link App-Root-Komponente wird geladen");
9
+
10
+ // Ermittle den aktuellen Pfad, um zu sehen, welche Route angezeigt werden soll
11
+ const currentPath = window.location.pathname;
12
+ console.log("Aktueller Pfad:", currentPath);
13
+ console.log("Plugin ID:", pluginId);
14
+
15
+ // Einfachere Bedingungen, die nur prüfen, ob der Pfad bestimmte Segmente enthält
16
+ const isTokensPage = currentPath.includes(`/plugins/${pluginId}/tokens`) ||
17
+ currentPath.includes(`/plugins/magic-link/tokens`);
18
+ const isHomePage = (currentPath.includes(`/plugins/${pluginId}`) ||
19
+ currentPath.includes(`/plugins/magic-link`)) && !isTokensPage;
20
+
21
+ console.log("isTokensPage:", isTokensPage);
22
+ console.log("isHomePage:", isHomePage);
23
+
24
+ let content;
25
+ if (isTokensPage) {
26
+ console.log("Zeige TokensPage an");
27
+ content = <TokensPage />;
28
+ } else if (isHomePage) {
29
+ console.log("Zeige HomePage an");
30
+ content = <HomePage />;
31
+ } else {
32
+ console.log("Zeige 404 Seite an");
33
+ content = (
34
+ <Box padding={8} background="neutral100">
35
+ <Typography variant="alpha">
36
+ 404 - Route nicht gefunden: {currentPath}
37
+ </Typography>
38
+ </Box>
39
+ );
40
+ }
41
+
42
+ return (
43
+ <Main>
44
+ {content}
45
+ </Main>
46
+ );
47
+ };
48
+
49
+ export { App };
50
+ export default App;
@@ -0,0 +1,15 @@
1
+ import { Page } from '@strapi/strapi/admin';
2
+ import { Routes, Route } from 'react-router-dom';
3
+
4
+ import { HomePage } from './HomePage';
5
+
6
+ const App = () => {
7
+ return (
8
+ <Routes>
9
+ <Route index element={<HomePage />} />
10
+ <Route path="*" element={<Page.Error />} />
11
+ </Routes>
12
+ );
13
+ };
14
+
15
+ export { App };
@@ -0,0 +1,2 @@
1
+ // Vereinfachter Export
2
+ export { default } from '../HomePage.jsx';