add changes

This commit is contained in:
AD2025
2025-11-11 00:25:50 +02:00
commit e3ca132c5e
86 changed files with 22238 additions and 0 deletions

View 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;
};