Working with latest DB
This commit is contained in:
parent
ea87d97364
commit
9e842c6774
187
public/test.html
187
public/test.html
|
|
@ -41,6 +41,7 @@
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>3. Complete Profile (name + user_type)</h2>
|
<h2>3. Complete Profile (name + user_type)</h2>
|
||||||
|
<p><strong>⚠️ Requires OTP verification first (Step 2)</strong></p>
|
||||||
<p>Uses the <code>access_token</code> from step 2</p>
|
<p>Uses the <code>access_token</code> from step 2</p>
|
||||||
|
|
||||||
<label>Name</label>
|
<label>Name</label>
|
||||||
|
|
@ -53,21 +54,64 @@
|
||||||
<option value="service_provider">Service Provider</option>
|
<option value="service_provider">Service Provider</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<button id="updateProfileBtn">Update Profile</button>
|
<button id="updateProfileBtn" disabled>Update Profile (Verify OTP First)</button>
|
||||||
|
|
||||||
<pre id="updateProfileResult"></pre>
|
<pre id="updateProfileResult"></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>4. Get User Details</h2>
|
<h2>4. Get User Details</h2>
|
||||||
|
<p><strong>⚠️ Requires OTP verification first (Step 2)</strong></p>
|
||||||
<p>Authenticated GET request to fetch user profile with JWT token</p>
|
<p>Authenticated GET request to fetch user profile with JWT token</p>
|
||||||
<p>Shows: phone number, name, profile type, location, last login, and all saved locations</p>
|
<p>Shows: phone number, name, profile type, location, last login, and all saved locations</p>
|
||||||
|
|
||||||
<button id="getUserDetailsBtn">Get User Details</button>
|
<button id="getUserDetailsBtn" disabled>Get User Details (Verify OTP First)</button>
|
||||||
|
|
||||||
<pre id="getUserDetailsResult"></pre>
|
<pre id="getUserDetailsResult"></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>5. Add/Update Location</h2>
|
||||||
|
<p><strong>⚠️ Requires OTP verification first (Step 2)</strong></p>
|
||||||
|
<p>Save your current location or a custom address</p>
|
||||||
|
|
||||||
|
<label>Location Type</label>
|
||||||
|
<select id="locationType">
|
||||||
|
<option value="home">Home</option>
|
||||||
|
<option value="farm">Farm</option>
|
||||||
|
<option value="office">Office</option>
|
||||||
|
<option value="temporary_gps">Temporary GPS</option>
|
||||||
|
<option value="other_saved">Other Saved</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="useCurrentLocation" /> Use Current GPS Location
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>Country</label>
|
||||||
|
<input id="country" type="text" placeholder="India" />
|
||||||
|
|
||||||
|
<label>State</label>
|
||||||
|
<input id="state" type="text" placeholder="Maharashtra" />
|
||||||
|
|
||||||
|
<label>District</label>
|
||||||
|
<input id="district" type="text" placeholder="Mumbai" />
|
||||||
|
|
||||||
|
<label>City/Village</label>
|
||||||
|
<input id="cityVillage" type="text" placeholder="Mumbai" />
|
||||||
|
|
||||||
|
<label>Pincode</label>
|
||||||
|
<input id="pincode" type="text" placeholder="400001" />
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="isPrimaryLocation" /> Set as Primary Location
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button id="saveLocationBtn" disabled>Save Location (Verify OTP First)</button>
|
||||||
|
|
||||||
|
<pre id="saveLocationResult"></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let accessToken = null;
|
let accessToken = null;
|
||||||
let refreshToken = null;
|
let refreshToken = null;
|
||||||
|
|
@ -165,6 +209,25 @@
|
||||||
refreshToken = data.refresh_token;
|
refreshToken = data.refresh_token;
|
||||||
console.log('Access token set:', accessToken);
|
console.log('Access token set:', accessToken);
|
||||||
console.log('Refresh token set:', refreshToken);
|
console.log('Refresh token set:', refreshToken);
|
||||||
|
|
||||||
|
// Enable buttons that require authentication
|
||||||
|
const getUserDetailsBtn = document.getElementById('getUserDetailsBtn');
|
||||||
|
const updateProfileBtn = document.getElementById('updateProfileBtn');
|
||||||
|
const saveLocationBtn = document.getElementById('saveLocationBtn');
|
||||||
|
|
||||||
|
if (getUserDetailsBtn) {
|
||||||
|
getUserDetailsBtn.disabled = false;
|
||||||
|
getUserDetailsBtn.textContent = 'Get User Details';
|
||||||
|
}
|
||||||
|
if (updateProfileBtn) {
|
||||||
|
updateProfileBtn.disabled = false;
|
||||||
|
updateProfileBtn.textContent = 'Update Profile';
|
||||||
|
}
|
||||||
|
if (saveLocationBtn) {
|
||||||
|
saveLocationBtn.disabled = false;
|
||||||
|
saveLocationBtn.textContent = 'Save Location';
|
||||||
|
}
|
||||||
|
|
||||||
alert('OTP verified successfully! needs_profile = ' + data.needs_profile);
|
alert('OTP verified successfully! needs_profile = ' + data.needs_profile);
|
||||||
} else {
|
} else {
|
||||||
alert('Error: ' + (data.error || data.message || 'OTP verification failed'));
|
alert('Error: ' + (data.error || data.message || 'OTP verification failed'));
|
||||||
|
|
@ -249,8 +312,43 @@
|
||||||
console.log('Response data:', data);
|
console.log('Response data:', data);
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
out.textContent = JSON.stringify(data, null, 2);
|
// Format user details with new schema fields
|
||||||
|
const formattedData = {
|
||||||
|
id: data.id,
|
||||||
|
phone_number: data.phone_number,
|
||||||
|
name: data.name,
|
||||||
|
user_type: data.user_type, // System role: user/admin/moderator
|
||||||
|
roles: data.roles, // Marketplace roles array
|
||||||
|
active_role: data.active_role, // Active marketplace role
|
||||||
|
rating_average: data.rating_average,
|
||||||
|
rating_count: data.rating_count,
|
||||||
|
subscription_plan_id: data.subscription_plan_id,
|
||||||
|
subscription_expires_at: data.subscription_expires_at,
|
||||||
|
avatar_url: data.avatar_url,
|
||||||
|
language: data.language,
|
||||||
|
timezone: data.timezone,
|
||||||
|
country_code: data.country_code,
|
||||||
|
created_at: data.created_at,
|
||||||
|
last_login_at: data.last_login_at,
|
||||||
|
active_devices_count: data.active_devices_count,
|
||||||
|
primary_location: data.location,
|
||||||
|
all_locations: data.locations
|
||||||
|
};
|
||||||
|
|
||||||
|
out.textContent = JSON.stringify(formattedData, null, 2);
|
||||||
alert('User details fetched successfully!');
|
alert('User details fetched successfully!');
|
||||||
|
|
||||||
|
// Auto-fill location fields if primary location exists
|
||||||
|
if (data.location) {
|
||||||
|
document.getElementById('country').value = data.location.country || '';
|
||||||
|
document.getElementById('state').value = data.location.state || '';
|
||||||
|
document.getElementById('district').value = data.location.district || '';
|
||||||
|
document.getElementById('cityVillage').value = data.location.city_village || '';
|
||||||
|
document.getElementById('pincode').value = data.location.pincode || '';
|
||||||
|
if (data.location.location_type) {
|
||||||
|
document.getElementById('locationType').value = data.location.location_type;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
out.textContent = `Error ${res.status}: ${JSON.stringify(data, null, 2)}`;
|
out.textContent = `Error ${res.status}: ${JSON.stringify(data, null, 2)}`;
|
||||||
|
|
||||||
|
|
@ -268,6 +366,84 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveLocation() {
|
||||||
|
const out = document.getElementById('saveLocationResult');
|
||||||
|
let lat = null;
|
||||||
|
let lng = null;
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
alert('No access_token. Verify OTP first.');
|
||||||
|
out.textContent = 'Error: No access token. Please verify OTP first.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current location if checkbox is checked
|
||||||
|
if (document.getElementById('useCurrentLocation').checked) {
|
||||||
|
try {
|
||||||
|
out.textContent = 'Getting current location...';
|
||||||
|
const position = await new Promise((resolve, reject) => {
|
||||||
|
navigator.geolocation.getCurrentPosition(resolve, reject);
|
||||||
|
});
|
||||||
|
lat = position.coords.latitude;
|
||||||
|
lng = position.coords.longitude;
|
||||||
|
console.log('Current location:', lat, lng);
|
||||||
|
} catch (err) {
|
||||||
|
alert('Error getting location: ' + err.message);
|
||||||
|
out.textContent = 'Error getting GPS location: ' + err.message;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const locationData = {
|
||||||
|
location_type: document.getElementById('locationType').value,
|
||||||
|
country: document.getElementById('country').value.trim() || null,
|
||||||
|
state: document.getElementById('state').value.trim() || null,
|
||||||
|
district: document.getElementById('district').value.trim() || null,
|
||||||
|
city_village: document.getElementById('cityVillage').value.trim() || null,
|
||||||
|
pincode: document.getElementById('pincode').value.trim() || null,
|
||||||
|
is_saved_address: true,
|
||||||
|
selected_location: document.getElementById('isPrimaryLocation').checked,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (lat !== null && lng !== null) {
|
||||||
|
locationData.lat = lat;
|
||||||
|
locationData.lng = lng;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.textContent = 'Saving location...';
|
||||||
|
console.log('Saving location:', locationData);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Note: You'll need to add a POST /users/me/locations endpoint
|
||||||
|
// For now, we'll use a generic approach
|
||||||
|
const res = await fetch('/users/me/locations', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer ' + accessToken,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(locationData)
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Response status:', res.status);
|
||||||
|
const data = await res.json();
|
||||||
|
console.log('Response data:', data);
|
||||||
|
out.textContent = JSON.stringify(data, null, 2);
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
alert('Location saved successfully!');
|
||||||
|
// Refresh user details to show new location
|
||||||
|
getUserDetails();
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + (data.error || data.message || 'Failed to save location'));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Save location error:', err);
|
||||||
|
out.textContent = 'Error: ' + err.message;
|
||||||
|
alert('Error: ' + err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Attach event listeners to buttons (replaces inline onclick handlers)
|
// Attach event listeners to buttons (replaces inline onclick handlers)
|
||||||
// Since script is at end of body, DOM is already loaded
|
// Since script is at end of body, DOM is already loaded
|
||||||
(function() {
|
(function() {
|
||||||
|
|
@ -275,6 +451,7 @@
|
||||||
const verifyOtpBtn = document.getElementById('verifyOtpBtn');
|
const verifyOtpBtn = document.getElementById('verifyOtpBtn');
|
||||||
const updateProfileBtn = document.getElementById('updateProfileBtn');
|
const updateProfileBtn = document.getElementById('updateProfileBtn');
|
||||||
const getUserDetailsBtn = document.getElementById('getUserDetailsBtn');
|
const getUserDetailsBtn = document.getElementById('getUserDetailsBtn');
|
||||||
|
const saveLocationBtn = document.getElementById('saveLocationBtn');
|
||||||
|
|
||||||
if (requestOtpBtn) {
|
if (requestOtpBtn) {
|
||||||
requestOtpBtn.addEventListener('click', requestOtp);
|
requestOtpBtn.addEventListener('click', requestOtp);
|
||||||
|
|
@ -292,6 +469,10 @@
|
||||||
getUserDetailsBtn.addEventListener('click', getUserDetails);
|
getUserDetailsBtn.addEventListener('click', getUserDetails);
|
||||||
console.log('Get User Details button listener attached');
|
console.log('Get User Details button listener attached');
|
||||||
}
|
}
|
||||||
|
if (saveLocationBtn) {
|
||||||
|
saveLocationBtn.addEventListener('click', saveLocation);
|
||||||
|
console.log('Save Location button listener attached');
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Test page loaded. All event listeners attached.');
|
console.log('Test page loaded. All event listeners attached.');
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -43,12 +43,12 @@ async function requireRecentOtpOrReauth(req, res, next) {
|
||||||
|
|
||||||
const userId = req.user.id;
|
const userId = req.user.id;
|
||||||
|
|
||||||
// Check if token has high_assurance claim
|
// Check if token has high_assurance claim (set after OTP verification)
|
||||||
if (req.user.high_assurance === true) {
|
if (req.user.high_assurance === true) {
|
||||||
return next(); // Token already has high assurance
|
return next(); // Token already has high assurance
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for recent OTP verification in audit logs
|
// Check for recent OTP verification in audit logs (within the time window)
|
||||||
const recentOtpResult = await db.query(
|
const recentOtpResult = await db.query(
|
||||||
`SELECT created_at
|
`SELECT created_at
|
||||||
FROM auth_audit
|
FROM auth_audit
|
||||||
|
|
@ -65,6 +65,24 @@ async function requireRecentOtpOrReauth(req, res, next) {
|
||||||
// Recent OTP verification found, allow the request
|
// Recent OTP verification found, allow the request
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also check if this is initial profile completion (user has no name)
|
||||||
|
// Allow initial profile setup without step-up auth
|
||||||
|
try {
|
||||||
|
const userCheck = await db.query(
|
||||||
|
`SELECT name FROM users WHERE id = $1 AND deleted = FALSE`,
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (userCheck.rows.length > 0 && (!userCheck.rows[0].name || userCheck.rows[0].name.trim() === '')) {
|
||||||
|
// User has no name, this is initial profile completion - allow it
|
||||||
|
console.log(`[STEP_UP_AUTH] Allowing initial profile completion for user ${userId}`);
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// If check fails, continue with step-up requirement
|
||||||
|
console.error('Error checking user profile:', err);
|
||||||
|
}
|
||||||
|
|
||||||
// No recent OTP or high assurance token
|
// No recent OTP or high assurance token
|
||||||
// Log the attempt
|
// Log the attempt
|
||||||
|
|
|
||||||
|
|
@ -225,21 +225,41 @@ function validateLogoutBody(req, res, next) {
|
||||||
// === VALIDATION: USER ROUTES ===
|
// === VALIDATION: USER ROUTES ===
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate user_type enum value
|
* Validate marketplace role (for active_role field)
|
||||||
* Allowed values: 'seller', 'buyer', 'service_provider' (from user_type_enum in database)
|
* Maps old values to new schema:
|
||||||
|
* - 'seller' or 'buyer' -> 'seller_buyer'
|
||||||
|
* - 'service_provider' -> 'service_provider'
|
||||||
|
*
|
||||||
|
* Database: active_role uses listing_role_enum: ('seller_buyer', 'service_provider')
|
||||||
*/
|
*/
|
||||||
function validateUserType(userType) {
|
function validateMarketplaceRole(role) {
|
||||||
if (userType === undefined || userType === null) {
|
if (role === undefined || role === null) {
|
||||||
return { valid: true }; // Optional field
|
return { valid: true }; // Optional field
|
||||||
}
|
}
|
||||||
if (typeof userType !== 'string') {
|
if (typeof role !== 'string') {
|
||||||
return { valid: false, error: 'user_type must be a string' };
|
return { valid: false, error: 'user_type must be a string' };
|
||||||
}
|
}
|
||||||
const allowedValues = ['seller', 'buyer', 'service_provider'];
|
// Normalize to lowercase for comparison
|
||||||
if (!allowedValues.includes(userType)) {
|
const normalized = role.toLowerCase();
|
||||||
return { valid: false, error: `user_type must be one of: ${allowedValues.join(', ')}` };
|
|
||||||
|
// Map old values to new schema
|
||||||
|
const roleMapping = {
|
||||||
|
'seller': 'seller_buyer',
|
||||||
|
'buyer': 'seller_buyer',
|
||||||
|
'service_provider': 'service_provider'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (roleMapping[normalized]) {
|
||||||
|
return { valid: true, mappedValue: roleMapping[normalized] };
|
||||||
}
|
}
|
||||||
return { valid: true };
|
|
||||||
|
// Also accept new values directly
|
||||||
|
const allowedValues = ['seller_buyer', 'service_provider'];
|
||||||
|
if (allowedValues.includes(normalized)) {
|
||||||
|
return { valid: true, mappedValue: normalized };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: false, error: `user_type must be one of: seller, buyer, service_provider, seller_buyer` };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -261,7 +281,8 @@ function validateName(name) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Middleware: Validate PUT /users/me request body
|
* Middleware: Validate PUT /users/me request body
|
||||||
* Validates: name (optional string, trimmed, max 100), user_type (optional enum)
|
* Validates: name (required string, trimmed, max 100), user_type (marketplace role: seller/buyer/service_provider)
|
||||||
|
* Maps user_type to active_role field in database
|
||||||
* Rejects: unknown extra fields, invalid types, overly long strings
|
* Rejects: unknown extra fields, invalid types, overly long strings
|
||||||
*/
|
*/
|
||||||
function validateUpdateProfileBody(req, res, next) {
|
function validateUpdateProfileBody(req, res, next) {
|
||||||
|
|
@ -283,22 +304,30 @@ function validateUpdateProfileBody(req, res, next) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate name (optional)
|
// Validate name (required)
|
||||||
|
if (!name) {
|
||||||
|
return res.status(400).json({ error: 'name is required' });
|
||||||
|
}
|
||||||
const nameCheck = validateName(name);
|
const nameCheck = validateName(name);
|
||||||
if (!nameCheck.valid) {
|
if (!nameCheck.valid) {
|
||||||
return res.status(400).json({ error: nameCheck.error });
|
return res.status(400).json({ error: nameCheck.error });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate user_type (optional)
|
// Validate user_type (required) - maps to active_role in database
|
||||||
const userTypeCheck = validateUserType(user_type);
|
if (!user_type) {
|
||||||
if (!userTypeCheck.valid) {
|
return res.status(400).json({ error: 'user_type is required' });
|
||||||
return res.status(400).json({ error: userTypeCheck.error });
|
}
|
||||||
|
const roleCheck = validateMarketplaceRole(user_type);
|
||||||
|
if (!roleCheck.valid) {
|
||||||
|
return res.status(400).json({ error: roleCheck.error });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trim name if provided
|
// Trim name and store mapped role value
|
||||||
if (name && typeof name === 'string') {
|
if (name && typeof name === 'string') {
|
||||||
req.body.name = name.trim();
|
req.body.name = name.trim();
|
||||||
}
|
}
|
||||||
|
// Store the mapped marketplace role value for the route handler
|
||||||
|
req.body.active_role = roleCheck.mappedValue;
|
||||||
|
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -398,6 +398,8 @@ router.post(
|
||||||
device_model = EXCLUDED.device_model,
|
device_model = EXCLUDED.device_model,
|
||||||
os_version = EXCLUDED.os_version,
|
os_version = EXCLUDED.os_version,
|
||||||
app_version = EXCLUDED.app_version,
|
app_version = EXCLUDED.app_version,
|
||||||
|
language_code = EXCLUDED.language_code,
|
||||||
|
timezone = EXCLUDED.timezone,
|
||||||
is_active = true
|
is_active = true
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
|
|
@ -427,10 +429,10 @@ router.post(
|
||||||
});
|
});
|
||||||
|
|
||||||
// === SECURITY HARDENING: AUDIT LOGS & ANOMALY FLAGS ===
|
// === SECURITY HARDENING: AUDIT LOGS & ANOMALY FLAGS ===
|
||||||
// Log authentication event with risk level
|
// Log OTP verification event with risk level (needed for step-up authentication)
|
||||||
await logAuthEvent({
|
await logAuthEvent({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
action: 'login',
|
action: 'otp_verify',
|
||||||
status: 'success',
|
status: 'success',
|
||||||
riskLevel: riskScore.isSuspicious
|
riskLevel: riskScore.isSuspicious
|
||||||
? (riskScore.score >= 50 ? RISK_LEVELS.HIGH_RISK : RISK_LEVELS.SUSPICIOUS)
|
? (riskScore.score >= 50 ? RISK_LEVELS.HIGH_RISK : RISK_LEVELS.SUSPICIOUS)
|
||||||
|
|
@ -562,7 +564,24 @@ router.post(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newAccess = signAccessToken(user);
|
// === SECURITY HARDENING: ACCESS TOKEN REPLAY MITIGATION ===
|
||||||
|
// Preserve high_assurance status if there's been a recent OTP verification
|
||||||
|
// This ensures step-up authentication remains valid after token refresh
|
||||||
|
const RECENT_OTP_WINDOW_MINUTES = parseInt(process.env.STEP_UP_OTP_WINDOW_MINUTES || '5', 10);
|
||||||
|
const recentOtpCheck = await db.query(
|
||||||
|
`SELECT created_at
|
||||||
|
FROM auth_audit
|
||||||
|
WHERE user_id = $1
|
||||||
|
AND action = 'otp_verify'
|
||||||
|
AND status = 'success'
|
||||||
|
AND created_at > NOW() - INTERVAL '${RECENT_OTP_WINDOW_MINUTES} minutes'
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 1`,
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
const hasRecentOtp = recentOtpCheck.rows.length > 0;
|
||||||
|
|
||||||
|
const newAccess = signAccessToken(user, { highAssurance: hasRecentOtp });
|
||||||
const newRefresh = await rotateRefreshToken({
|
const newRefresh = await rotateRefreshToken({
|
||||||
tokenRow,
|
tokenRow,
|
||||||
userAgent: req.headers['user-agent'],
|
userAgent: req.headers['user-agent'],
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,13 @@ const router = express.Router();
|
||||||
// Rate limited: Read operation (100 requests per 15 minutes per user)
|
// Rate limited: Read operation (100 requests per 15 minutes per user)
|
||||||
router.get('/me', auth, userRateLimitRead, async (req, res) => {
|
router.get('/me', auth, userRateLimitRead, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
// Get user basic information
|
// Get user basic information (with new schema fields)
|
||||||
const { rows } = await db.query(
|
const { rows } = await db.query(
|
||||||
`SELECT id, phone_number, name, role, user_type, created_at, last_login_at, avatar_url, language, timezone
|
`SELECT id, phone_number, name, user_type, roles, active_role,
|
||||||
|
rating_average, rating_count, subscription_plan_id, subscription_expires_at,
|
||||||
|
created_at, last_login_at, avatar_url, language, timezone, country_code
|
||||||
FROM users
|
FROM users
|
||||||
WHERE id = $1`,
|
WHERE id = $1 AND deleted = FALSE`,
|
||||||
[req.user.id]
|
[req.user.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -55,7 +57,7 @@ router.get('/me', auth, userRateLimitRead, async (req, res) => {
|
||||||
);
|
);
|
||||||
const activeDevicesCount = parseInt(deviceCountResult.rows[0].count, 10);
|
const activeDevicesCount = parseInt(deviceCountResult.rows[0].count, 10);
|
||||||
|
|
||||||
// Get user's saved locations (addresses)
|
// Get user's saved locations (addresses) - with new schema fields
|
||||||
const locationsResult = await db.query(
|
const locationsResult = await db.query(
|
||||||
`SELECT
|
`SELECT
|
||||||
id,
|
id,
|
||||||
|
|
@ -68,12 +70,13 @@ router.get('/me', auth, userRateLimitRead, async (req, res) => {
|
||||||
lng,
|
lng,
|
||||||
location_type,
|
location_type,
|
||||||
is_saved_address,
|
is_saved_address,
|
||||||
|
selected_location,
|
||||||
source_type,
|
source_type,
|
||||||
source_confidence,
|
source_confidence,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM locations
|
FROM locations
|
||||||
WHERE user_id = $1 AND is_saved_address = true
|
WHERE user_id = $1 AND is_saved_address = true AND deleted = FALSE
|
||||||
ORDER BY updated_at DESC`,
|
ORDER BY updated_at DESC`,
|
||||||
[req.user.id]
|
[req.user.id]
|
||||||
);
|
);
|
||||||
|
|
@ -91,6 +94,7 @@ router.get('/me', auth, userRateLimitRead, async (req, res) => {
|
||||||
} : null,
|
} : null,
|
||||||
location_type: loc.location_type,
|
location_type: loc.location_type,
|
||||||
is_saved_address: loc.is_saved_address,
|
is_saved_address: loc.is_saved_address,
|
||||||
|
selected_location: loc.selected_location || false,
|
||||||
source_type: loc.source_type,
|
source_type: loc.source_type,
|
||||||
source_confidence: loc.source_confidence,
|
source_confidence: loc.source_confidence,
|
||||||
created_at: loc.created_at,
|
created_at: loc.created_at,
|
||||||
|
|
@ -104,11 +108,17 @@ router.get('/me', auth, userRateLimitRead, async (req, res) => {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
phone_number: user.phone_number,
|
phone_number: user.phone_number,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
role: user.role,
|
user_type: user.user_type, // System role: 'user', 'admin', 'moderator'
|
||||||
user_type: user.user_type,
|
roles: user.roles || ['seller_buyer'], // Marketplace roles array
|
||||||
|
active_role: user.active_role || 'seller_buyer', // Active marketplace role
|
||||||
|
rating_average: user.rating_average || 0,
|
||||||
|
rating_count: user.rating_count || 0,
|
||||||
|
subscription_plan_id: user.subscription_plan_id,
|
||||||
|
subscription_expires_at: user.subscription_expires_at,
|
||||||
avatar_url: user.avatar_url,
|
avatar_url: user.avatar_url,
|
||||||
language: user.language,
|
language: user.language,
|
||||||
timezone: user.timezone,
|
timezone: user.timezone,
|
||||||
|
country_code: user.country_code || '+91',
|
||||||
created_at: user.created_at,
|
created_at: user.created_at,
|
||||||
last_login_at: user.last_login_at,
|
last_login_at: user.last_login_at,
|
||||||
active_devices_count: activeDevicesCount,
|
active_devices_count: activeDevicesCount,
|
||||||
|
|
@ -132,21 +142,39 @@ router.put(
|
||||||
validateUpdateProfileBody,
|
validateUpdateProfileBody,
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { name, user_type } = req.body;
|
const { name, active_role } = req.body;
|
||||||
|
|
||||||
if (!name || !user_type) {
|
if (!name) {
|
||||||
return res.status(400).json({ error: 'name and user_type are required' });
|
return res.status(400).json({ error: 'name is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build update query - active_role comes from validation middleware
|
||||||
|
const updateFields = ['name = $1'];
|
||||||
|
const updateValues = [name];
|
||||||
|
let paramIndex = 2;
|
||||||
|
|
||||||
|
if (active_role) {
|
||||||
|
// active_role is already validated and mapped by validation middleware
|
||||||
|
updateFields.push(`active_role = $${paramIndex}`);
|
||||||
|
updateValues.push(active_role);
|
||||||
|
paramIndex++;
|
||||||
|
|
||||||
|
// Also update roles array to include this role if not already present
|
||||||
|
updateFields.push(`roles = CASE WHEN $${paramIndex}::listing_role_enum = ANY(roles) THEN roles ELSE array_append(roles, $${paramIndex}::listing_role_enum) END`);
|
||||||
|
updateValues.push(active_role);
|
||||||
|
paramIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateValues.push(req.user.id);
|
||||||
|
|
||||||
const { rows } = await db.query(
|
const { rows } = await db.query(
|
||||||
`
|
`
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET name = $1,
|
SET ${updateFields.join(', ')}
|
||||||
user_type = $2
|
WHERE id = $${paramIndex} AND deleted = FALSE
|
||||||
WHERE id = $3
|
RETURNING id, phone_number, name, user_type, roles, active_role, rating_average, rating_count
|
||||||
RETURNING id, phone_number, name, role, user_type
|
|
||||||
`,
|
`,
|
||||||
[name, user_type, req.user.id]
|
updateValues
|
||||||
);
|
);
|
||||||
|
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
|
|
@ -379,4 +407,96 @@ router.post(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// POST /users/me/locations - Save a new location
|
||||||
|
// Rate limited: Write operation (20 requests per 15 minutes per user)
|
||||||
|
router.post(
|
||||||
|
'/me/locations',
|
||||||
|
auth,
|
||||||
|
userRateLimitWrite,
|
||||||
|
async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
location_type,
|
||||||
|
country,
|
||||||
|
state,
|
||||||
|
district,
|
||||||
|
city_village,
|
||||||
|
pincode,
|
||||||
|
lat,
|
||||||
|
lng,
|
||||||
|
is_saved_address = true,
|
||||||
|
selected_location = false,
|
||||||
|
source_type = 'manual',
|
||||||
|
source_confidence = 'medium',
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!location_type) {
|
||||||
|
return res.status(400).json({ error: 'location_type is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert location
|
||||||
|
const { rows } = await db.query(
|
||||||
|
`INSERT INTO locations (
|
||||||
|
user_id, location_type, country, state, district, city_village, pincode,
|
||||||
|
lat, lng, is_saved_address, selected_location, source_type, source_confidence
|
||||||
|
)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
||||||
|
RETURNING id, location_type, country, state, district, city_village, pincode,
|
||||||
|
lat, lng, is_saved_address, selected_location, source_type, source_confidence,
|
||||||
|
created_at, updated_at`,
|
||||||
|
[
|
||||||
|
req.user.id,
|
||||||
|
location_type,
|
||||||
|
country || null,
|
||||||
|
state || null,
|
||||||
|
district || null,
|
||||||
|
city_village || null,
|
||||||
|
pincode || null,
|
||||||
|
lat || null,
|
||||||
|
lng || null,
|
||||||
|
is_saved_address,
|
||||||
|
selected_location,
|
||||||
|
source_type,
|
||||||
|
source_confidence,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// If this is set as primary location, unset others
|
||||||
|
if (selected_location) {
|
||||||
|
await db.query(
|
||||||
|
`UPDATE locations
|
||||||
|
SET selected_location = false
|
||||||
|
WHERE user_id = $1 AND id != $2`,
|
||||||
|
[req.user.id, rows[0].id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const location = rows[0];
|
||||||
|
return res.json({
|
||||||
|
id: location.id,
|
||||||
|
location_type: location.location_type,
|
||||||
|
country: location.country,
|
||||||
|
state: location.state,
|
||||||
|
district: location.district,
|
||||||
|
city_village: location.city_village,
|
||||||
|
pincode: location.pincode,
|
||||||
|
coordinates: location.lat && location.lng ? {
|
||||||
|
latitude: parseFloat(location.lat),
|
||||||
|
longitude: parseFloat(location.lng)
|
||||||
|
} : null,
|
||||||
|
is_saved_address: location.is_saved_address,
|
||||||
|
selected_location: location.selected_location,
|
||||||
|
source_type: location.source_type,
|
||||||
|
source_confidence: location.source_confidence,
|
||||||
|
created_at: location.created_at,
|
||||||
|
updated_at: location.updated_at,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Save location error:', err);
|
||||||
|
return res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
|
||||||
|
|
@ -217,21 +217,23 @@ async function getPreviousAuthInfo(userId, deviceId) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: check refresh_tokens for this device
|
// Fallback: check user_devices for this device
|
||||||
if (deviceId) {
|
if (deviceId) {
|
||||||
const tokenResult = await db.query(
|
const deviceResult = await db.query(
|
||||||
`SELECT ip_address, user_agent
|
`SELECT lat, lng, last_seen_at
|
||||||
FROM refresh_tokens
|
FROM user_devices
|
||||||
WHERE user_id = $1 AND device_id = $2
|
WHERE user_id = $1 AND device_identifier = $2
|
||||||
ORDER BY created_at DESC
|
ORDER BY last_seen_at DESC
|
||||||
LIMIT 1`,
|
LIMIT 1`,
|
||||||
[userId, deviceId]
|
[userId, deviceId]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (tokenResult.rows.length > 0) {
|
// Device info available but IP/user_agent not tracked in user_devices table
|
||||||
|
// Return deviceId as previousDeviceId
|
||||||
|
if (deviceResult.rows.length > 0) {
|
||||||
return {
|
return {
|
||||||
previousIp: tokenResult.rows[0].ip_address,
|
previousIp: null, // Not tracked in new schema
|
||||||
previousUserAgent: tokenResult.rows[0].user_agent,
|
previousUserAgent: null, // Not tracked in new schema
|
||||||
previousDeviceId: deviceId,
|
previousDeviceId: deviceId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -259,10 +261,3 @@ module.exports = {
|
||||||
getPreviousAuthInfo,
|
getPreviousAuthInfo,
|
||||||
REQUIRE_OTP_ON_SUSPICIOUS_REFRESH,
|
REQUIRE_OTP_ON_SUSPICIOUS_REFRESH,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,60 +4,28 @@ const bcrypt = require('bcrypt');
|
||||||
const { randomUUID } = require('crypto');
|
const { randomUUID } = require('crypto');
|
||||||
const db = require('../db');
|
const db = require('../db');
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
// === SECURITY HARDENING: JWT KEY ROTATION ===
|
|
||||||
const {
|
|
||||||
getActiveKey,
|
|
||||||
getKeySecret,
|
|
||||||
getAllKeys,
|
|
||||||
getIssuer,
|
|
||||||
getAudience,
|
|
||||||
validateTokenClaims,
|
|
||||||
} = require('./jwtKeys');
|
|
||||||
|
|
||||||
|
const ACCESS_SECRET = config.jwtAccessSecret;
|
||||||
|
const REFRESH_SECRET = config.jwtRefreshSecret;
|
||||||
const ACCESS_TTL = config.jwtAccessTtl;
|
const ACCESS_TTL = config.jwtAccessTtl;
|
||||||
const REFRESH_TTL = config.jwtRefreshTtl;
|
const REFRESH_TTL = config.jwtRefreshTtl;
|
||||||
const REFRESH_MAX_IDLE_MS = config.refreshMaxIdleMinutes * 60 * 1000;
|
const REFRESH_MAX_IDLE_MS = config.refreshMaxIdleMinutes * 60 * 1000;
|
||||||
|
|
||||||
// === SECURITY HARDENING: JWT KEY ROTATION ===
|
|
||||||
// Get active key for signing
|
|
||||||
const { keyId: ACTIVE_KEY_ID, secret: ACTIVE_SECRET } = getActiveKey();
|
|
||||||
// For refresh tokens, use a separate key if available, otherwise use active key
|
|
||||||
const REFRESH_KEY_ID = process.env.JWT_REFRESH_KEY_ID || ACTIVE_KEY_ID;
|
|
||||||
const REFRESH_SECRET = getKeySecret(REFRESH_KEY_ID) || ACTIVE_SECRET;
|
|
||||||
|
|
||||||
function signAccessToken(user, options = {}) {
|
function signAccessToken(user, options = {}) {
|
||||||
// === SECURITY HARDENING: JWT KEY ROTATION ===
|
|
||||||
// Include key ID in token header and add standard claims
|
|
||||||
const payload = {
|
const payload = {
|
||||||
sub: user.id,
|
sub: user.id,
|
||||||
phone_number: user.phone_number,
|
phone_number: user.phone_number,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
user_type: user.user_type || null,
|
user_type: user.user_type || null,
|
||||||
// === SECURITY HARDENING: JWT KEY ROTATION ===
|
|
||||||
// Standard JWT claims
|
|
||||||
iss: getIssuer(),
|
|
||||||
aud: getAudience(),
|
|
||||||
iat: Math.floor(Date.now() / 1000),
|
|
||||||
// High assurance flag (set after OTP verification for sensitive actions)
|
|
||||||
high_assurance: options.highAssurance || false,
|
|
||||||
// === SECURITY HARDENING: GLOBAL LOGOUT ===
|
|
||||||
// Token version for immediate invalidation on logout-all-devices
|
|
||||||
token_version: user.token_version || 1,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return jwt.sign(
|
// === SECURITY HARDENING: ACCESS TOKEN REPLAY MITIGATION ===
|
||||||
payload,
|
// Include high_assurance flag if requested (for step-up authentication)
|
||||||
ACTIVE_SECRET,
|
if (options.highAssurance === true) {
|
||||||
{
|
payload.high_assurance = true;
|
||||||
expiresIn: ACCESS_TTL,
|
}
|
||||||
// === SECURITY HARDENING: JWT KEY ROTATION ===
|
|
||||||
// Include key ID in header for key rotation support
|
return jwt.sign(payload, ACCESS_SECRET, { expiresIn: ACCESS_TTL });
|
||||||
header: {
|
|
||||||
kid: ACTIVE_KEY_ID,
|
|
||||||
alg: 'HS256',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function issueRefreshToken({
|
async function issueRefreshToken({
|
||||||
|
|
@ -67,31 +35,15 @@ async function issueRefreshToken({
|
||||||
ip,
|
ip,
|
||||||
rotatedFromId = null,
|
rotatedFromId = null,
|
||||||
}) {
|
}) {
|
||||||
// === SECURITY HARDENING: JWT KEY ROTATION ===
|
|
||||||
const tokenId = randomUUID();
|
const tokenId = randomUUID();
|
||||||
const payload = {
|
|
||||||
sub: userId,
|
|
||||||
device_id: deviceId,
|
|
||||||
jti: tokenId,
|
|
||||||
// === SECURITY HARDENING: JWT KEY ROTATION ===
|
|
||||||
// Standard JWT claims
|
|
||||||
iss: getIssuer(),
|
|
||||||
aud: getAudience(),
|
|
||||||
iat: Math.floor(Date.now() / 1000),
|
|
||||||
};
|
|
||||||
|
|
||||||
const token = jwt.sign(
|
const token = jwt.sign(
|
||||||
payload,
|
|
||||||
REFRESH_SECRET,
|
|
||||||
{
|
{
|
||||||
expiresIn: REFRESH_TTL,
|
sub: userId,
|
||||||
// === SECURITY HARDENING: JWT KEY ROTATION ===
|
device_id: deviceId,
|
||||||
// Include key ID in header
|
jti: tokenId,
|
||||||
header: {
|
},
|
||||||
kid: REFRESH_KEY_ID,
|
REFRESH_SECRET,
|
||||||
alg: 'HS256',
|
{ expiresIn: REFRESH_TTL }
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
const decoded = jwt.decode(token);
|
const decoded = jwt.decode(token);
|
||||||
const expiresAt = new Date(decoded.exp * 1000);
|
const expiresAt = new Date(decoded.exp * 1000);
|
||||||
|
|
@ -150,62 +102,13 @@ async function storeRefreshToken({
|
||||||
}
|
}
|
||||||
|
|
||||||
async function verifyRefreshToken(rawToken) {
|
async function verifyRefreshToken(rawToken) {
|
||||||
// === SECURITY HARDENING: JWT KEY ROTATION ===
|
let payload;
|
||||||
// Decode token to get key ID from header
|
|
||||||
let decoded;
|
|
||||||
try {
|
try {
|
||||||
decoded = jwt.decode(rawToken, { complete: true });
|
payload = jwt.verify(rawToken, REFRESH_SECRET);
|
||||||
if (!decoded || !decoded.header) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get secret for the key ID (if present) or try all keys
|
|
||||||
const keyId = decoded.header.kid;
|
|
||||||
let payload = null;
|
|
||||||
let verified = false;
|
|
||||||
|
|
||||||
if (keyId) {
|
|
||||||
const secret = getKeySecret(keyId);
|
|
||||||
if (secret) {
|
|
||||||
try {
|
|
||||||
payload = jwt.verify(rawToken, secret);
|
|
||||||
verified = true;
|
|
||||||
} catch (err) {
|
|
||||||
// Key ID specified but verification failed
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If key ID not found or not specified, try all keys (for rotation support)
|
|
||||||
if (!verified) {
|
|
||||||
const allKeys = getAllKeys();
|
|
||||||
for (const [kid, keySecret] of Object.entries(allKeys)) {
|
|
||||||
try {
|
|
||||||
payload = jwt.verify(rawToken, keySecret);
|
|
||||||
verified = true;
|
|
||||||
break;
|
|
||||||
} catch (err) {
|
|
||||||
// Try next key
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!verified || !payload) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// === SECURITY HARDENING: JWT KEY ROTATION ===
|
|
||||||
// Validate JWT claims (iss, aud, exp, iat, nbf)
|
|
||||||
const claimsValidation = validateTokenClaims(payload);
|
|
||||||
if (!claimsValidation.valid) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { sub: userId, device_id: deviceId, jti: tokenId } = payload;
|
const { sub: userId, device_id: deviceId, jti: tokenId } = payload;
|
||||||
if (!tokenId) {
|
if (!tokenId) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -306,61 +209,10 @@ async function handleReuse(tokenRow) {
|
||||||
await revokeDeviceTokens(tokenRow.user_id, tokenRow.device_id);
|
await revokeDeviceTokens(tokenRow.user_id, tokenRow.device_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* === SECURITY HARDENING: GLOBAL LOGOUT ===
|
|
||||||
* Revoke all refresh tokens and invalidate all access tokens for a user
|
|
||||||
* by incrementing their token_version. This is used for "logout from all devices".
|
|
||||||
*
|
|
||||||
* @param {string} userId - User ID
|
|
||||||
* @returns {Promise<Object>} Object with revoked tokens count and new token version
|
|
||||||
*/
|
|
||||||
async function revokeAllUserTokens(userId) {
|
|
||||||
// Revoke all refresh tokens for the user
|
|
||||||
const revokeResult = await db.query(
|
|
||||||
`
|
|
||||||
UPDATE refresh_tokens
|
|
||||||
SET revoked_at = NOW()
|
|
||||||
WHERE user_id = $1 AND revoked_at IS NULL
|
|
||||||
RETURNING id
|
|
||||||
`,
|
|
||||||
[userId]
|
|
||||||
);
|
|
||||||
const revokedTokensCount = revokeResult.rows.length;
|
|
||||||
|
|
||||||
// Mark all devices as inactive
|
|
||||||
await db.query(
|
|
||||||
`
|
|
||||||
UPDATE user_devices
|
|
||||||
SET is_active = false
|
|
||||||
WHERE user_id = $1 AND is_active = true
|
|
||||||
`,
|
|
||||||
[userId]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Increment token_version to invalidate all existing access tokens
|
|
||||||
const versionResult = await db.query(
|
|
||||||
`
|
|
||||||
UPDATE users
|
|
||||||
SET token_version = COALESCE(token_version, 1) + 1
|
|
||||||
WHERE id = $1
|
|
||||||
RETURNING token_version
|
|
||||||
`,
|
|
||||||
[userId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const newTokenVersion = versionResult.rows[0]?.token_version || 1;
|
|
||||||
|
|
||||||
return {
|
|
||||||
revokedTokensCount,
|
|
||||||
newTokenVersion,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
signAccessToken,
|
signAccessToken,
|
||||||
verifyRefreshToken,
|
verifyRefreshToken,
|
||||||
issueRefreshToken,
|
issueRefreshToken,
|
||||||
rotateRefreshToken,
|
rotateRefreshToken,
|
||||||
revokeRefreshToken,
|
revokeRefreshToken,
|
||||||
revokeAllUserTokens,
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue