curl -X POST "http://localhost:8080/admin/invite" \
-H "Authorization: Bearer service_role_key_here" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"data": {
"first_name": "John",
"last_name": "Doe",
"role": "team_member"
},
"redirect_to": "https://yourapp.com/welcome"
}'
{
"user": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"aud": "authenticated",
"role": "authenticated",
"email": "[email protected]",
"phone": null,
"email_confirmed_at": null,
"phone_confirmed_at": null,
"invited_at": "2023-01-01T00:00:00Z",
"confirmation_sent_at": "2023-01-01T00:00:00Z",
"app_metadata": {
"provider": "email",
"providers": ["email"]
},
"user_metadata": {
"first_name": "John",
"last_name": "Doe",
"role": "team_member"
},
"created_at": "2023-01-01T00:00:00Z",
"updated_at": "2023-01-01T00:00:00Z"
},
"message_id": "msg_1234567890abcdef"
}
Send user invitation via email (admin only)
curl -X POST "http://localhost:8080/admin/invite" \
-H "Authorization: Bearer service_role_key_here" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"data": {
"first_name": "John",
"last_name": "Doe",
"role": "team_member"
},
"redirect_to": "https://yourapp.com/welcome"
}'
{
"user": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"aud": "authenticated",
"role": "authenticated",
"email": "[email protected]",
"phone": null,
"email_confirmed_at": null,
"phone_confirmed_at": null,
"invited_at": "2023-01-01T00:00:00Z",
"confirmation_sent_at": "2023-01-01T00:00:00Z",
"app_metadata": {
"provider": "email",
"providers": ["email"]
},
"user_metadata": {
"first_name": "John",
"last_name": "Doe",
"role": "team_member"
},
"created_at": "2023-01-01T00:00:00Z",
"updated_at": "2023-01-01T00:00:00Z"
},
"message_id": "msg_1234567890abcdef"
}
curl -X POST "http://localhost:8080/admin/invite" \
-H "Authorization: Bearer service_role_key_here" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"data": {
"first_name": "John",
"last_name": "Doe",
"role": "team_member"
},
"redirect_to": "https://yourapp.com/welcome"
}'
first_name - User’s first namelast_name - User’s last namerole - User’s role in the systemteam_id - Team or organization IDdepartment - User’s departmentcaptcha_token - Captcha token for verificationdata - Additional app metadata{
"user": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"aud": "authenticated",
"role": "authenticated",
"email": "[email protected]",
"phone": null,
"email_confirmed_at": null,
"phone_confirmed_at": null,
"invited_at": "2023-01-01T00:00:00Z",
"confirmation_sent_at": "2023-01-01T00:00:00Z",
"app_metadata": {
"provider": "email",
"providers": ["email"]
},
"user_metadata": {
"first_name": "John",
"last_name": "Doe",
"role": "team_member"
},
"created_at": "2023-01-01T00:00:00Z",
"updated_at": "2023-01-01T00:00:00Z"
},
"message_id": "msg_1234567890abcdef"
}
{
"code": 400,
"msg": "Invalid email format",
"details": "Please provide a valid email address"
}
<!DOCTYPE html>
<html>
<head>
<title>You're Invited to Join Strike</title>
</head>
<body>
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<h1>You're Invited!</h1>
<p>Hi {{.FirstName}},</p>
<p>You've been invited to join Strike as a {{.Role}}.</p>
<a href="{{.InvitationURL}}"
style="background-color: #0D9373; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">
Accept Invitation
</a>
<p>Or copy and paste this link into your browser:</p>
<p>{{.InvitationURL}}</p>
<p>This invitation will expire in 7 days.</p>
<p>Welcome to the team!</p>
<p>Best regards,<br>The Strike Team</p>
</div>
</body>
</html>
import { useState } from 'react';
function AdminInviteForm({ onInviteSent }) {
const [formData, setFormData] = useState({
email: '',
firstName: '',
lastName: '',
role: 'user',
teamId: '',
department: ''
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setError('');
setSuccess(false);
try {
const response = await fetch('/api/admin/invite', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.REACT_APP_SERVICE_ROLE_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: formData.email,
data: {
first_name: formData.firstName,
last_name: formData.lastName,
role: formData.role,
team_id: formData.teamId,
department: formData.department
},
redirect_to: 'https://yourapp.com/welcome'
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.msg || 'Failed to send invitation');
}
const result = await response.json();
setSuccess(true);
onInviteSent?.(result);
// Reset form
setFormData({
email: '',
firstName: '',
lastName: '',
role: 'user',
teamId: '',
department: ''
});
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
const handleInputChange = (field, value) => {
setFormData(prev => ({
...prev,
[field]: value
}));
};
return (
<div className="admin-invite-form">
<h2>Invite New User</h2>
{error && (
<div className="error-message">
{error}
</div>
)}
{success && (
<div className="success-message">
✅ Invitation sent successfully!
</div>
)}
<form onSubmit={handleSubmit} className="invite-form">
<div className="form-row">
<div className="form-group">
<label htmlFor="firstName">First Name</label>
<input
type="text"
id="firstName"
value={formData.firstName}
onChange={(e) => handleInputChange('firstName', e.target.value)}
required
/>
</div>
<div className="form-group">
<label htmlFor="lastName">Last Name</label>
<input
type="text"
id="lastName"
value={formData.lastName}
onChange={(e) => handleInputChange('lastName', e.target.value)}
required
/>
</div>
</div>
<div className="form-group">
<label htmlFor="email">Email Address</label>
<input
type="email"
id="email"
value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
placeholder="[email protected]"
required
/>
</div>
<div className="form-row">
<div className="form-group">
<label htmlFor="role">Role</label>
<select
id="role"
value={formData.role}
onChange={(e) => handleInputChange('role', e.target.value)}
>
<option value="user">User</option>
<option value="team_member">Team Member</option>
<option value="team_lead">Team Lead</option>
<option value="admin">Admin</option>
</select>
</div>
<div className="form-group">
<label htmlFor="department">Department</label>
<input
type="text"
id="department"
value={formData.department}
onChange={(e) => handleInputChange('department', e.target.value)}
placeholder="Engineering"
/>
</div>
</div>
<div className="form-group">
<label htmlFor="teamId">Team ID (optional)</label>
<input
type="text"
id="teamId"
value={formData.teamId}
onChange={(e) => handleInputChange('teamId', e.target.value)}
placeholder="team_123"
/>
</div>
<button type="submit" disabled={loading}>
{loading ? 'Sending Invitation...' : 'Send Invitation'}
</button>
</form>
</div>
);
}
export default AdminInviteForm;
import { useState } from 'react';
function BulkInviteForm() {
const [csvData, setCsvData] = useState('');
const [loading, setLoading] = useState(false);
const [results, setResults] = useState([]);
const handleBulkInvite = async (e) => {
e.preventDefault();
setLoading(true);
setResults([]);
// Parse CSV data
const lines = csvData.trim().split('\n');
const headers = lines[0].split(',').map(h => h.trim());
const invitations = [];
for (let i = 1; i < lines.length; i++) {
const values = lines[i].split(',').map(v => v.trim());
const invitation = {};
headers.forEach((header, index) => {
invitation[header] = values[index];
});
invitations.push(invitation);
}
// Send invitations
const inviteResults = [];
for (const invitation of invitations) {
try {
const response = await fetch('/api/admin/invite', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.REACT_APP_SERVICE_ROLE_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: invitation.email,
data: {
first_name: invitation.first_name,
last_name: invitation.last_name,
role: invitation.role || 'user',
department: invitation.department
},
redirect_to: 'https://yourapp.com/welcome'
}),
});
if (response.ok) {
const result = await response.json();
inviteResults.push({
email: invitation.email,
success: true,
userId: result.user.id
});
} else {
const errorData = await response.json();
inviteResults.push({
email: invitation.email,
success: false,
error: errorData.msg
});
}
} catch (err) {
inviteResults.push({
email: invitation.email,
success: false,
error: err.message
});
}
}
setResults(inviteResults);
setLoading(false);
};
return (
<div className="bulk-invite-form">
<h2>Bulk User Invitation</h2>
<div className="csv-instructions">
<h3>CSV Format</h3>
<p>Use the following format for your CSV data:</p>
<pre>
email,first_name,last_name,role,department
[email protected],John,Doe,team_member,Engineering
[email protected],Jane,Smith,admin,Marketing
</pre>
</div>
<form onSubmit={handleBulkInvite} className="bulk-form">
<div className="form-group">
<label htmlFor="csvData">CSV Data</label>
<textarea
id="csvData"
value={csvData}
onChange={(e) => setCsvData(e.target.value)}
placeholder="email,first_name,last_name,role,department [email protected],John,Doe,team_member,Engineering"
rows={10}
required
/>
</div>
<button type="submit" disabled={loading}>
{loading ? 'Sending Invitations...' : 'Send All Invitations'}
</button>
</form>
{results.length > 0 && (
<div className="results">
<h3>Invitation Results</h3>
<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'}`}>
<span className="email">{result.email}</span>
{result.success ? (
<span className="status">✅ Invited (ID: {result.userId})</span>
) : (
<span className="error">❌ {result.error}</span>
)}
</div>
))}
</div>
</div>
)}
</div>
);
}
export default BulkInviteForm;
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/invite', [
verifyServiceRole,
body('email').isEmail().normalizeEmail(),
body('data').optional().isObject(),
body('redirect_to').optional().isURL()
], 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 { email, data, redirect_to, options } = req.body;
// Call Strike Auth Service
const response = await fetch(`${process.env.AUTH_SERVICE_URL}/admin/invite`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.SERVICE_ROLE_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email,
data,
redirect_to: redirect_to || `${process.env.APP_URL}/welcome`,
options
}),
});
const result = await response.json();
if (!response.ok) {
return res.status(response.status).json(result);
}
// Log invitation
console.log(`User invited: ${email}, role: ${data?.role || 'user'}`);
res.json(result);
} catch (error) {
console.error('Invitation error:', error);
res.status(500).json({
code: 500,
msg: 'Internal server error',
details: 'Please try again later'
});
}
});
module.exports = router;
{
"email": "[email protected]",
"data": {
"first_name": "Alex",
"last_name": "Johnson",
"role": "developer",
"team_id": "engineering_team",
"department": "Engineering",
"start_date": "2023-02-01"
},
"redirect_to": "https://yourapp.com/onboarding"
}
{
"email": "[email protected]",
"data": {
"first_name": "Sarah",
"last_name": "Wilson",
"role": "client",
"company": "Wilson Corp",
"access_level": "read_only"
},
"redirect_to": "https://yourapp.com/client-portal"
}
{
"email": "[email protected]",
"data": {
"first_name": "Michael",
"last_name": "Brown",
"role": "admin",
"permissions": ["user_management", "system_config"],
"department": "IT"
},
"redirect_to": "https://yourapp.com/admin-setup"
}
Invitation Management
User Experience
Security
describe('POST /admin/invite', () => {
test('should send invitation to new user', async () => {
const inviteData = {
email: '[email protected]',
data: {
first_name: 'John',
last_name: 'Doe',
role: 'team_member'
}
};
const response = await request(app)
.post('/admin/invite')
.set('Authorization', 'Bearer valid_service_role_key')
.send(inviteData)
.expect(200);
expect(response.body.user.email).toBe('[email protected]');
expect(response.body.user.user_metadata.first_name).toBe('John');
expect(response.body.message_id).toBeDefined();
});
test('should reject invitation for existing user', async () => {
await request(app)
.post('/admin/invite')
.set('Authorization', 'Bearer valid_service_role_key')
.send({
email: '[email protected]',
data: { first_name: 'Test' }
})
.expect(409);
});
test('should reject invalid service role', async () => {
await request(app)
.post('/admin/invite')
.set('Authorization', 'Bearer invalid_key')
.send({
email: '[email protected]',
data: { first_name: 'Test' }
})
.expect(401);
});
});