Skip to content

Commit 0a33da5

Browse files
committed
created and tested authentication model
1 parent 50bf585 commit 0a33da5

File tree

2 files changed

+291
-0
lines changed

2 files changed

+291
-0
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Schema, model, Types } from 'mongoose';
2+
3+
// interface that defines what fields an authentication model should have
4+
// ? means optional
5+
// Types.ObjectId is MongoDB's special ID type
6+
export interface IAuth {
7+
userID: Types.ObjectId; // reference to userID in users collection
8+
email: string;
9+
passwordHash: string; // this is the hash (NOT plain text)
10+
createdAt?: Date;
11+
updatedAt?: Date;
12+
lastLogin?: Date;
13+
passwordChangedAt?: Date; // track when password was last changed
14+
}
15+
16+
// Schema that defines structure and validation rules
17+
const AuthSchema = new Schema<IAuth>(
18+
{
19+
userID: {
20+
type: Schema.Types.ObjectId,
21+
// create relationship between collections
22+
ref: 'User', // references the user model
23+
required: true,
24+
index: true, // index for faster lookup
25+
unique: true // one auth doc per user
26+
},
27+
email: {
28+
type: String,
29+
required: true,
30+
unique: true,
31+
lowercase: true,
32+
trim: true,
33+
index: true,
34+
match: [ // email format validation
35+
/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/,
36+
'Please provide a valid email address'
37+
]
38+
},
39+
passwordHash: {
40+
type: String,
41+
required: true,
42+
select: false, // Don't include in queries (for security)
43+
},
44+
lastLogin: {
45+
type: Date,
46+
default: null
47+
},
48+
passwordChangedAt: {
49+
type: Date,
50+
default: null
51+
}
52+
},
53+
{
54+
timestamps: true // automatically adds createdAt and updatedAt
55+
}
56+
);
57+
58+
// add some middleware to autoupdate passwordChangedAt when password actually changes
59+
// this will be used later on to invalidate old JWT tokens
60+
//
61+
// pre(save) runs before document is saved to DB
62+
AuthSchema.pre('save', function (next) {
63+
// only update if passwordHash was modified (not on creation)
64+
if (this.isModified('passwordHash') && !this.isNew) {
65+
this.passwordChangedAt = new Date();
66+
}
67+
// continue with the next operation
68+
next();
69+
});
70+
71+
// create and export the Authentication model
72+
// this will be used to interact with authenticaion collection
73+
export const Auth = model<IAuth>('Auth', AuthSchema);
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import mongoose from 'mongoose';
2+
import dotenv from 'dotenv';
3+
import { User } from './user_model';
4+
import { Auth } from './auth_model';
5+
import { hashPassword } from '../utils/password_hash';
6+
7+
dotenv.config();
8+
9+
async function testAuthModel() {
10+
try {
11+
// Connect to MongoDB
12+
const MONGODB_URI = process.env.MONGO_URI;
13+
if (!MONGODB_URI) {
14+
throw new Error('MONGO_URI not set in .env');
15+
}
16+
17+
await mongoose.connect(MONGODB_URI);
18+
console.log('Connected to MongoDB\n');
19+
20+
// Clean up any existing test data first
21+
await User.deleteMany({ email: '[email protected]' });
22+
await Auth.deleteMany({ email: '[email protected]' });
23+
24+
// ========================================
25+
// Test 1: Create a user
26+
// ========================================
27+
console.log('Test 1: Creating user...');
28+
const newUser = await User.create({
29+
name: 'Test User', // Changed from displayName to name
30+
31+
});
32+
console.log('User created:', {
33+
_id: newUser._id,
34+
email: newUser.email,
35+
name: newUser.name // Changed from displayName to name
36+
});
37+
38+
// ========================================
39+
// Test 2: Create authentication for that user
40+
// ========================================
41+
console.log('\nTest 2: Creating authentication...');
42+
const hashedPassword = await hashPassword('testPassword123');
43+
const newAuth = await Auth.create({
44+
userID: newUser._id,
45+
46+
passwordHash: hashedPassword
47+
});
48+
console.log('Auth created:', {
49+
_id: newAuth._id,
50+
email: newAuth.email,
51+
userID: newAuth.userID,
52+
hasTimestamps: !!(newAuth.createdAt && newAuth.updatedAt)
53+
});
54+
55+
// ========================================
56+
// Test 3: Verify passwordHash is NOT returned by default (select: false)
57+
// ========================================
58+
console.log('\nTest 3: Testing select: false on passwordHash...');
59+
const authWithoutHash = await Auth.findOne({ email: '[email protected]' });
60+
console.log('Query without select:', {
61+
hasPasswordHash: 'passwordHash' in (authWithoutHash || {}),
62+
expected: false
63+
});
64+
65+
// ========================================
66+
// Test 4: Explicitly select passwordHash with +passwordHash
67+
// ========================================
68+
console.log('\nTest 4: Explicitly selecting passwordHash...');
69+
const authWithHash = await Auth.findOne({
70+
71+
}).select('+passwordHash');
72+
console.log('Query with select(+passwordHash):', {
73+
hasPasswordHash: !!(authWithHash?.passwordHash),
74+
hashLength: authWithHash?.passwordHash?.length,
75+
expected: 60
76+
});
77+
78+
// ========================================
79+
// Test 5: Test passwordChangedAt middleware
80+
// ========================================
81+
console.log('\nTest 5: Testing passwordChangedAt middleware...');
82+
console.log(' Initial passwordChangedAt:', authWithHash?.passwordChangedAt || 'null (correct for new doc)');
83+
84+
// Wait 1 second to ensure timestamp difference
85+
await new Promise(resolve => setTimeout(resolve, 1000));
86+
87+
// Update password
88+
if (authWithHash) {
89+
const newHashedPassword = await hashPassword('newPassword456');
90+
authWithHash.passwordHash = newHashedPassword;
91+
await authWithHash.save(); // Middleware runs here!
92+
}
93+
94+
const updatedAuth = await Auth.findOne({
95+
96+
}).select('+passwordHash');
97+
console.log('After password change:', {
98+
passwordChangedAt: updatedAuth?.passwordChangedAt,
99+
wasUpdated: !!updatedAuth?.passwordChangedAt
100+
});
101+
102+
// ========================================
103+
// Test 6: Test lastLogin update
104+
// ========================================
105+
console.log('\nTest 6: Testing lastLogin update...');
106+
const beforeLogin = await Auth.findOne({ email: '[email protected]' });
107+
console.log(' Before login - lastLogin:', beforeLogin?.lastLogin || 'null');
108+
109+
// Simulate login
110+
await Auth.updateOne(
111+
{ email: '[email protected]' },
112+
{ lastLogin: new Date() }
113+
);
114+
115+
const afterLogin = await Auth.findOne({ email: '[email protected]' });
116+
console.log('After login - lastLogin:', afterLogin?.lastLogin);
117+
118+
// ========================================
119+
// Test 7: Populate user data from auth
120+
// ========================================
121+
console.log('\nTest 7: Testing populate (join with User)...');
122+
const authWithUser = await Auth.findOne({
123+
124+
}).populate('userID');
125+
console.log('Auth with populated user:', {
126+
authId: authWithUser?._id,
127+
email: authWithUser?.email,
128+
populatedUser: authWithUser?.userID
129+
});
130+
131+
// ========================================
132+
// Test 8: Test email validation
133+
// ========================================
134+
console.log('\nTest 8: Testing email validation...');
135+
try {
136+
const testUser2 = await User.create({
137+
name: 'Test User 2', // Changed from displayName to name
138+
email: 'invalid-email' // Invalid format
139+
});
140+
const invalidAuth = await Auth.create({
141+
userID: testUser2._id,
142+
email: 'invalid-email',
143+
passwordHash: 'hash'
144+
});
145+
console.log('Email validation FAILED - invalid email was accepted');
146+
} catch (error: any) {
147+
console.log('Email validation PASSED - invalid email rejected:',
148+
error.message.includes('valid email') ? 'Correct error message' : error.message
149+
);
150+
}
151+
152+
// ========================================
153+
// Test 9: Test unique email constraint
154+
// ========================================
155+
console.log('\nTest 9: Testing unique email constraint...');
156+
try {
157+
const testUser3 = await User.create({
158+
name: 'Test User 3', // Changed from displayName to name
159+
160+
});
161+
await Auth.create({
162+
userID: testUser3._id,
163+
164+
passwordHash: 'hash1'
165+
});
166+
167+
const testUser4 = await User.create({
168+
name: 'Test User 4', // Changed from displayName to name
169+
170+
});
171+
await Auth.create({
172+
userID: testUser4._id,
173+
email: '[email protected]', // Same email!
174+
passwordHash: 'hash2'
175+
});
176+
console.log('Unique constraint FAILED - duplicate email accepted');
177+
} catch (error: any) {
178+
console.log('Unique constraint PASSED - duplicate email rejected');
179+
}
180+
181+
// ========================================
182+
// Test 10: Test unique userID constraint
183+
// ========================================
184+
console.log('\nTest 10: Testing unique userID constraint...');
185+
try {
186+
await Auth.create({
187+
userID: newUser._id, // Same userID as first auth!
188+
189+
passwordHash: 'hash'
190+
});
191+
console.log('Unique userID constraint FAILED - duplicate userID accepted');
192+
} catch (error: any) {
193+
console.log('Unique userID constraint PASSED - duplicate userID rejected');
194+
}
195+
196+
// ========================================
197+
// Clean up all test data
198+
// ========================================
199+
console.log('\nCleaning up test data...');
200+
await User.deleteMany({
201+
202+
});
203+
await Auth.deleteMany({
204+
205+
});
206+
console.log('Test data cleaned up');
207+
208+
console.log('\nAll tests passed!');
209+
210+
} catch (error) {
211+
console.error('Test failed:', error);
212+
} finally {
213+
await mongoose.connection.close();
214+
console.log('\nDisconnected from MongoDB');
215+
}
216+
}
217+
218+
testAuthModel();

0 commit comments

Comments
 (0)