Compare commits

...

2 Commits

Author SHA1 Message Date
jackiettran
532f3014df email can't be null, username removed since email can't be null 2025-11-24 15:41:35 -05:00
jackiettran
6aac929ec1 config files and scripts for database migration system 2025-11-24 13:48:10 -05:00
9 changed files with 128 additions and 100 deletions

7
backend/.sequelizerc Normal file
View File

@@ -0,0 +1,7 @@
const path = require('path');
module.exports = {
'config': path.resolve('config', 'database.js'),
'migrations-path': path.resolve('migrations'),
'models-path': path.resolve('models'),
};

View File

@@ -1,21 +1,62 @@
const { Sequelize } = require('sequelize'); const { Sequelize } = require("sequelize");
const sequelize = new Sequelize( // Load environment variables based on NODE_ENV
process.env.DB_NAME, // This ensures variables are available for both CLI and programmatic usage
process.env.DB_USER, if (!process.env.DB_NAME && process.env.NODE_ENV) {
process.env.DB_PASSWORD, const dotenv = require("dotenv");
{ const envFile = `.env.${process.env.NODE_ENV}`;
const result = dotenv.config({ path: envFile });
if (result.error && process.env.NODE_ENV !== "production") {
console.warn(
`Warning: Could not load ${envFile}, using existing environment variables`
);
}
}
// Database configuration object
// Used by both Sequelize CLI and programmatic initialization
const dbConfig = {
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
host: process.env.DB_HOST, host: process.env.DB_HOST,
port: process.env.DB_PORT, port: process.env.DB_PORT || 5432,
dialect: 'postgres', dialect: "postgres",
logging: false, logging: false,
pool: { pool: {
max: 5, max: 5,
min: 0, min: 0,
acquire: 30000, acquire: 10000,
idle: 10000 idle: 10000,
} },
};
// Configuration for Sequelize CLI (supports multiple environments)
// All environments use the same configuration from environment variables
const cliConfig = {
development: dbConfig,
dev: dbConfig,
test: dbConfig,
qa: dbConfig,
production: dbConfig,
prod: dbConfig,
};
// Create Sequelize instance for programmatic use
const sequelize = new Sequelize(
dbConfig.database,
dbConfig.username,
dbConfig.password,
{
host: dbConfig.host,
port: dbConfig.port,
dialect: dbConfig.dialect,
logging: dbConfig.logging,
pool: dbConfig.pool,
} }
); );
// Export the sequelize instance as default (for backward compatibility)
// Also export all environment configs for Sequelize CLI
module.exports = sequelize; module.exports = sequelize;
Object.assign(module.exports, cliConfig);

View File

@@ -110,16 +110,6 @@ const validateRegistration = [
"Last name can only contain letters, spaces, hyphens, and apostrophes" "Last name can only contain letters, spaces, hyphens, and apostrophes"
), ),
body("username")
.optional()
.trim()
.isLength({ min: 3, max: 30 })
.withMessage("Username must be between 3 and 30 characters")
.matches(/^[a-zA-Z0-9_-]+$/)
.withMessage(
"Username can only contain letters, numbers, underscores, and hyphens"
),
body("phone") body("phone")
.optional() .optional()
.isMobilePhone() .isMobilePhone()

View File

