Skip to main content
POST
/
admin
/
generate_link
curl -X POST "http://localhost:8080/admin/generate_link" \
  -H "Authorization: Bearer service_role_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "magiclink",
    "email": "user@example.com",
    "redirect_to": "https://yourapp.com/dashboard"
  }'
{
  "action_link": "http://localhost:8080/verify?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...&type=magiclink&redirect_to=https%3A//yourapp.com/dashboard",
  "email_otp": "123456",
  "hashed_token": "sha256_hash_of_token",
  "verification_type": "magiclink",
  "redirect_to": "https://yourapp.com/dashboard"
}
Generate secure authentication links for users. This endpoint allows administrators to create magic links, recovery links, or invitation links that can be sent to users via custom channels.
This endpoint requires service role authentication. Only use the service role key on your backend servers, never in client-side code.
curl -X POST "http://localhost:8080/admin/generate_link" \
  -H "Authorization: Bearer service_role_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "magiclink",
    "email": "user@example.com",
    "redirect_to": "https://yourapp.com/dashboard"
  }'

Request Body

type
string
required
The type of link to generate.Options:
  • signup - Email confirmation link for new users
  • magiclink - Passwordless authentication link
  • recovery - Password recovery link
  • invite - User invitation link
  • email_change - Email change confirmation link
email
string
required
Email address of the user for whom to generate the link.
redirect_to
string
URL to redirect to after the link is used. If not provided, uses the default redirect URL.
data
object
Additional data to include with the link (for invitations or user creation).
password
string
Password for the user (required for some link types like recovery).

Response

The generated authentication link that can be sent to the user
email_otp
string
One-time password for email verification (when applicable)
hashed_token
string
Hashed version of the token for verification purposes
verification_type
string
Type of verification this link performs
redirect_to
string
The redirect URL that will be used after verification
{
  "action_link": "http://localhost:8080/verify?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...&type=magiclink&redirect_to=https%3A//yourapp.com/dashboard",
  "email_otp": "123456",
  "hashed_token": "sha256_hash_of_token",
  "verification_type": "magiclink",
  "redirect_to": "https://yourapp.com/dashboard"
}

Error Responses

{
  "code": 400,
  "msg": "Invalid link type",
  "details": "Link type must be one of: signup, magiclink, recovery, invite, email_change"
}
Generate a passwordless authentication link:
{
  "type": "magiclink",
  "email": "user@example.com",
  "redirect_to": "https://yourapp.com/dashboard"
}
Generate a password recovery link:
{
  "type": "recovery",
  "email": "user@example.com",
  "redirect_to": "https://yourapp.com/reset-password"
}
Generate an email confirmation link for new users:
{
  "type": "signup",
  "email": "newuser@example.com",
  "redirect_to": "https://yourapp.com/welcome"
}
Generate an invitation link for new users:
{
  "type": "invite",
  "email": "invite@example.com",
  "data": {
    "first_name": "John",
    "last_name": "Doe",
    "role": "team_member"
  },
  "redirect_to": "https://yourapp.com/setup"
}

Email Change Confirmation

Generate a link to confirm email address changes:
{
  "type": "email_change",
  "email": "newemail@example.com",
  "redirect_to": "https://yourapp.com/profile"
}

Implementation Examples

import { useState } from 'react';

function AdminLinkGenerator() {
  const [linkType, setLinkType] = useState('magiclink');
  const [email, setEmail] = useState('');
  const [redirectTo, setRedirectTo] = useState('');
  const [loading, setLoading] = useState(false);
  const [generatedLink, setGeneratedLink] = useState('');
  const [error, setError] = useState('');

  const handleGenerate = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError('');
    setGeneratedLink('');

    try {
      const response = await fetch('/api/admin/generate_link', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${process.env.REACT_APP_SERVICE_ROLE_KEY}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          type: linkType,
          email,
          redirect_to: redirectTo || undefined
        }),
      });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.msg || 'Failed to generate link');
      }

      const linkData = await response.json();
      setGeneratedLink(linkData.action_link);
      
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  const copyToClipboard = async () => {
    try {
      await navigator.clipboard.writeText(generatedLink);
      alert('Link copied to clipboard!');
    } catch (err) {
      console.error('Failed to copy link:', err);
    }
  };

  return (
    <div className="admin-link-generator">
      <h2>Generate Authentication Link</h2>
      
      {error && (
        <div className="error-message">
          {error}
        </div>
      )}

      <form onSubmit={handleGenerate} className="link-form">
        <div className="form-group">
          <label htmlFor="linkType">Link Type</label>
          <select
            id="linkType"
            value={linkType}
            onChange={(e) => setLinkType(e.target.value)}
            required
          >
            <option value="magiclink">Magic Link</option>
            <option value="recovery">Password Recovery</option>
            <option value="signup">Email Confirmation</option>
            <option value="invite">User Invitation</option>
            <option value="email_change">Email Change</option>
          </select>
        </div>

        <div className="form-group">
          <label htmlFor="email">Email Address</label>
          <input
            type="email"
            id="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            placeholder="user@example.com"
            required
          />
        </div>

        <div className="form-group">
          <label htmlFor="redirectTo">Redirect URL (optional)</label>
          <input
            type="url"
            id="redirectTo"
            value={redirectTo}
            onChange={(e) => setRedirectTo(e.target.value)}
            placeholder="https://yourapp.com/dashboard"
          />
        </div>

        <button type="submit" disabled={loading}>
          {loading ? 'Generating...' : 'Generate Link'}
        </button>
      </form>

      {generatedLink && (
        <div className="generated-link">
          <h3>Generated Link</h3>
          <div className="link-container">
            <input
              type="text"
              value={generatedLink}
              readOnly
              className="link-input"
            />
            <button onClick={copyToClipboard} className="copy-button">
              Copy
            </button>
          </div>
          <p className="link-info">
            This link will expire in 1 hour and can only be used once.
          </p>
        </div>
      )}
    </div>
  );
}

export default AdminLinkGenerator;
import { useState } from 'react';

function BulkLinkGenerator() {
  const [emails, setEmails] = useState('');
  const [linkType, setLinkType] = useState('invite');
  const [redirectTo, setRedirectTo] = useState('');
  const [loading, setLoading] = useState(false);
  const [results, setResults] = useState([]);

  const handleBulkGenerate = async (e) => {
    e.preventDefault();
    setLoading(true);
    setResults([]);

    const emailList = emails.split('\n').filter(email => email.trim());
    const generatedLinks = [];

    for (const email of emailList) {
      try {
        const response = await fetch('/api/admin/generate_link', {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${process.env.REACT_APP_SERVICE_ROLE_KEY}`,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            type: linkType,
            email: email.trim(),
            redirect_to: redirectTo || undefined
          }),
        });

        if (response.ok) {
          const linkData = await response.json();
          generatedLinks.push({
            email: email.trim(),
            success: true,
            link: linkData.action_link
          });
        } else {
          const errorData = await response.json();
          generatedLinks.push({
            email: email.trim(),
            success: false,
            error: errorData.msg
          });
        }
      } catch (err) {
        generatedLinks.push({
          email: email.trim(),
          success: false,
          error: err.message
        });
      }
    }

    setResults(generatedLinks);
    setLoading(false);
  };

  const exportResults = () => {
    const csvContent = [
      'Email,Status,Link,Error',
      ...results.map(result => 
        `${result.email},${result.success ? 'Success' : 'Failed'},${result.link || ''},${result.error || ''}`
      )
    ].join('\n');

    const blob = new Blob([csvContent], { type: 'text/csv' });
    const url = window.URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `bulk_links_${Date.now()}.csv`;
    a.click();
    window.URL.revokeObjectURL(url);
  };

  return (
    <div className="bulk-link-generator">
      <h2>Bulk Link Generation</h2>
      
      <form onSubmit={handleBulkGenerate} className="bulk-form">
        <div className="form-group">
          <label htmlFor="emails">Email Addresses (one per line)</label>
          <textarea
            id="emails"
            value={emails}
            onChange={(e) => setEmails(e.target.value)}
            placeholder="user1@example.com&#10;user2@example.com&#10;user3@example.com"
            rows={10}
            required
          />
        </div>

        <div className="form-group">
          <label htmlFor="linkType">Link Type</label>
          <select
            id="linkType"
            value={linkType}
            onChange={(e) => setLinkType(e.target.value)}
          >
            <option value="invite">User Invitation</option>
            <option value="magiclink">Magic Link</option>
            <option value="recovery">Password Recovery</option>
            <option value="signup">Email Confirmation</option>
          </select>
        </div>

        <div className="form-group">
          <label htmlFor="redirectTo">Redirect URL (optional)</label>
          <input
            type="url"
            id="redirectTo"
            value={redirectTo}
            onChange={(e) => setRedirectTo(e.target.value)}
            placeholder="https://yourapp.com/welcome"
          />
        </div>

        <button type="submit" disabled={loading}>
          {loading ? 'Generating Links...' : 'Generate All Links'}
        </button>
      </form>

      {results.length > 0 && (
        <div className="results">
          <div className="results-header">
            <h3>Results ({results.length} total)</h3>
            <button onClick={exportResults} className="export-button">
              Export CSV
            </button>
          </div>
          
          <div className="results-summary">
            <span className="success-count">
{results.filter(r => r.success).length} successful
            </span>
            <span className="error-count">
{results.filter(r => !r.success).length} failed
            </span>
          </div>

          <div className="results-list">
            {results.map((result, index) => (
              <div key={index} className={`result-item ${result.success ? 'success' : 'error'}`}>
                <div className="result-email">{result.email}</div>
                {result.success ? (
                  <div className="result-link">
                    <input type="text" value={result.link} readOnly />
                    <button onClick={() => navigator.clipboard.writeText(result.link)}>
                      Copy
                    </button>
                  </div>
                ) : (
                  <div className="result-error">{result.error}</div>
                )}
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

export default BulkLinkGenerator;

Node.js Backend Handler

const express = require('express');
const { body, validationResult } = require('express-validator');

const router = express.Router();

// Middleware to verify service role
const verifyServiceRole = (req, res, next) => {
  const authHeader = req.headers.authorization;
  const token = authHeader?.split(' ')[1];

  if (!token || token !== process.env.SERVICE_ROLE_KEY) {
    return res.status(401).json({
      code: 401,
      msg: 'Invalid service role key',
      details: 'Please provide a valid service role key'
    });
  }

  next();
};

router.post('/admin/generate_link', [
  verifyServiceRole,
  body('type').isIn(['signup', 'magiclink', 'recovery', 'invite', 'email_change']),
  body('email').isEmail().normalizeEmail(),
  body('redirect_to').optional().isURL(),
  body('data').optional().isObject()
], async (req, res) => {
  try {
    // Check validation errors
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({
        code: 400,
        msg: 'Invalid request data',
        details: errors.array()
      });
    }

    const { type, email, redirect_to, data, password } = req.body;

    // Call Strike Auth Service
    const response = await fetch(`${process.env.AUTH_SERVICE_URL}/admin/generate_link`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.SERVICE_ROLE_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        type,
        email,
        redirect_to,
        data,
        password
      }),
    });

    const result = await response.json();

    if (!response.ok) {
      return res.status(response.status).json(result);
    }

    // Log link generation
    console.log(`Link generated: ${type} for ${email}`);

    res.json(result);

  } catch (error) {
    console.error('Link generation error:', error);
    res.status(500).json({
      code: 500,
      msg: 'Internal server error',
      details: 'Please try again later'
    });
  }
});

module.exports = router;

Use Cases

Custom Email Templates

Generate links for use in custom email templates:
async function sendCustomInvitation(email, userData) {
  // Generate invitation link
  const linkResponse = await fetch('/api/admin/generate_link', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.SERVICE_ROLE_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      type: 'invite',
      email,
      data: userData,
      redirect_to: 'https://yourapp.com/welcome'
    }),
  });

  const { action_link } = await linkResponse.json();

  // Send custom email with the generated link
  await sendEmail({
    to: email,
    subject: 'Welcome to Strike!',
    template: 'custom-invitation',
    data: {
      inviteLink: action_link,
      userData
    }
  });
}

Integration with External Systems

Generate links for integration with CRM or marketing tools:
async function syncUserWithCRM(userData) {
  // Generate magic link for user
  const linkResponse = await fetch('/api/admin/generate_link', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.SERVICE_ROLE_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      type: 'magiclink',
      email: userData.email,
      redirect_to: 'https://yourapp.com/dashboard'
    }),
  });

  const { action_link } = await linkResponse.json();

  // Update CRM with login link
  await updateCRMContact(userData.email, {
    login_link: action_link,
    last_updated: new Date().toISOString()
  });
}

Security Considerations

  • Service Role Protection: Never expose service role keys in client-side code
  • Link Expiration: Generated links expire after 1 hour by default
  • Single Use: Links can only be used once for security
  • Rate Limiting: Implement rate limits for link generation
  • Audit Logging: Log all link generation activities

Best Practices

  • Validate all input parameters
  • Use HTTPS for all redirect URLs
  • Implement proper access controls
  • Monitor for suspicious link generation patterns
  • Provide clear instructions with generated links
  • Use descriptive redirect URLs
  • Handle expired links gracefully
  • Offer alternative authentication methods

Testing

Unit Tests

describe('POST /admin/generate_link', () => {
  test('should generate magic link for existing user', async () => {
    const response = await request(app)
      .post('/admin/generate_link')
      .set('Authorization', 'Bearer valid_service_role_key')
      .send({
        type: 'magiclink',
        email: 'existing@example.com',
        redirect_to: 'https://example.com/dashboard'
      })
      .expect(200);

    expect(response.body.action_link).toContain('verify?token=');
    expect(response.body.verification_type).toBe('magiclink');
  });

  test('should generate invitation link with user data', async () => {
    const response = await request(app)
      .post('/admin/generate_link')
      .set('Authorization', 'Bearer valid_service_role_key')
      .send({
        type: 'invite',
        email: 'new@example.com',
        data: { first_name: 'John', role: 'admin' }
      })
      .expect(200);

    expect(response.body.action_link).toBeDefined();
  });

  test('should reject invalid service role', async () => {
    await request(app)
      .post('/admin/generate_link')
      .set('Authorization', 'Bearer invalid_key')
      .send({
        type: 'magiclink',
        email: 'test@example.com'
      })
      .expect(401);
  });
});
I