add changes
This commit is contained in:
330
models/GuestSession.js
Normal file
330
models/GuestSession.js
Normal file
@@ -0,0 +1,330 @@
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const config = require('../config/config');
|
||||
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const GuestSession = sequelize.define('GuestSession', {
|
||||
id: {
|
||||
type: DataTypes.CHAR(36),
|
||||
primaryKey: true,
|
||||
defaultValue: () => uuidv4(),
|
||||
allowNull: false,
|
||||
comment: 'UUID primary key'
|
||||
},
|
||||
guestId: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
unique: {
|
||||
msg: 'Guest ID already exists'
|
||||
},
|
||||
field: 'guest_id',
|
||||
validate: {
|
||||
notEmpty: {
|
||||
msg: 'Guest ID cannot be empty'
|
||||
}
|
||||
},
|
||||
comment: 'Unique guest identifier'
|
||||
},
|
||||
sessionToken: {
|
||||
type: DataTypes.STRING(500),
|
||||
allowNull: false,
|
||||
unique: {
|
||||
msg: 'Session token already exists'
|
||||
},
|
||||
field: 'session_token',
|
||||
comment: 'JWT session token'
|
||||
},
|
||||
deviceId: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
field: 'device_id',
|
||||
comment: 'Device identifier (optional)'
|
||||
},
|
||||
ipAddress: {
|
||||
type: DataTypes.STRING(45),
|
||||
allowNull: true,
|
||||
field: 'ip_address',
|
||||
comment: 'IP address (supports IPv6)'
|
||||
},
|
||||
userAgent: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
field: 'user_agent',
|
||||
comment: 'Browser user agent string'
|
||||
},
|
||||
quizzesAttempted: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
field: 'quizzes_attempted',
|
||||
validate: {
|
||||
min: 0
|
||||
},
|
||||
comment: 'Number of quizzes attempted by guest'
|
||||
},
|
||||
maxQuizzes: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 3,
|
||||
field: 'max_quizzes',
|
||||
validate: {
|
||||
min: 1,
|
||||
max: 100
|
||||
},
|
||||
comment: 'Maximum quizzes allowed for this guest'
|
||||
},
|
||||
expiresAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
field: 'expires_at',
|
||||
validate: {
|
||||
isDate: true,
|
||||
isAfter: new Date().toISOString()
|
||||
},
|
||||
comment: 'Session expiration timestamp'
|
||||
},
|
||||
isConverted: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
field: 'is_converted',
|
||||
comment: 'Whether guest converted to registered user'
|
||||
},
|
||||
convertedUserId: {
|
||||
type: DataTypes.CHAR(36),
|
||||
allowNull: true,
|
||||
field: 'converted_user_id',
|
||||
comment: 'User ID if guest converted to registered user'
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'GuestSession',
|
||||
tableName: 'guest_sessions',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{
|
||||
unique: true,
|
||||
fields: ['guest_id']
|
||||
},
|
||||
{
|
||||
unique: true,
|
||||
fields: ['session_token']
|
||||
},
|
||||
{
|
||||
fields: ['expires_at']
|
||||
},
|
||||
{
|
||||
fields: ['is_converted']
|
||||
},
|
||||
{
|
||||
fields: ['converted_user_id']
|
||||
},
|
||||
{
|
||||
fields: ['device_id']
|
||||
},
|
||||
{
|
||||
fields: ['created_at']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Static method to generate guest ID
|
||||
GuestSession.generateGuestId = function() {
|
||||
const timestamp = Date.now();
|
||||
const randomStr = Math.random().toString(36).substring(2, 15);
|
||||
return `guest_${timestamp}_${randomStr}`;
|
||||
};
|
||||
|
||||
// Static method to generate session token (JWT)
|
||||
GuestSession.generateToken = function(guestId, sessionId) {
|
||||
const payload = {
|
||||
guestId,
|
||||
sessionId,
|
||||
type: 'guest'
|
||||
};
|
||||
|
||||
return jwt.sign(payload, config.jwt.secret, {
|
||||
expiresIn: config.guest.sessionExpireHours + 'h'
|
||||
});
|
||||
};
|
||||
|
||||
// Static method to verify and decode token
|
||||
GuestSession.verifyToken = function(token) {
|
||||
try {
|
||||
return jwt.verify(token, config.jwt.secret);
|
||||
} catch (error) {
|
||||
throw new Error('Invalid or expired token');
|
||||
}
|
||||
};
|
||||
|
||||
// Static method to create new guest session
|
||||
GuestSession.createSession = async function(options = {}) {
|
||||
const guestId = GuestSession.generateGuestId();
|
||||
const sessionId = uuidv4();
|
||||
const sessionToken = GuestSession.generateToken(guestId, sessionId);
|
||||
|
||||
const expiryHours = options.expiryHours || config.guest.sessionExpireHours || 24;
|
||||
const expiresAt = new Date(Date.now() + expiryHours * 60 * 60 * 1000);
|
||||
|
||||
const session = await GuestSession.create({
|
||||
id: sessionId,
|
||||
guestId,
|
||||
sessionToken,
|
||||
deviceId: options.deviceId || null,
|
||||
ipAddress: options.ipAddress || null,
|
||||
userAgent: options.userAgent || null,
|
||||
maxQuizzes: options.maxQuizzes || config.guest.maxQuizzes || 3,
|
||||
expiresAt
|
||||
});
|
||||
|
||||
return session;
|
||||
};
|
||||
|
||||
// Instance methods
|
||||
GuestSession.prototype.isExpired = function() {
|
||||
return new Date() > new Date(this.expiresAt);
|
||||
};
|
||||
|
||||
GuestSession.prototype.hasReachedQuizLimit = function() {
|
||||
return this.quizzesAttempted >= this.maxQuizzes;
|
||||
};
|
||||
|
||||
GuestSession.prototype.getRemainingQuizzes = function() {
|
||||
return Math.max(0, this.maxQuizzes - this.quizzesAttempted);
|
||||
};
|
||||
|
||||
GuestSession.prototype.incrementQuizAttempt = async function() {
|
||||
this.quizzesAttempted += 1;
|
||||
await this.save();
|
||||
};
|
||||
|
||||
GuestSession.prototype.extend = async function(hours = 24) {
|
||||
const newExpiry = new Date(Date.now() + hours * 60 * 60 * 1000);
|
||||
this.expiresAt = newExpiry;
|
||||
|
||||
// Regenerate token with new expiry
|
||||
this.sessionToken = GuestSession.generateToken(this.guestId, this.id);
|
||||
await this.save();
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
GuestSession.prototype.convertToUser = async function(userId) {
|
||||
this.isConverted = true;
|
||||
this.convertedUserId = userId;
|
||||
await this.save();
|
||||
};
|
||||
|
||||
GuestSession.prototype.getSessionInfo = function() {
|
||||
return {
|
||||
guestId: this.guestId,
|
||||
sessionId: this.id,
|
||||
quizzesAttempted: this.quizzesAttempted,
|
||||
maxQuizzes: this.maxQuizzes,
|
||||
remainingQuizzes: this.getRemainingQuizzes(),
|
||||
expiresAt: this.expiresAt,
|
||||
isExpired: this.isExpired(),
|
||||
hasReachedLimit: this.hasReachedQuizLimit(),
|
||||
isConverted: this.isConverted
|
||||
};
|
||||
};
|
||||
|
||||
// Class methods
|
||||
GuestSession.findByGuestId = async function(guestId) {
|
||||
return await this.findOne({
|
||||
where: { guestId }
|
||||
});
|
||||
};
|
||||
|
||||
GuestSession.findByToken = async function(token) {
|
||||
try {
|
||||
const decoded = GuestSession.verifyToken(token);
|
||||
return await this.findOne({
|
||||
where: {
|
||||
guestId: decoded.guestId,
|
||||
id: decoded.sessionId
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
GuestSession.findActiveSession = async function(guestId) {
|
||||
return await this.findOne({
|
||||
where: {
|
||||
guestId,
|
||||
isConverted: false
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
GuestSession.cleanupExpiredSessions = async function() {
|
||||
const expiredCount = await this.destroy({
|
||||
where: {
|
||||
expiresAt: {
|
||||
[sequelize.Sequelize.Op.lt]: new Date()
|
||||
},
|
||||
isConverted: false
|
||||
}
|
||||
});
|
||||
return expiredCount;
|
||||
};
|
||||
|
||||
GuestSession.getActiveGuestCount = async function() {
|
||||
return await this.count({
|
||||
where: {
|
||||
expiresAt: {
|
||||
[sequelize.Sequelize.Op.gt]: new Date()
|
||||
},
|
||||
isConverted: false
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
GuestSession.getConversionRate = async function() {
|
||||
const total = await this.count();
|
||||
if (total === 0) return 0;
|
||||
|
||||
const converted = await this.count({
|
||||
where: { isConverted: true }
|
||||
});
|
||||
|
||||
return Math.round((converted / total) * 100);
|
||||
};
|
||||
|
||||
// Hooks
|
||||
GuestSession.beforeValidate((session) => {
|
||||
// Ensure UUID is set
|
||||
if (!session.id) {
|
||||
session.id = uuidv4();
|
||||
}
|
||||
|
||||
// Ensure expiry is in the future (only for new records, not updates)
|
||||
if (session.isNewRecord && session.expiresAt && new Date(session.expiresAt) <= new Date()) {
|
||||
throw new Error('Expiry date must be in the future');
|
||||
}
|
||||
});
|
||||
|
||||
// Define associations
|
||||
GuestSession.associate = function(models) {
|
||||
// GuestSession belongs to a User (if converted)
|
||||
if (models.User) {
|
||||
GuestSession.belongsTo(models.User, {
|
||||
foreignKey: 'convertedUserId',
|
||||
as: 'convertedUser'
|
||||
});
|
||||
}
|
||||
|
||||
// GuestSession has many quiz sessions
|
||||
if (models.QuizSession) {
|
||||
GuestSession.hasMany(models.QuizSession, {
|
||||
foreignKey: 'guestSessionId',
|
||||
as: 'quizSessions'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return GuestSession;
|
||||
};
|
||||
Reference in New Issue
Block a user