@@ -10,15 +10,10 @@ const User = sequelize.define(
defaultValue: DataTypes.UUIDV4, defaultValue: DataTypes.UUIDV4,
primaryKey: true, primaryKey: true,
}, },
username: {
type: DataTypes.STRING,
unique: true,
allowNull: true,
},
email: { email: {
type: DataTypes.STRING, type: DataTypes.STRING,
unique: true, unique: true,
allowNull: true, allowNull: false,
validate: { validate: {
isEmail: true, isEmail: true,
}, },

View File

@@ -16,6 +16,12 @@
"test:unit": "NODE_ENV=test jest tests/unit", "test:unit": "NODE_ENV=test jest tests/unit",
"test:integration": "NODE_ENV=test jest tests/integration", "test:integration": "NODE_ENV=test jest tests/integration",
"test:ci": "NODE_ENV=test jest --ci --coverage --maxWorkers=2", "test:ci": "NODE_ENV=test jest --ci --coverage --maxWorkers=2",
"db:migrate": "sequelize-cli db:migrate",
"db:migrate:undo": "sequelize-cli db:migrate:undo",
"db:migrate:undo:all": "sequelize-cli db:migrate:undo:all",
"db:migrate:status": "sequelize-cli db:migrate:status",
"db:create": "sequelize-cli db:create",
"test:migrations": "node scripts/test-migrations.js",
"alpha:add": "NODE_ENV=dev node scripts/manageAlphaInvitations.js add", "alpha:add": "NODE_ENV=dev node scripts/manageAlphaInvitations.js add",
"alpha:list": "NODE_ENV=dev node scripts/manageAlphaInvitations.js list", "alpha:list": "NODE_ENV=dev node scripts/manageAlphaInvitations.js list",
"alpha:revoke": "NODE_ENV=dev node scripts/manageAlphaInvitations.js revoke", "alpha:revoke": "NODE_ENV=dev node scripts/manageAlphaInvitations.js revoke",

View File

@@ -43,13 +43,11 @@ router.post(
validateRegistration, validateRegistration,
async (req, res) => { async (req, res) => {
try { try {
const { username, email, password, firstName, lastName, phone } = const { email, password, firstName, lastName, phone } =
req.body; req.body;
const existingUser = await User.findOne({ const existingUser = await User.findOne({
where: { where: { email },
[require("sequelize").Op.or]: [{ email }, { username }],
},
}); });
if (existingUser) { if (existingUser) {
@@ -96,7 +94,6 @@ router.post(
} }
const user = await User.create({ const user = await User.create({
username,
email, email,
password, password,
firstName, firstName,
@@ -161,14 +158,12 @@ router.post(
const reqLogger = logger.withRequestId(req.id); const reqLogger = logger.withRequestId(req.id);
reqLogger.info("User registration successful", { reqLogger.info("User registration successful", {
userId: user.id, userId: user.id,
username: user.username,
email: user.email, email: user.email,
}); });
res.status(201).json({ res.status(201).json({
user: { user: {
id: user.id, id: user.id,
username: user.username,
email: user.email, email: user.email,
firstName: user.firstName, firstName: user.firstName,
lastName: user.lastName, lastName: user.lastName,
@@ -184,7 +179,6 @@ router.post(
error: error.message, error: error.message,
stack: error.stack, stack: error.stack,
email: req.body.email, email: req.body.email,
username: req.body.username,
}); });
res.status(500).json({ error: "Registration failed. Please try again." }); res.status(500).json({ error: "Registration failed. Please try again." });
} }
@@ -265,7 +259,6 @@ router.post(
res.json({ res.json({
user: { user: {
id: user.id, id: user.id,
username: user.username,
email: user.email, email: user.email,
firstName: user.firstName, firstName: user.firstName,
lastName: user.lastName, lastName: user.lastName,
@@ -326,9 +319,9 @@ router.post(
} = payload; } = payload;
if (!email) { if (!email) {
return res return res.status(400).json({
.status(400) error: "Email permission is required to continue. Please grant email access when signing in with Google and try again."
.json({ error: "Email not provided by Google" }); });
} }
// Handle cases where Google doesn't provide name fields // Handle cases where Google doesn't provide name fields
@@ -375,7 +368,6 @@ router.post(
authProvider: "google", authProvider: "google",
providerId: googleId, providerId: googleId,
profileImage: picture, profileImage: picture,
username: email.split("@")[0] + "_" + googleId.slice(-6), // Generate unique username
isVerified: true, isVerified: true,
verifiedAt: new Date(), verifiedAt: new Date(),
}); });
@@ -439,7 +431,6 @@ router.post(
res.json({ res.json({
user: { user: {
id: user.id, id: user.id,
username: user.username,
email: user.email, email: user.email,
firstName: user.firstName, firstName: user.firstName,
lastName: user.lastName, lastName: user.lastName,
@@ -677,7 +668,6 @@ router.post("/refresh", async (req, res) => {
res.json({ res.json({
user: { user: {
id: user.id, id: user.id,
username: user.username,
email: user.email, email: user.email,
firstName: user.firstName, firstName: user.firstName,
lastName: user.lastName, lastName: user.lastName,

View File

@@ -95,7 +95,7 @@ router.get('/posts', optionalAuth, async (req, res) => {
{ {
model: User, model: User,
as: 'author', as: 'author',
attributes: ['id', 'username', 'firstName', 'lastName'] attributes: ['id', 'firstName', 'lastName']
}, },
{ {
model: PostTag, model: PostTag,
@@ -170,12 +170,12 @@ router.get('/posts/:id', optionalAuth, async (req, res) => {
{ {
model: User, model: User,
as: 'author', as: 'author',
attributes: ['id', 'username', 'firstName', 'lastName', 'role'] attributes: ['id', 'firstName', 'lastName', 'role']
}, },
{ {
model: User, model: User,
as: 'closer', as: 'closer',
attributes: ['id', 'username', 'firstName', 'lastName', 'role'], attributes: ['id', 'firstName', 'lastName', 'role'],
required: false required: false
}, },
{ {
@@ -191,7 +191,7 @@ router.get('/posts/:id', optionalAuth, async (req, res) => {
{ {
model: User, model: User,
as: 'author', as: 'author',
attributes: ['id', 'username', 'firstName', 'lastName', 'role'] attributes: ['id', 'firstName', 'lastName', 'role']
} }
] ]
} }
@@ -335,7 +335,7 @@ router.post('/posts', authenticateToken, uploadForumPostImages, async (req, res)
{ {
model: User, model: User,
as: 'author', as: 'author',
attributes: ['id', 'username', 'firstName', 'lastName'] attributes: ['id', 'firstName', 'lastName']
}, },
{ {
model: PostTag, model: PostTag,
@@ -524,7 +524,7 @@ router.put('/posts/:id', authenticateToken, async (req, res) => {
{ {
model: User, model: User,
as: 'author', as: 'author',
attributes: ['id', 'username', 'firstName', 'lastName'] attributes: ['id', 'firstName', 'lastName']
}, },
{ {
model: PostTag, model: PostTag,
@@ -622,7 +622,7 @@ router.patch('/posts/:id/status', authenticateToken, async (req, res) => {
(async () => { (async () => {
try { try {
const closerUser = await User.findByPk(req.user.id, { const closerUser = await User.findByPk(req.user.id, {
attributes: ['id', 'username', 'firstName', 'lastName', 'email'] attributes: ['id', 'firstName', 'lastName', 'email']
}); });
const postWithAuthor = await ForumPost.findByPk(post.id, { const postWithAuthor = await ForumPost.findByPk(post.id, {
@@ -630,7 +630,7 @@ router.patch('/posts/:id/status', authenticateToken, async (req, res) => {
{ {
model: User, model: User,
as: 'author', as: 'author',
attributes: ['id', 'username', 'firstName', 'lastName', 'email'] attributes: ['id', 'firstName', 'lastName', 'email']
} }
] ]
}); });
@@ -702,12 +702,12 @@ router.patch('/posts/:id/status', authenticateToken, async (req, res) => {
{ {
model: User, model: User,
as: 'author', as: 'author',
attributes: ['id', 'username', 'firstName', 'lastName'] attributes: ['id', 'firstName', 'lastName']
}, },
{ {
model: User, model: User,
as: 'closer', as: 'closer',
attributes: ['id', 'username', 'firstName', 'lastName'], attributes: ['id', 'firstName', 'lastName'],
required: false required: false
}, },
{ {
@@ -790,13 +790,13 @@ router.patch('/posts/:id/accept-answer', authenticateToken, async (req, res) =>
{ {
model: User, model: User,
as: 'author', as: 'author',
attributes: ['id', 'username', 'firstName', 'lastName', 'email'] attributes: ['id', 'firstName', 'lastName', 'email']
} }
] ]
}); });
const postAuthor = await User.findByPk(req.user.id, { const postAuthor = await User.findByPk(req.user.id, {
attributes: ['id', 'username', 'firstName', 'lastName', 'email'] attributes: ['id', 'firstName', 'lastName', 'email']
}); });
const postWithAuthor = await ForumPost.findByPk(post.id, { const postWithAuthor = await ForumPost.findByPk(post.id, {
@@ -804,7 +804,7 @@ router.patch('/posts/:id/accept-answer', authenticateToken, async (req, res) =>
{ {
model: User, model: User,
as: 'author', as: 'author',
attributes: ['id', 'username', 'firstName', 'lastName', 'email'] attributes: ['id', 'firstName', 'lastName', 'email']
} }
] ]
}); });
@@ -882,7 +882,7 @@ router.patch('/posts/:id/accept-answer', authenticateToken, async (req, res) =>
{ {
model: User, model: User,
as: 'author', as: 'author',
attributes: ['id', 'username', 'firstName', 'lastName'] attributes: ['id', 'firstName', 'lastName']
}, },
{ {
model: PostTag, model: PostTag,
@@ -972,7 +972,7 @@ router.post('/posts/:id/comments', authenticateToken, uploadForumCommentImages,
{ {
model: User, model: User,
as: 'author', as: 'author',
attributes: ['id', 'username', 'firstName', 'lastName', 'email'] attributes: ['id', 'firstName', 'lastName', 'email']
} }
] ]
}); });
@@ -984,7 +984,7 @@ router.post('/posts/:id/comments', authenticateToken, uploadForumCommentImages,
{ {
model: User, model: User,
as: 'author', as: 'author',
attributes: ['id', 'username', 'firstName', 'lastName', 'email'] attributes: ['id', 'firstName', 'lastName', 'email']
} }
] ]
}); });
@@ -1027,7 +1027,7 @@ router.post('/posts/:id/comments', authenticateToken, uploadForumCommentImages,
{ {
model: User, model: User,
as: 'author', as: 'author',
attributes: ['id', 'username', 'firstName', 'lastName', 'email'] attributes: ['id', 'firstName', 'lastName', 'email']
} }
], ],
group: ['ForumComment.authorId', 'author.id'] group: ['ForumComment.authorId', 'author.id']
@@ -1102,7 +1102,7 @@ router.put('/comments/:id', authenticateToken, async (req, res) => {
{ {
model: User, model: User,
as: 'author', as: 'author',
attributes: ['id', 'username', 'firstName', 'lastName'] attributes: ['id', 'firstName', 'lastName']
} }
] ]
}); });
@@ -1177,7 +1177,7 @@ router.get('/my-posts', authenticateToken, async (req, res) => {
{ {
model: User, model: User,
as: 'author', as: 'author',
attributes: ['id', 'username', 'firstName', 'lastName'] attributes: ['id', 'firstName', 'lastName']
}, },
{ {
model: PostTag, model: PostTag,
@@ -1294,7 +1294,7 @@ router.delete('/admin/posts/:id', authenticateToken, requireAdmin, async (req, r
(async () => { (async () => {
try { try {
const admin = await User.findByPk(req.user.id, { const admin = await User.findByPk(req.user.id, {
attributes: ['id', 'username', 'firstName', 'lastName'] attributes: ['id', 'firstName', 'lastName']
}); });
if (post.author && admin) { if (post.author && admin) {
@@ -1422,7 +1422,7 @@ router.delete('/admin/comments/:id', authenticateToken, requireAdmin, async (req
(async () => { (async () => {
try { try {
const admin = await User.findByPk(req.user.id, { const admin = await User.findByPk(req.user.id, {
attributes: ['id', 'username', 'firstName', 'lastName'] attributes: ['id', 'firstName', 'lastName']
}); });
if (comment.author && admin && post) { if (comment.author && admin && post) {

View File

@@ -43,7 +43,7 @@ router.get("/", async (req, res) => {
{ {
model: User, model: User,
as: "owner", as: "owner",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
}, },
], ],
limit: parseInt(limit), limit: parseInt(limit),
@@ -180,12 +180,12 @@ router.get("/:id", optionalAuth, async (req, res) => {
{ {
model: User, model: User,
as: "owner", as: "owner",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
}, },
{ {
model: User, model: User,
as: "deleter", as: "deleter",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
}, },
], ],
}); });
@@ -242,7 +242,7 @@ router.post("/", authenticateToken, requireVerifiedEmail, async (req, res) => {
{ {
model: User, model: User,
as: "owner", as: "owner",
attributes: ["id", "username", "firstName", "lastName", "email", "stripeConnectedAccountId"], attributes: ["id", "firstName", "lastName", "email", "stripeConnectedAccountId"],
}, },
], ],
}); });
@@ -307,7 +307,7 @@ router.put("/:id", authenticateToken, async (req, res) => {
{ {
model: User, model: User,
as: "owner", as: "owner",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
}, },
], ],
}); });
@@ -378,7 +378,7 @@ router.delete("/admin/:id", authenticateToken, requireAdmin, async (req, res) =>
{ {
model: User, model: User,
as: "owner", as: "owner",
attributes: ["id", "username", "firstName", "lastName", "email"], attributes: ["id", "firstName", "lastName", "email"],
}, },
], ],
}); });
@@ -422,12 +422,12 @@ router.delete("/admin/:id", authenticateToken, requireAdmin, async (req, res) =>
{ {
model: User, model: User,
as: "owner", as: "owner",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
}, },
{ {
model: User, model: User,
as: "deleter", as: "deleter",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
} }
], ],
}); });
@@ -492,7 +492,7 @@ router.patch("/admin/:id/restore", authenticateToken, requireAdmin, async (req,
{ {
model: User, model: User,
as: "owner", as: "owner",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
} }
], ],
}); });

View File

@@ -64,7 +64,7 @@ router.get("/renting", authenticateToken, async (req, res) => {
{ {
model: User, model: User,
as: "owner", as: "owner",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
}, },
], ],
order: [["createdAt", "DESC"]], order: [["createdAt", "DESC"]],
@@ -92,7 +92,7 @@ router.get("/owning", authenticateToken, async (req, res) => {
{ {
model: User, model: User,
as: "renter", as: "renter",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
}, },
], ],
order: [["createdAt", "DESC"]], order: [["createdAt", "DESC"]],
@@ -141,12 +141,12 @@ router.get("/:id", authenticateToken, async (req, res) => {
{ {
model: User, model: User,
as: "owner", as: "owner",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
}, },
{ {
model: User, model: User,
as: "renter", as: "renter",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
}, },
], ],
}); });
@@ -290,12 +290,12 @@ router.post("/", authenticateToken, requireVerifiedEmail, async (req, res) => {
{ {
model: User, model: User,
as: "owner", as: "owner",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
}, },
{ {
model: User, model: User,
as: "renter", as: "renter",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
}, },
], ],
}); });
@@ -351,14 +351,13 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
{ {
model: User, model: User,
as: "owner", as: "owner",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
}, },
{ {
model: User, model: User,
as: "renter", as: "renter",
attributes: [ attributes: [
"id", "id",
"username",
"firstName", "firstName",
"lastName", "lastName",
"stripeCustomerId", "stripeCustomerId",
@@ -431,12 +430,12 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
{ {
model: User, model: User,
as: "owner", as: "owner",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
}, },
{ {
model: User, model: User,
as: "renter", as: "renter",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
}, },
], ],
}); });
@@ -523,12 +522,12 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
{ {
model: User, model: User,
as: "owner", as: "owner",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
}, },
{ {
model: User, model: User,
as: "renter", as: "renter",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
}, },
], ],
}); });
@@ -600,12 +599,12 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
{ {
model: User, model: User,
as: "owner", as: "owner",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
}, },
{ {
model: User, model: User,
as: "renter", as: "renter",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
}, },
], ],
}); });
@@ -634,12 +633,12 @@ router.put("/:id/decline", authenticateToken, async (req, res) => {
{ {
model: User, model: User,
as: "owner", as: "owner",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
}, },
{ {
model: User, model: User,
as: "renter", as: "renter",
attributes: ["id", "username", "firstName", "lastName", "email"], attributes: ["id", "firstName", "lastName", "email"],
}, },
], ],
}); });
@@ -674,12 +673,12 @@ router.put("/:id/decline", authenticateToken, async (req, res) => {
{ {
model: User, model: User,
as: "owner", as: "owner",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
}, },
{ {
model: User, model: User,
as: "renter", as: "renter",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
}, },
], ],
}); });
@@ -832,12 +831,12 @@ router.post("/:id/mark-completed", authenticateToken, async (req, res) => {
{ {
model: User, model: User,
as: "owner", as: "owner",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
}, },
{ {
model: User, model: User,
as: "renter", as: "renter",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
}, },
], ],
}); });
@@ -1048,12 +1047,12 @@ router.post("/:id/cancel", authenticateToken, async (req, res) => {
{ {
model: User, model: User,
as: "owner", as: "owner",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
}, },
{ {
model: User, model: User,
as: "renter", as: "renter",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
}, },
], ],
}); });
@@ -1143,12 +1142,12 @@ router.post("/:id/mark-return", authenticateToken, async (req, res) => {
{ {
model: User, model: User,
as: "owner", as: "owner",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
}, },
{ {
model: User, model: User,
as: "renter", as: "renter",
attributes: ["id", "username", "firstName", "lastName"], attributes: ["id", "firstName", "lastName"],
}, },
], ],
}); });