Yash Goyal commited on
Commit
2070fe3
·
0 Parent(s):

Correction

Browse files
.DS_Store ADDED
Binary file (6.15 kB). View file
 
.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ *.jpg filter=lfs diff=lfs merge=lfs -text
2
+ *.png filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Node 18
2
+ FROM node:18
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Copy everything (safe)
8
+ COPY . .
9
+
10
+ # If your code is in a 'backend' subfolder, uncomment this:
11
+ WORKDIR /app/backend
12
+
13
+ # Install dependencies (make sure package.json is in the right place)
14
+ RUN npm install
15
+
16
+ # Expose HF's required port
17
+ EXPOSE 7860
18
+
19
+ # Run the server
20
+ CMD ["node", "server.js"]
README.md ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: PopcornPing
3
+ emoji: 🍿
4
+ colorFrom: yellow
5
+ colorTo: red
6
+ sdk: docker
7
+ app_port: 7860
8
+ ---
backend/.gitignore ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ /node_modules
3
+ /jspm_packages/
4
+
5
+ # Production Build Artifacts
6
+ /dist/
7
+ /build/
8
+ /out/
9
+
10
+ # Environments and Credentials
11
+ # Environment variables file is often used for secrets and configuration
12
+ .env
13
+ .env.*
14
+ # For example: .env.development, .env.production
15
+
16
+ # Logs
17
+ npm-debug.log*
18
+ yarn-debug.log*
19
+ yarn-error.log*
20
+
21
+ # OS generated files
22
+ .DS_Store
23
+ Thumbs.db
backend/config/database.js ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const mongoose = require('mongoose');
2
+
3
+ const connectDB = async () => {
4
+ try {
5
+ await mongoose.connect(process.env.MONGODB_URI);
6
+
7
+ console.log('MongoDB Connected Successfully');
8
+ } catch (error) {
9
+ console.error('MongoDB Connection Error:', error);
10
+ process.exit(1);
11
+ }
12
+ };
13
+
14
+ module.exports = connectDB;
backend/config/passport.js ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const passport = require('passport');
2
+ const LocalStrategy = require('passport-local').Strategy;
3
+ const GoogleStrategy = require('passport-google-oauth20').Strategy;
4
+ const User = require('../models/User');
5
+
6
+ // Helper: Timeout Promise
7
+ const timeout = (ms) => new Promise((_, reject) =>
8
+ setTimeout(() => reject(new Error('Database operation timed out')), ms)
9
+ );
10
+
11
+ passport.serializeUser((user, done) => {
12
+ console.log('Serializing user:', user.id);
13
+ done(null, user.id);
14
+ });
15
+
16
+ passport.deserializeUser(async (id, done) => {
17
+ try {
18
+ console.log('Deserializing user:', id);
19
+ const user = await Promise.race([
20
+ User.findById(id),
21
+ timeout(5000)
22
+ ]);
23
+ console.log('User deserialized:', user ? user.email : 'not found');
24
+ done(null, user);
25
+ } catch (error) {
26
+ console.error('Deserialize Error:', error);
27
+ done(error, null);
28
+ }
29
+ });
30
+
31
+ passport.use(new LocalStrategy({
32
+ usernameField: 'email',
33
+ }, async (email, password, done) => {
34
+ try {
35
+ const user = await Promise.race([
36
+ User.findOne({ email: email.toLowerCase() }),
37
+ timeout(5000)
38
+ ]);
39
+
40
+ if (!user) return done(null, false, { message: 'Invalid email or password' });
41
+
42
+ const isMatch = await user.comparePassword(password);
43
+ if (!isMatch) return done(null, false, { message: 'Invalid email or password' });
44
+
45
+ return done(null, user);
46
+ } catch (error) {
47
+ return done(error);
48
+ }
49
+ }));
50
+
51
+ // --- GOOGLE STRATEGY ---
52
+ passport.use(new GoogleStrategy({
53
+ clientID: process.env.GOOGLE_CLIENT_ID,
54
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
55
+ // IMPORTANT: Use BACKEND_URL for callback
56
+ callbackURL: `${process.env.BACKEND_URL}/api/auth/google/callback`,
57
+ proxy: true
58
+ }, async (accessToken, refreshToken, profile, done) => {
59
+ try {
60
+ console.log('>>> Google Strategy: Processing profile for', profile.displayName);
61
+ console.log('>>> Profile email:', profile.emails[0].value);
62
+
63
+ // Check if user exists with Google ID
64
+ let user = await Promise.race([
65
+ User.findOne({ googleId: profile.id }),
66
+ timeout(5000)
67
+ ]);
68
+
69
+ if (user) {
70
+ console.log('>>> Found existing user by Google ID:', user.email);
71
+ return done(null, user);
72
+ }
73
+
74
+ // Check if user exists with email
75
+ user = await Promise.race([
76
+ User.findOne({ email: profile.emails[0].value.toLowerCase() }),
77
+ timeout(5000)
78
+ ]);
79
+
80
+ if (user) {
81
+ console.log('>>> Found existing user by email, linking Google ID:', user.email);
82
+ user.googleId = profile.id;
83
+ user.avatar = profile.photos[0]?.value || user.avatar;
84
+ await user.save();
85
+ return done(null, user);
86
+ }
87
+
88
+ // Create new user
89
+ console.log('>>> Creating new user:', profile.emails[0].value);
90
+ user = await User.create({
91
+ googleId: profile.id,
92
+ email: profile.emails[0].value.toLowerCase(),
93
+ username: profile.displayName,
94
+ avatar: profile.photos[0]?.value || '',
95
+ });
96
+
97
+ console.log('>>> New user created:', user.email);
98
+ done(null, user);
99
+ } catch (error) {
100
+ console.error('>>> Google Strategy Error:', error);
101
+ done(error, null);
102
+ }
103
+ }));
104
+
105
+ module.exports = passport;
backend/middleware/auth.js ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ const isAuthenticated = (req, res, next) => {
2
+ if (req.isAuthenticated()) {
3
+ return next();
4
+ }
5
+ res.status(401).json({ message: 'Unauthorized. Please log in.' });
6
+ };
7
+
8
+ module.exports = { isAuthenticated };
backend/minimal.js ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const http = require('http');
2
+
3
+ // 1. MUST use port 7860
4
+ const PORT = 7860;
5
+
6
+ // 2. MUST use host '0.0.0.0' (Not localhost!)
7
+ const HOST = '0.0.0.0';
8
+
9
+ const server = http.createServer((req, res) => {
10
+ console.log(`Request received: ${req.url}`); // Log every hit
11
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
12
+ res.end('SUCCESS: The Backend is Reachable on Port 7860!');
13
+ });
14
+
15
+ server.listen(PORT, HOST, () => {
16
+ console.log(`Minimal server running at http://${HOST}:${PORT}/`);
17
+ });
backend/models/Room.js ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const mongoose = require('mongoose');
2
+
3
+ const roomSchema = new mongoose.Schema({
4
+ roomId: {
5
+ type: String,
6
+ required: true,
7
+ unique: true,
8
+ },
9
+ name: {
10
+ type: String,
11
+ required: true,
12
+ },
13
+ host: {
14
+ type: mongoose.Schema.Types.ObjectId,
15
+ ref: 'User',
16
+ required: true,
17
+ },
18
+ participants: [{
19
+ type: mongoose.Schema.Types.ObjectId,
20
+ ref: 'User',
21
+ }],
22
+ isActive: {
23
+ type: Boolean,
24
+ default: true,
25
+ },
26
+ createdAt: {
27
+ type: Date,
28
+ default: Date.now,
29
+ },
30
+ expiresAt: {
31
+ type: Date,
32
+ default: () => new Date(+new Date() + 24*60*60*1000), // 24 hours
33
+ },
34
+ });
35
+
36
+ module.exports = mongoose.model('Room', roomSchema);
backend/models/User.js ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const mongoose = require('mongoose');
2
+ const bcrypt = require('bcryptjs');
3
+
4
+ const userSchema = new mongoose.Schema({
5
+ googleId: {
6
+ type: String,
7
+ sparse: true,
8
+ },
9
+ email: {
10
+ type: String,
11
+ required: true,
12
+ unique: true,
13
+ lowercase: true,
14
+ },
15
+ username: {
16
+ type: String,
17
+ required: true,
18
+ },
19
+ password: {
20
+ type: String,
21
+ required: function() {
22
+ return !this.googleId;
23
+ },
24
+ },
25
+ avatar: {
26
+ type: String,
27
+ default: '',
28
+ },
29
+ createdAt: {
30
+ type: Date,
31
+ default: Date.now,
32
+ },
33
+ });
34
+
35
+ userSchema.pre('save', async function(next) {
36
+ if (!this.isModified('password')) return next();
37
+
38
+ if (this.password) {
39
+ this.password = await bcrypt.hash(this.password, 10);
40
+ }
41
+ next();
42
+ });
43
+
44
+ userSchema.methods.comparePassword = async function(candidatePassword) {
45
+ return await bcrypt.compare(candidatePassword, this.password);
46
+ };
47
+
48
+ module.exports = mongoose.model('User', userSchema);
backend/package-lock.json ADDED
@@ -0,0 +1,2051 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "popcornping-backend",
3
+ "version": "2.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "popcornping-backend",
9
+ "version": "2.0.0",
10
+ "dependencies": {
11
+ "bcryptjs": "^2.4.3",
12
+ "connect-mongo": "^5.1.0",
13
+ "cors": "^2.8.5",
14
+ "dotenv": "^16.6.1",
15
+ "express": "^4.18.2",
16
+ "express-session": "^1.17.3",
17
+ "mongoose": "^8.0.3",
18
+ "passport": "^0.7.0",
19
+ "passport-google-oauth20": "^2.0.0",
20
+ "passport-local": "^1.0.0",
21
+ "socket.io": "^4.6.0"
22
+ },
23
+ "devDependencies": {
24
+ "nodemon": "^3.0.2"
25
+ }
26
+ },
27
+ "node_modules/@mongodb-js/saslprep": {
28
+ "version": "1.4.0",
29
+ "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.0.tgz",
30
+ "integrity": "sha512-ZHzx7Z3rdlWL1mECydvpryWN/ETXJiCxdgQKTAH+djzIPe77HdnSizKBDi1TVDXZjXyOj2IqEG/vPw71ULF06w==",
31
+ "license": "MIT",
32
+ "dependencies": {
33
+ "sparse-bitfield": "^3.0.3"
34
+ }
35
+ },
36
+ "node_modules/@socket.io/component-emitter": {
37
+ "version": "3.1.2",
38
+ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
39
+ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
40
+ "license": "MIT"
41
+ },
42
+ "node_modules/@types/cors": {
43
+ "version": "2.8.19",
44
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
45
+ "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
46
+ "license": "MIT",
47
+ "dependencies": {
48
+ "@types/node": "*"
49
+ }
50
+ },
51
+ "node_modules/@types/node": {
52
+ "version": "25.0.0",
53
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.0.tgz",
54
+ "integrity": "sha512-rl78HwuZlaDIUSeUKkmogkhebA+8K1Hy7tddZuJ3D0xV8pZSfsYGTsliGUol1JPzu9EKnTxPC4L1fiWouStRew==",
55
+ "license": "MIT",
56
+ "dependencies": {
57
+ "undici-types": "~7.16.0"
58
+ }
59
+ },
60
+ "node_modules/@types/webidl-conversions": {
61
+ "version": "7.0.3",
62
+ "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
63
+ "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
64
+ "license": "MIT"
65
+ },
66
+ "node_modules/@types/whatwg-url": {
67
+ "version": "11.0.5",
68
+ "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz",
69
+ "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==",
70
+ "license": "MIT",
71
+ "dependencies": {
72
+ "@types/webidl-conversions": "*"
73
+ }
74
+ },
75
+ "node_modules/accepts": {
76
+ "version": "1.3.8",
77
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
78
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
79
+ "license": "MIT",
80
+ "dependencies": {
81
+ "mime-types": "~2.1.34",
82
+ "negotiator": "0.6.3"
83
+ },
84
+ "engines": {
85
+ "node": ">= 0.6"
86
+ }
87
+ },
88
+ "node_modules/anymatch": {
89
+ "version": "3.1.3",
90
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
91
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
92
+ "dev": true,
93
+ "license": "ISC",
94
+ "dependencies": {
95
+ "normalize-path": "^3.0.0",
96
+ "picomatch": "^2.0.4"
97
+ },
98
+ "engines": {
99
+ "node": ">= 8"
100
+ }
101
+ },
102
+ "node_modules/array-flatten": {
103
+ "version": "1.1.1",
104
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
105
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
106
+ "license": "MIT"
107
+ },
108
+ "node_modules/asn1.js": {
109
+ "version": "5.4.1",
110
+ "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
111
+ "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
112
+ "license": "MIT",
113
+ "dependencies": {
114
+ "bn.js": "^4.0.0",
115
+ "inherits": "^2.0.1",
116
+ "minimalistic-assert": "^1.0.0",
117
+ "safer-buffer": "^2.1.0"
118
+ }
119
+ },
120
+ "node_modules/balanced-match": {
121
+ "version": "1.0.2",
122
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
123
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
124
+ "dev": true,
125
+ "license": "MIT"
126
+ },
127
+ "node_modules/base64id": {
128
+ "version": "2.0.0",
129
+ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
130
+ "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
131
+ "license": "MIT",
132
+ "engines": {
133
+ "node": "^4.5.0 || >= 5.9"
134
+ }
135
+ },
136
+ "node_modules/base64url": {
137
+ "version": "3.0.1",
138
+ "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
139
+ "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
140
+ "license": "MIT",
141
+ "engines": {
142
+ "node": ">=6.0.0"
143
+ }
144
+ },
145
+ "node_modules/bcryptjs": {
146
+ "version": "2.4.3",
147
+ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
148
+ "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
149
+ "license": "MIT"
150
+ },
151
+ "node_modules/binary-extensions": {
152
+ "version": "2.3.0",
153
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
154
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
155
+ "dev": true,
156
+ "license": "MIT",
157
+ "engines": {
158
+ "node": ">=8"
159
+ },
160
+ "funding": {
161
+ "url": "https://github.com/sponsors/sindresorhus"
162
+ }
163
+ },
164
+ "node_modules/bn.js": {
165
+ "version": "4.12.2",
166
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
167
+ "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
168
+ "license": "MIT"
169
+ },
170
+ "node_modules/body-parser": {
171
+ "version": "1.20.4",
172
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
173
+ "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
174
+ "license": "MIT",
175
+ "dependencies": {
176
+ "bytes": "~3.1.2",
177
+ "content-type": "~1.0.5",
178
+ "debug": "2.6.9",
179
+ "depd": "2.0.0",
180
+ "destroy": "~1.2.0",
181
+ "http-errors": "~2.0.1",
182
+ "iconv-lite": "~0.4.24",
183
+ "on-finished": "~2.4.1",
184
+ "qs": "~6.14.0",
185
+ "raw-body": "~2.5.3",
186
+ "type-is": "~1.6.18",
187
+ "unpipe": "~1.0.0"
188
+ },
189
+ "engines": {
190
+ "node": ">= 0.8",
191
+ "npm": "1.2.8000 || >= 1.4.16"
192
+ }
193
+ },
194
+ "node_modules/body-parser/node_modules/debug": {
195
+ "version": "2.6.9",
196
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
197
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
198
+ "license": "MIT",
199
+ "dependencies": {
200
+ "ms": "2.0.0"
201
+ }
202
+ },
203
+ "node_modules/body-parser/node_modules/ms": {
204
+ "version": "2.0.0",
205
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
206
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
207
+ "license": "MIT"
208
+ },
209
+ "node_modules/brace-expansion": {
210
+ "version": "1.1.12",
211
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
212
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
213
+ "dev": true,
214
+ "license": "MIT",
215
+ "dependencies": {
216
+ "balanced-match": "^1.0.0",
217
+ "concat-map": "0.0.1"
218
+ }
219
+ },
220
+ "node_modules/braces": {
221
+ "version": "3.0.3",
222
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
223
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
224
+ "dev": true,
225
+ "license": "MIT",
226
+ "dependencies": {
227
+ "fill-range": "^7.1.1"
228
+ },
229
+ "engines": {
230
+ "node": ">=8"
231
+ }
232
+ },
233
+ "node_modules/bson": {
234
+ "version": "6.10.4",
235
+ "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz",
236
+ "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==",
237
+ "license": "Apache-2.0",
238
+ "engines": {
239
+ "node": ">=16.20.1"
240
+ }
241
+ },
242
+ "node_modules/bytes": {
243
+ "version": "3.1.2",
244
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
245
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
246
+ "license": "MIT",
247
+ "engines": {
248
+ "node": ">= 0.8"
249
+ }
250
+ },
251
+ "node_modules/call-bind-apply-helpers": {
252
+ "version": "1.0.2",
253
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
254
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
255
+ "license": "MIT",
256
+ "dependencies": {
257
+ "es-errors": "^1.3.0",
258
+ "function-bind": "^1.1.2"
259
+ },
260
+ "engines": {
261
+ "node": ">= 0.4"
262
+ }
263
+ },
264
+ "node_modules/call-bound": {
265
+ "version": "1.0.4",
266
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
267
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
268
+ "license": "MIT",
269
+ "dependencies": {
270
+ "call-bind-apply-helpers": "^1.0.2",
271
+ "get-intrinsic": "^1.3.0"
272
+ },
273
+ "engines": {
274
+ "node": ">= 0.4"
275
+ },
276
+ "funding": {
277
+ "url": "https://github.com/sponsors/ljharb"
278
+ }
279
+ },
280
+ "node_modules/chokidar": {
281
+ "version": "3.6.0",
282
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
283
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
284
+ "dev": true,
285
+ "license": "MIT",
286
+ "dependencies": {
287
+ "anymatch": "~3.1.2",
288
+ "braces": "~3.0.2",
289
+ "glob-parent": "~5.1.2",
290
+ "is-binary-path": "~2.1.0",
291
+ "is-glob": "~4.0.1",
292
+ "normalize-path": "~3.0.0",
293
+ "readdirp": "~3.6.0"
294
+ },
295
+ "engines": {
296
+ "node": ">= 8.10.0"
297
+ },
298
+ "funding": {
299
+ "url": "https://paulmillr.com/funding/"
300
+ },
301
+ "optionalDependencies": {
302
+ "fsevents": "~2.3.2"
303
+ }
304
+ },
305
+ "node_modules/concat-map": {
306
+ "version": "0.0.1",
307
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
308
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
309
+ "dev": true,
310
+ "license": "MIT"
311
+ },
312
+ "node_modules/connect-mongo": {
313
+ "version": "5.1.0",
314
+ "resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-5.1.0.tgz",
315
+ "integrity": "sha512-xT0vxQLqyqoUTxPLzlP9a/u+vir0zNkhiy9uAdHjSCcUUf7TS5b55Icw8lVyYFxfemP3Mf9gdwUOgeF3cxCAhw==",
316
+ "license": "MIT",
317
+ "dependencies": {
318
+ "debug": "^4.3.1",
319
+ "kruptein": "^3.0.0"
320
+ },
321
+ "engines": {
322
+ "node": ">=12.9.0"
323
+ },
324
+ "peerDependencies": {
325
+ "express-session": "^1.17.1",
326
+ "mongodb": ">= 5.1.0 < 7"
327
+ }
328
+ },
329
+ "node_modules/content-disposition": {
330
+ "version": "0.5.4",
331
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
332
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
333
+ "license": "MIT",
334
+ "dependencies": {
335
+ "safe-buffer": "5.2.1"
336
+ },
337
+ "engines": {
338
+ "node": ">= 0.6"
339
+ }
340
+ },
341
+ "node_modules/content-type": {
342
+ "version": "1.0.5",
343
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
344
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
345
+ "license": "MIT",
346
+ "engines": {
347
+ "node": ">= 0.6"
348
+ }
349
+ },
350
+ "node_modules/cookie": {
351
+ "version": "0.7.2",
352
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
353
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
354
+ "license": "MIT",
355
+ "engines": {
356
+ "node": ">= 0.6"
357
+ }
358
+ },
359
+ "node_modules/cookie-signature": {
360
+ "version": "1.0.7",
361
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
362
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
363
+ "license": "MIT"
364
+ },
365
+ "node_modules/cors": {
366
+ "version": "2.8.5",
367
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
368
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
369
+ "license": "MIT",
370
+ "dependencies": {
371
+ "object-assign": "^4",
372
+ "vary": "^1"
373
+ },
374
+ "engines": {
375
+ "node": ">= 0.10"
376
+ }
377
+ },
378
+ "node_modules/debug": {
379
+ "version": "4.4.3",
380
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
381
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
382
+ "license": "MIT",
383
+ "dependencies": {
384
+ "ms": "^2.1.3"
385
+ },
386
+ "engines": {
387
+ "node": ">=6.0"
388
+ },
389
+ "peerDependenciesMeta": {
390
+ "supports-color": {
391
+ "optional": true
392
+ }
393
+ }
394
+ },
395
+ "node_modules/depd": {
396
+ "version": "2.0.0",
397
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
398
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
399
+ "license": "MIT",
400
+ "engines": {
401
+ "node": ">= 0.8"
402
+ }
403
+ },
404
+ "node_modules/destroy": {
405
+ "version": "1.2.0",
406
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
407
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
408
+ "license": "MIT",
409
+ "engines": {
410
+ "node": ">= 0.8",
411
+ "npm": "1.2.8000 || >= 1.4.16"
412
+ }
413
+ },
414
+ "node_modules/dotenv": {
415
+ "version": "16.6.1",
416
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
417
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
418
+ "license": "BSD-2-Clause",
419
+ "engines": {
420
+ "node": ">=12"
421
+ },
422
+ "funding": {
423
+ "url": "https://dotenvx.com"
424
+ }
425
+ },
426
+ "node_modules/dunder-proto": {
427
+ "version": "1.0.1",
428
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
429
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
430
+ "license": "MIT",
431
+ "dependencies": {
432
+ "call-bind-apply-helpers": "^1.0.1",
433
+ "es-errors": "^1.3.0",
434
+ "gopd": "^1.2.0"
435
+ },
436
+ "engines": {
437
+ "node": ">= 0.4"
438
+ }
439
+ },
440
+ "node_modules/ee-first": {
441
+ "version": "1.1.1",
442
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
443
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
444
+ "license": "MIT"
445
+ },
446
+ "node_modules/encodeurl": {
447
+ "version": "2.0.0",
448
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
449
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
450
+ "license": "MIT",
451
+ "engines": {
452
+ "node": ">= 0.8"
453
+ }
454
+ },
455
+ "node_modules/engine.io": {
456
+ "version": "6.6.4",
457
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
458
+ "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
459
+ "license": "MIT",
460
+ "dependencies": {
461
+ "@types/cors": "^2.8.12",
462
+ "@types/node": ">=10.0.0",
463
+ "accepts": "~1.3.4",
464
+ "base64id": "2.0.0",
465
+ "cookie": "~0.7.2",
466
+ "cors": "~2.8.5",
467
+ "debug": "~4.3.1",
468
+ "engine.io-parser": "~5.2.1",
469
+ "ws": "~8.17.1"
470
+ },
471
+ "engines": {
472
+ "node": ">=10.2.0"
473
+ }
474
+ },
475
+ "node_modules/engine.io-parser": {
476
+ "version": "5.2.3",
477
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
478
+ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
479
+ "license": "MIT",
480
+ "engines": {
481
+ "node": ">=10.0.0"
482
+ }
483
+ },
484
+ "node_modules/engine.io/node_modules/debug": {
485
+ "version": "4.3.7",
486
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
487
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
488
+ "license": "MIT",
489
+ "dependencies": {
490
+ "ms": "^2.1.3"
491
+ },
492
+ "engines": {
493
+ "node": ">=6.0"
494
+ },
495
+ "peerDependenciesMeta": {
496
+ "supports-color": {
497
+ "optional": true
498
+ }
499
+ }
500
+ },
501
+ "node_modules/es-define-property": {
502
+ "version": "1.0.1",
503
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
504
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
505
+ "license": "MIT",
506
+ "engines": {
507
+ "node": ">= 0.4"
508
+ }
509
+ },
510
+ "node_modules/es-errors": {
511
+ "version": "1.3.0",
512
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
513
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
514
+ "license": "MIT",
515
+ "engines": {
516
+ "node": ">= 0.4"
517
+ }
518
+ },
519
+ "node_modules/es-object-atoms": {
520
+ "version": "1.1.1",
521
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
522
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
523
+ "license": "MIT",
524
+ "dependencies": {
525
+ "es-errors": "^1.3.0"
526
+ },
527
+ "engines": {
528
+ "node": ">= 0.4"
529
+ }
530
+ },
531
+ "node_modules/escape-html": {
532
+ "version": "1.0.3",
533
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
534
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
535
+ "license": "MIT"
536
+ },
537
+ "node_modules/etag": {
538
+ "version": "1.8.1",
539
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
540
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
541
+ "license": "MIT",
542
+ "engines": {
543
+ "node": ">= 0.6"
544
+ }
545
+ },
546
+ "node_modules/express": {
547
+ "version": "4.22.1",
548
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
549
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
550
+ "license": "MIT",
551
+ "dependencies": {
552
+ "accepts": "~1.3.8",
553
+ "array-flatten": "1.1.1",
554
+ "body-parser": "~1.20.3",
555
+ "content-disposition": "~0.5.4",
556
+ "content-type": "~1.0.4",
557
+ "cookie": "~0.7.1",
558
+ "cookie-signature": "~1.0.6",
559
+ "debug": "2.6.9",
560
+ "depd": "2.0.0",
561
+ "encodeurl": "~2.0.0",
562
+ "escape-html": "~1.0.3",
563
+ "etag": "~1.8.1",
564
+ "finalhandler": "~1.3.1",
565
+ "fresh": "~0.5.2",
566
+ "http-errors": "~2.0.0",
567
+ "merge-descriptors": "1.0.3",
568
+ "methods": "~1.1.2",
569
+ "on-finished": "~2.4.1",
570
+ "parseurl": "~1.3.3",
571
+ "path-to-regexp": "~0.1.12",
572
+ "proxy-addr": "~2.0.7",
573
+ "qs": "~6.14.0",
574
+ "range-parser": "~1.2.1",
575
+ "safe-buffer": "5.2.1",
576
+ "send": "~0.19.0",
577
+ "serve-static": "~1.16.2",
578
+ "setprototypeof": "1.2.0",
579
+ "statuses": "~2.0.1",
580
+ "type-is": "~1.6.18",
581
+ "utils-merge": "1.0.1",
582
+ "vary": "~1.1.2"
583
+ },
584
+ "engines": {
585
+ "node": ">= 0.10.0"
586
+ },
587
+ "funding": {
588
+ "type": "opencollective",
589
+ "url": "https://opencollective.com/express"
590
+ }
591
+ },
592
+ "node_modules/express-session": {
593
+ "version": "1.18.2",
594
+ "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz",
595
+ "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==",
596
+ "license": "MIT",
597
+ "peer": true,
598
+ "dependencies": {
599
+ "cookie": "0.7.2",
600
+ "cookie-signature": "1.0.7",
601
+ "debug": "2.6.9",
602
+ "depd": "~2.0.0",
603
+ "on-headers": "~1.1.0",
604
+ "parseurl": "~1.3.3",
605
+ "safe-buffer": "5.2.1",
606
+ "uid-safe": "~2.1.5"
607
+ },
608
+ "engines": {
609
+ "node": ">= 0.8.0"
610
+ }
611
+ },
612
+ "node_modules/express-session/node_modules/debug": {
613
+ "version": "2.6.9",
614
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
615
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
616
+ "license": "MIT",
617
+ "dependencies": {
618
+ "ms": "2.0.0"
619
+ }
620
+ },
621
+ "node_modules/express-session/node_modules/ms": {
622
+ "version": "2.0.0",
623
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
624
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
625
+ "license": "MIT"
626
+ },
627
+ "node_modules/express/node_modules/debug": {
628
+ "version": "2.6.9",
629
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
630
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
631
+ "license": "MIT",
632
+ "dependencies": {
633
+ "ms": "2.0.0"
634
+ }
635
+ },
636
+ "node_modules/express/node_modules/ms": {
637
+ "version": "2.0.0",
638
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
639
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
640
+ "license": "MIT"
641
+ },
642
+ "node_modules/fill-range": {
643
+ "version": "7.1.1",
644
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
645
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
646
+ "dev": true,
647
+ "license": "MIT",
648
+ "dependencies": {
649
+ "to-regex-range": "^5.0.1"
650
+ },
651
+ "engines": {
652
+ "node": ">=8"
653
+ }
654
+ },
655
+ "node_modules/finalhandler": {
656
+ "version": "1.3.2",
657
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
658
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
659
+ "license": "MIT",
660
+ "dependencies": {
661
+ "debug": "2.6.9",
662
+ "encodeurl": "~2.0.0",
663
+ "escape-html": "~1.0.3",
664
+ "on-finished": "~2.4.1",
665
+ "parseurl": "~1.3.3",
666
+ "statuses": "~2.0.2",
667
+ "unpipe": "~1.0.0"
668
+ },
669
+ "engines": {
670
+ "node": ">= 0.8"
671
+ }
672
+ },
673
+ "node_modules/finalhandler/node_modules/debug": {
674
+ "version": "2.6.9",
675
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
676
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
677
+ "license": "MIT",
678
+ "dependencies": {
679
+ "ms": "2.0.0"
680
+ }
681
+ },
682
+ "node_modules/finalhandler/node_modules/ms": {
683
+ "version": "2.0.0",
684
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
685
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
686
+ "license": "MIT"
687
+ },
688
+ "node_modules/forwarded": {
689
+ "version": "0.2.0",
690
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
691
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
692
+ "license": "MIT",
693
+ "engines": {
694
+ "node": ">= 0.6"
695
+ }
696
+ },
697
+ "node_modules/fresh": {
698
+ "version": "0.5.2",
699
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
700
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
701
+ "license": "MIT",
702
+ "engines": {
703
+ "node": ">= 0.6"
704
+ }
705
+ },
706
+ "node_modules/fsevents": {
707
+ "version": "2.3.3",
708
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
709
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
710
+ "dev": true,
711
+ "hasInstallScript": true,
712
+ "license": "MIT",
713
+ "optional": true,
714
+ "os": [
715
+ "darwin"
716
+ ],
717
+ "engines": {
718
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
719
+ }
720
+ },
721
+ "node_modules/function-bind": {
722
+ "version": "1.1.2",
723
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
724
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
725
+ "license": "MIT",
726
+ "funding": {
727
+ "url": "https://github.com/sponsors/ljharb"
728
+ }
729
+ },
730
+ "node_modules/get-intrinsic": {
731
+ "version": "1.3.0",
732
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
733
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
734
+ "license": "MIT",
735
+ "dependencies": {
736
+ "call-bind-apply-helpers": "^1.0.2",
737
+ "es-define-property": "^1.0.1",
738
+ "es-errors": "^1.3.0",
739
+ "es-object-atoms": "^1.1.1",
740
+ "function-bind": "^1.1.2",
741
+ "get-proto": "^1.0.1",
742
+ "gopd": "^1.2.0",
743
+ "has-symbols": "^1.1.0",
744
+ "hasown": "^2.0.2",
745
+ "math-intrinsics": "^1.1.0"
746
+ },
747
+ "engines": {
748
+ "node": ">= 0.4"
749
+ },
750
+ "funding": {
751
+ "url": "https://github.com/sponsors/ljharb"
752
+ }
753
+ },
754
+ "node_modules/get-proto": {
755
+ "version": "1.0.1",
756
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
757
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
758
+ "license": "MIT",
759
+ "dependencies": {
760
+ "dunder-proto": "^1.0.1",
761
+ "es-object-atoms": "^1.0.0"
762
+ },
763
+ "engines": {
764
+ "node": ">= 0.4"
765
+ }
766
+ },
767
+ "node_modules/glob-parent": {
768
+ "version": "5.1.2",
769
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
770
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
771
+ "dev": true,
772
+ "license": "ISC",
773
+ "dependencies": {
774
+ "is-glob": "^4.0.1"
775
+ },
776
+ "engines": {
777
+ "node": ">= 6"
778
+ }
779
+ },
780
+ "node_modules/gopd": {
781
+ "version": "1.2.0",
782
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
783
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
784
+ "license": "MIT",
785
+ "engines": {
786
+ "node": ">= 0.4"
787
+ },
788
+ "funding": {
789
+ "url": "https://github.com/sponsors/ljharb"
790
+ }
791
+ },
792
+ "node_modules/has-flag": {
793
+ "version": "3.0.0",
794
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
795
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
796
+ "dev": true,
797
+ "license": "MIT",
798
+ "engines": {
799
+ "node": ">=4"
800
+ }
801
+ },
802
+ "node_modules/has-symbols": {
803
+ "version": "1.1.0",
804
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
805
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
806
+ "license": "MIT",
807
+ "engines": {
808
+ "node": ">= 0.4"
809
+ },
810
+ "funding": {
811
+ "url": "https://github.com/sponsors/ljharb"
812
+ }
813
+ },
814
+ "node_modules/hasown": {
815
+ "version": "2.0.2",
816
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
817
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
818
+ "license": "MIT",
819
+ "dependencies": {
820
+ "function-bind": "^1.1.2"
821
+ },
822
+ "engines": {
823
+ "node": ">= 0.4"
824
+ }
825
+ },
826
+ "node_modules/http-errors": {
827
+ "version": "2.0.1",
828
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
829
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
830
+ "license": "MIT",
831
+ "dependencies": {
832
+ "depd": "~2.0.0",
833
+ "inherits": "~2.0.4",
834
+ "setprototypeof": "~1.2.0",
835
+ "statuses": "~2.0.2",
836
+ "toidentifier": "~1.0.1"
837
+ },
838
+ "engines": {
839
+ "node": ">= 0.8"
840
+ },
841
+ "funding": {
842
+ "type": "opencollective",
843
+ "url": "https://opencollective.com/express"
844
+ }
845
+ },
846
+ "node_modules/iconv-lite": {
847
+ "version": "0.4.24",
848
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
849
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
850
+ "license": "MIT",
851
+ "dependencies": {
852
+ "safer-buffer": ">= 2.1.2 < 3"
853
+ },
854
+ "engines": {
855
+ "node": ">=0.10.0"
856
+ }
857
+ },
858
+ "node_modules/ignore-by-default": {
859
+ "version": "1.0.1",
860
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
861
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
862
+ "dev": true,
863
+ "license": "ISC"
864
+ },
865
+ "node_modules/inherits": {
866
+ "version": "2.0.4",
867
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
868
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
869
+ "license": "ISC"
870
+ },
871
+ "node_modules/ipaddr.js": {
872
+ "version": "1.9.1",
873
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
874
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
875
+ "license": "MIT",
876
+ "engines": {
877
+ "node": ">= 0.10"
878
+ }
879
+ },
880
+ "node_modules/is-binary-path": {
881
+ "version": "2.1.0",
882
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
883
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
884
+ "dev": true,
885
+ "license": "MIT",
886
+ "dependencies": {
887
+ "binary-extensions": "^2.0.0"
888
+ },
889
+ "engines": {
890
+ "node": ">=8"
891
+ }
892
+ },
893
+ "node_modules/is-extglob": {
894
+ "version": "2.1.1",
895
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
896
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
897
+ "dev": true,
898
+ "license": "MIT",
899
+ "engines": {
900
+ "node": ">=0.10.0"
901
+ }
902
+ },
903
+ "node_modules/is-glob": {
904
+ "version": "4.0.3",
905
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
906
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
907
+ "dev": true,
908
+ "license": "MIT",
909
+ "dependencies": {
910
+ "is-extglob": "^2.1.1"
911
+ },
912
+ "engines": {
913
+ "node": ">=0.10.0"
914
+ }
915
+ },
916
+ "node_modules/is-number": {
917
+ "version": "7.0.0",
918
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
919
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
920
+ "dev": true,
921
+ "license": "MIT",
922
+ "engines": {
923
+ "node": ">=0.12.0"
924
+ }
925
+ },
926
+ "node_modules/kareem": {
927
+ "version": "2.6.3",
928
+ "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz",
929
+ "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==",
930
+ "license": "Apache-2.0",
931
+ "engines": {
932
+ "node": ">=12.0.0"
933
+ }
934
+ },
935
+ "node_modules/kruptein": {
936
+ "version": "3.1.5",
937
+ "resolved": "https://registry.npmjs.org/kruptein/-/kruptein-3.1.5.tgz",
938
+ "integrity": "sha512-esEW4ahoc7MN3Xx9VmSamW9dl/pLt4aj83vC+HveqAsJOIKv9r0HhS9ShB10AqPlZIJsYmNxKGLWxhfsCTJ26A==",
939
+ "license": "MIT",
940
+ "dependencies": {
941
+ "asn1.js": "^5.4.1"
942
+ },
943
+ "engines": {
944
+ "node": ">8"
945
+ }
946
+ },
947
+ "node_modules/math-intrinsics": {
948
+ "version": "1.1.0",
949
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
950
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
951
+ "license": "MIT",
952
+ "engines": {
953
+ "node": ">= 0.4"
954
+ }
955
+ },
956
+ "node_modules/media-typer": {
957
+ "version": "0.3.0",
958
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
959
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
960
+ "license": "MIT",
961
+ "engines": {
962
+ "node": ">= 0.6"
963
+ }
964
+ },
965
+ "node_modules/memory-pager": {
966
+ "version": "1.5.0",
967
+ "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
968
+ "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
969
+ "license": "MIT"
970
+ },
971
+ "node_modules/merge-descriptors": {
972
+ "version": "1.0.3",
973
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
974
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
975
+ "license": "MIT",
976
+ "funding": {
977
+ "url": "https://github.com/sponsors/sindresorhus"
978
+ }
979
+ },
980
+ "node_modules/methods": {
981
+ "version": "1.1.2",
982
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
983
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
984
+ "license": "MIT",
985
+ "engines": {
986
+ "node": ">= 0.6"
987
+ }
988
+ },
989
+ "node_modules/mime": {
990
+ "version": "1.6.0",
991
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
992
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
993
+ "license": "MIT",
994
+ "bin": {
995
+ "mime": "cli.js"
996
+ },
997
+ "engines": {
998
+ "node": ">=4"
999
+ }
1000
+ },
1001
+ "node_modules/mime-db": {
1002
+ "version": "1.52.0",
1003
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
1004
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
1005
+ "license": "MIT",
1006
+ "engines": {
1007
+ "node": ">= 0.6"
1008
+ }
1009
+ },
1010
+ "node_modules/mime-types": {
1011
+ "version": "2.1.35",
1012
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
1013
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
1014
+ "license": "MIT",
1015
+ "dependencies": {
1016
+ "mime-db": "1.52.0"
1017
+ },
1018
+ "engines": {
1019
+ "node": ">= 0.6"
1020
+ }
1021
+ },
1022
+ "node_modules/minimalistic-assert": {
1023
+ "version": "1.0.1",
1024
+ "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
1025
+ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
1026
+ "license": "ISC"
1027
+ },
1028
+ "node_modules/minimatch": {
1029
+ "version": "3.1.2",
1030
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
1031
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
1032
+ "dev": true,
1033
+ "license": "ISC",
1034
+ "dependencies": {
1035
+ "brace-expansion": "^1.1.7"
1036
+ },
1037
+ "engines": {
1038
+ "node": "*"
1039
+ }
1040
+ },
1041
+ "node_modules/mongodb": {
1042
+ "version": "6.21.0",
1043
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.21.0.tgz",
1044
+ "integrity": "sha512-URyb/VXMjJ4da46OeSXg+puO39XH9DeQpWCslifrRn9JWugy0D+DvvBvkm2WxmHe61O/H19JM66p1z7RHVkZ6A==",
1045
+ "license": "Apache-2.0",
1046
+ "peer": true,
1047
+ "dependencies": {
1048
+ "@mongodb-js/saslprep": "^1.3.0",
1049
+ "bson": "^6.10.4",
1050
+ "mongodb-connection-string-url": "^3.0.2"
1051
+ },
1052
+ "engines": {
1053
+ "node": ">=16.20.1"
1054
+ },
1055
+ "peerDependencies": {
1056
+ "@aws-sdk/credential-providers": "^3.188.0",
1057
+ "@mongodb-js/zstd": "^1.1.0 || ^2.0.0",
1058
+ "gcp-metadata": "^5.2.0",
1059
+ "kerberos": "^2.0.1",
1060
+ "mongodb-client-encryption": ">=6.0.0 <7",
1061
+ "snappy": "^7.3.2",
1062
+ "socks": "^2.7.1"
1063
+ },
1064
+ "peerDependenciesMeta": {
1065
+ "@aws-sdk/credential-providers": {
1066
+ "optional": true
1067
+ },
1068
+ "@mongodb-js/zstd": {
1069
+ "optional": true
1070
+ },
1071
+ "gcp-metadata": {
1072
+ "optional": true
1073
+ },
1074
+ "kerberos": {
1075
+ "optional": true
1076
+ },
1077
+ "mongodb-client-encryption": {
1078
+ "optional": true
1079
+ },
1080
+ "snappy": {
1081
+ "optional": true
1082
+ },
1083
+ "socks": {
1084
+ "optional": true
1085
+ }
1086
+ }
1087
+ },
1088
+ "node_modules/mongodb-connection-string-url": {
1089
+ "version": "3.0.2",
1090
+ "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz",
1091
+ "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
1092
+ "license": "Apache-2.0",
1093
+ "dependencies": {
1094
+ "@types/whatwg-url": "^11.0.2",
1095
+ "whatwg-url": "^14.1.0 || ^13.0.0"
1096
+ }
1097
+ },
1098
+ "node_modules/mongoose": {
1099
+ "version": "8.20.2",
1100
+ "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.20.2.tgz",
1101
+ "integrity": "sha512-U0TPupnqBOAI3p9H9qdShX8/nJUBylliRcHFKuhbewEkM7Y0qc9BbrQR9h4q6+1easoZqej7cq2Ee36AZ0gMzQ==",
1102
+ "license": "MIT",
1103
+ "dependencies": {
1104
+ "bson": "^6.10.4",
1105
+ "kareem": "2.6.3",
1106
+ "mongodb": "~6.20.0",
1107
+ "mpath": "0.9.0",
1108
+ "mquery": "5.0.0",
1109
+ "ms": "2.1.3",
1110
+ "sift": "17.1.3"
1111
+ },
1112
+ "engines": {
1113
+ "node": ">=16.20.1"
1114
+ },
1115
+ "funding": {
1116
+ "type": "opencollective",
1117
+ "url": "https://opencollective.com/mongoose"
1118
+ }
1119
+ },
1120
+ "node_modules/mongoose/node_modules/mongodb": {
1121
+ "version": "6.20.0",
1122
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz",
1123
+ "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==",
1124
+ "license": "Apache-2.0",
1125
+ "dependencies": {
1126
+ "@mongodb-js/saslprep": "^1.3.0",
1127
+ "bson": "^6.10.4",
1128
+ "mongodb-connection-string-url": "^3.0.2"
1129
+ },
1130
+ "engines": {
1131
+ "node": ">=16.20.1"
1132
+ },
1133
+ "peerDependencies": {
1134
+ "@aws-sdk/credential-providers": "^3.188.0",
1135
+ "@mongodb-js/zstd": "^1.1.0 || ^2.0.0",
1136
+ "gcp-metadata": "^5.2.0",
1137
+ "kerberos": "^2.0.1",
1138
+ "mongodb-client-encryption": ">=6.0.0 <7",
1139
+ "snappy": "^7.3.2",
1140
+ "socks": "^2.7.1"
1141
+ },
1142
+ "peerDependenciesMeta": {
1143
+ "@aws-sdk/credential-providers": {
1144
+ "optional": true
1145
+ },
1146
+ "@mongodb-js/zstd": {
1147
+ "optional": true
1148
+ },
1149
+ "gcp-metadata": {
1150
+ "optional": true
1151
+ },
1152
+ "kerberos": {
1153
+ "optional": true
1154
+ },
1155
+ "mongodb-client-encryption": {
1156
+ "optional": true
1157
+ },
1158
+ "snappy": {
1159
+ "optional": true
1160
+ },
1161
+ "socks": {
1162
+ "optional": true
1163
+ }
1164
+ }
1165
+ },
1166
+ "node_modules/mpath": {
1167
+ "version": "0.9.0",
1168
+ "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
1169
+ "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
1170
+ "license": "MIT",
1171
+ "engines": {
1172
+ "node": ">=4.0.0"
1173
+ }
1174
+ },
1175
+ "node_modules/mquery": {
1176
+ "version": "5.0.0",
1177
+ "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz",
1178
+ "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==",
1179
+ "license": "MIT",
1180
+ "dependencies": {
1181
+ "debug": "4.x"
1182
+ },
1183
+ "engines": {
1184
+ "node": ">=14.0.0"
1185
+ }
1186
+ },
1187
+ "node_modules/ms": {
1188
+ "version": "2.1.3",
1189
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1190
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1191
+ "license": "MIT"
1192
+ },
1193
+ "node_modules/negotiator": {
1194
+ "version": "0.6.3",
1195
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
1196
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
1197
+ "license": "MIT",
1198
+ "engines": {
1199
+ "node": ">= 0.6"
1200
+ }
1201
+ },
1202
+ "node_modules/nodemon": {
1203
+ "version": "3.1.11",
1204
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz",
1205
+ "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==",
1206
+ "dev": true,
1207
+ "license": "MIT",
1208
+ "dependencies": {
1209
+ "chokidar": "^3.5.2",
1210
+ "debug": "^4",
1211
+ "ignore-by-default": "^1.0.1",
1212
+ "minimatch": "^3.1.2",
1213
+ "pstree.remy": "^1.1.8",
1214
+ "semver": "^7.5.3",
1215
+ "simple-update-notifier": "^2.0.0",
1216
+ "supports-color": "^5.5.0",
1217
+ "touch": "^3.1.0",
1218
+ "undefsafe": "^2.0.5"
1219
+ },
1220
+ "bin": {
1221
+ "nodemon": "bin/nodemon.js"
1222
+ },
1223
+ "engines": {
1224
+ "node": ">=10"
1225
+ },
1226
+ "funding": {
1227
+ "type": "opencollective",
1228
+ "url": "https://opencollective.com/nodemon"
1229
+ }
1230
+ },
1231
+ "node_modules/normalize-path": {
1232
+ "version": "3.0.0",
1233
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
1234
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
1235
+ "dev": true,
1236
+ "license": "MIT",
1237
+ "engines": {
1238
+ "node": ">=0.10.0"
1239
+ }
1240
+ },
1241
+ "node_modules/oauth": {
1242
+ "version": "0.10.2",
1243
+ "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz",
1244
+ "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==",
1245
+ "license": "MIT"
1246
+ },
1247
+ "node_modules/object-assign": {
1248
+ "version": "4.1.1",
1249
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
1250
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1251
+ "license": "MIT",
1252
+ "engines": {
1253
+ "node": ">=0.10.0"
1254
+ }
1255
+ },
1256
+ "node_modules/object-inspect": {
1257
+ "version": "1.13.4",
1258
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
1259
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
1260
+ "license": "MIT",
1261
+ "engines": {
1262
+ "node": ">= 0.4"
1263
+ },
1264
+ "funding": {
1265
+ "url": "https://github.com/sponsors/ljharb"
1266
+ }
1267
+ },
1268
+ "node_modules/on-finished": {
1269
+ "version": "2.4.1",
1270
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
1271
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
1272
+ "license": "MIT",
1273
+ "dependencies": {
1274
+ "ee-first": "1.1.1"
1275
+ },
1276
+ "engines": {
1277
+ "node": ">= 0.8"
1278
+ }
1279
+ },
1280
+ "node_modules/on-headers": {
1281
+ "version": "1.1.0",
1282
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
1283
+ "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
1284
+ "license": "MIT",
1285
+ "engines": {
1286
+ "node": ">= 0.8"
1287
+ }
1288
+ },
1289
+ "node_modules/parseurl": {
1290
+ "version": "1.3.3",
1291
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
1292
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
1293
+ "license": "MIT",
1294
+ "engines": {
1295
+ "node": ">= 0.8"
1296
+ }
1297
+ },
1298
+ "node_modules/passport": {
1299
+ "version": "0.7.0",
1300
+ "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
1301
+ "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
1302
+ "license": "MIT",
1303
+ "dependencies": {
1304
+ "passport-strategy": "1.x.x",
1305
+ "pause": "0.0.1",
1306
+ "utils-merge": "^1.0.1"
1307
+ },
1308
+ "engines": {
1309
+ "node": ">= 0.4.0"
1310
+ },
1311
+ "funding": {
1312
+ "type": "github",
1313
+ "url": "https://github.com/sponsors/jaredhanson"
1314
+ }
1315
+ },
1316
+ "node_modules/passport-google-oauth20": {
1317
+ "version": "2.0.0",
1318
+ "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",
1319
+ "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==",
1320
+ "license": "MIT",
1321
+ "dependencies": {
1322
+ "passport-oauth2": "1.x.x"
1323
+ },
1324
+ "engines": {
1325
+ "node": ">= 0.4.0"
1326
+ }
1327
+ },
1328
+ "node_modules/passport-local": {
1329
+ "version": "1.0.0",
1330
+ "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz",
1331
+ "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==",
1332
+ "dependencies": {
1333
+ "passport-strategy": "1.x.x"
1334
+ },
1335
+ "engines": {
1336
+ "node": ">= 0.4.0"
1337
+ }
1338
+ },
1339
+ "node_modules/passport-oauth2": {
1340
+ "version": "1.8.0",
1341
+ "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz",
1342
+ "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==",
1343
+ "license": "MIT",
1344
+ "dependencies": {
1345
+ "base64url": "3.x.x",
1346
+ "oauth": "0.10.x",
1347
+ "passport-strategy": "1.x.x",
1348
+ "uid2": "0.0.x",
1349
+ "utils-merge": "1.x.x"
1350
+ },
1351
+ "engines": {
1352
+ "node": ">= 0.4.0"
1353
+ },
1354
+ "funding": {
1355
+ "type": "github",
1356
+ "url": "https://github.com/sponsors/jaredhanson"
1357
+ }
1358
+ },
1359
+ "node_modules/passport-strategy": {
1360
+ "version": "1.0.0",
1361
+ "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
1362
+ "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
1363
+ "engines": {
1364
+ "node": ">= 0.4.0"
1365
+ }
1366
+ },
1367
+ "node_modules/path-to-regexp": {
1368
+ "version": "0.1.12",
1369
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
1370
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
1371
+ "license": "MIT"
1372
+ },
1373
+ "node_modules/pause": {
1374
+ "version": "0.0.1",
1375
+ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
1376
+ "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
1377
+ },
1378
+ "node_modules/picomatch": {
1379
+ "version": "2.3.1",
1380
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
1381
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
1382
+ "dev": true,
1383
+ "license": "MIT",
1384
+ "engines": {
1385
+ "node": ">=8.6"
1386
+ },
1387
+ "funding": {
1388
+ "url": "https://github.com/sponsors/jonschlinkert"
1389
+ }
1390
+ },
1391
+ "node_modules/proxy-addr": {
1392
+ "version": "2.0.7",
1393
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
1394
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
1395
+ "license": "MIT",
1396
+ "dependencies": {
1397
+ "forwarded": "0.2.0",
1398
+ "ipaddr.js": "1.9.1"
1399
+ },
1400
+ "engines": {
1401
+ "node": ">= 0.10"
1402
+ }
1403
+ },
1404
+ "node_modules/pstree.remy": {
1405
+ "version": "1.1.8",
1406
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
1407
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
1408
+ "dev": true,
1409
+ "license": "MIT"
1410
+ },
1411
+ "node_modules/punycode": {
1412
+ "version": "2.3.1",
1413
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
1414
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
1415
+ "license": "MIT",
1416
+ "engines": {
1417
+ "node": ">=6"
1418
+ }
1419
+ },
1420
+ "node_modules/qs": {
1421
+ "version": "6.14.0",
1422
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
1423
+ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
1424
+ "license": "BSD-3-Clause",
1425
+ "dependencies": {
1426
+ "side-channel": "^1.1.0"
1427
+ },
1428
+ "engines": {
1429
+ "node": ">=0.6"
1430
+ },
1431
+ "funding": {
1432
+ "url": "https://github.com/sponsors/ljharb"
1433
+ }
1434
+ },
1435
+ "node_modules/random-bytes": {
1436
+ "version": "1.0.0",
1437
+ "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
1438
+ "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
1439
+ "license": "MIT",
1440
+ "engines": {
1441
+ "node": ">= 0.8"
1442
+ }
1443
+ },
1444
+ "node_modules/range-parser": {
1445
+ "version": "1.2.1",
1446
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
1447
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
1448
+ "license": "MIT",
1449
+ "engines": {
1450
+ "node": ">= 0.6"
1451
+ }
1452
+ },
1453
+ "node_modules/raw-body": {
1454
+ "version": "2.5.3",
1455
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
1456
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
1457
+ "license": "MIT",
1458
+ "dependencies": {
1459
+ "bytes": "~3.1.2",
1460
+ "http-errors": "~2.0.1",
1461
+ "iconv-lite": "~0.4.24",
1462
+ "unpipe": "~1.0.0"
1463
+ },
1464
+ "engines": {
1465
+ "node": ">= 0.8"
1466
+ }
1467
+ },
1468
+ "node_modules/readdirp": {
1469
+ "version": "3.6.0",
1470
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
1471
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
1472
+ "dev": true,
1473
+ "license": "MIT",
1474
+ "dependencies": {
1475
+ "picomatch": "^2.2.1"
1476
+ },
1477
+ "engines": {
1478
+ "node": ">=8.10.0"
1479
+ }
1480
+ },
1481
+ "node_modules/safe-buffer": {
1482
+ "version": "5.2.1",
1483
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1484
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1485
+ "funding": [
1486
+ {
1487
+ "type": "github",
1488
+ "url": "https://github.com/sponsors/feross"
1489
+ },
1490
+ {
1491
+ "type": "patreon",
1492
+ "url": "https://www.patreon.com/feross"
1493
+ },
1494
+ {
1495
+ "type": "consulting",
1496
+ "url": "https://feross.org/support"
1497
+ }
1498
+ ],
1499
+ "license": "MIT"
1500
+ },
1501
+ "node_modules/safer-buffer": {
1502
+ "version": "2.1.2",
1503
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1504
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
1505
+ "license": "MIT"
1506
+ },
1507
+ "node_modules/semver": {
1508
+ "version": "7.7.3",
1509
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
1510
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
1511
+ "dev": true,
1512
+ "license": "ISC",
1513
+ "bin": {
1514
+ "semver": "bin/semver.js"
1515
+ },
1516
+ "engines": {
1517
+ "node": ">=10"
1518
+ }
1519
+ },
1520
+ "node_modules/send": {
1521
+ "version": "0.19.1",
1522
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.1.tgz",
1523
+ "integrity": "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==",
1524
+ "license": "MIT",
1525
+ "dependencies": {
1526
+ "debug": "2.6.9",
1527
+ "depd": "2.0.0",
1528
+ "destroy": "1.2.0",
1529
+ "encodeurl": "~2.0.0",
1530
+ "escape-html": "~1.0.3",
1531
+ "etag": "~1.8.1",
1532
+ "fresh": "0.5.2",
1533
+ "http-errors": "2.0.0",
1534
+ "mime": "1.6.0",
1535
+ "ms": "2.1.3",
1536
+ "on-finished": "2.4.1",
1537
+ "range-parser": "~1.2.1",
1538
+ "statuses": "2.0.1"
1539
+ },
1540
+ "engines": {
1541
+ "node": ">= 0.8.0"
1542
+ }
1543
+ },
1544
+ "node_modules/send/node_modules/debug": {
1545
+ "version": "2.6.9",
1546
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
1547
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
1548
+ "license": "MIT",
1549
+ "dependencies": {
1550
+ "ms": "2.0.0"
1551
+ }
1552
+ },
1553
+ "node_modules/send/node_modules/debug/node_modules/ms": {
1554
+ "version": "2.0.0",
1555
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
1556
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
1557
+ "license": "MIT"
1558
+ },
1559
+ "node_modules/send/node_modules/http-errors": {
1560
+ "version": "2.0.0",
1561
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
1562
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
1563
+ "license": "MIT",
1564
+ "dependencies": {
1565
+ "depd": "2.0.0",
1566
+ "inherits": "2.0.4",
1567
+ "setprototypeof": "1.2.0",
1568
+ "statuses": "2.0.1",
1569
+ "toidentifier": "1.0.1"
1570
+ },
1571
+ "engines": {
1572
+ "node": ">= 0.8"
1573
+ }
1574
+ },
1575
+ "node_modules/send/node_modules/statuses": {
1576
+ "version": "2.0.1",
1577
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
1578
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
1579
+ "license": "MIT",
1580
+ "engines": {
1581
+ "node": ">= 0.8"
1582
+ }
1583
+ },
1584
+ "node_modules/serve-static": {
1585
+ "version": "1.16.2",
1586
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
1587
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
1588
+ "license": "MIT",
1589
+ "dependencies": {
1590
+ "encodeurl": "~2.0.0",
1591
+ "escape-html": "~1.0.3",
1592
+ "parseurl": "~1.3.3",
1593
+ "send": "0.19.0"
1594
+ },
1595
+ "engines": {
1596
+ "node": ">= 0.8.0"
1597
+ }
1598
+ },
1599
+ "node_modules/serve-static/node_modules/debug": {
1600
+ "version": "2.6.9",
1601
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
1602
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
1603
+ "license": "MIT",
1604
+ "dependencies": {
1605
+ "ms": "2.0.0"
1606
+ }
1607
+ },
1608
+ "node_modules/serve-static/node_modules/debug/node_modules/ms": {
1609
+ "version": "2.0.0",
1610
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
1611
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
1612
+ "license": "MIT"
1613
+ },
1614
+ "node_modules/serve-static/node_modules/http-errors": {
1615
+ "version": "2.0.0",
1616
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
1617
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
1618
+ "license": "MIT",
1619
+ "dependencies": {
1620
+ "depd": "2.0.0",
1621
+ "inherits": "2.0.4",
1622
+ "setprototypeof": "1.2.0",
1623
+ "statuses": "2.0.1",
1624
+ "toidentifier": "1.0.1"
1625
+ },
1626
+ "engines": {
1627
+ "node": ">= 0.8"
1628
+ }
1629
+ },
1630
+ "node_modules/serve-static/node_modules/send": {
1631
+ "version": "0.19.0",
1632
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
1633
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
1634
+ "license": "MIT",
1635
+ "dependencies": {
1636
+ "debug": "2.6.9",
1637
+ "depd": "2.0.0",
1638
+ "destroy": "1.2.0",
1639
+ "encodeurl": "~1.0.2",
1640
+ "escape-html": "~1.0.3",
1641
+ "etag": "~1.8.1",
1642
+ "fresh": "0.5.2",
1643
+ "http-errors": "2.0.0",
1644
+ "mime": "1.6.0",
1645
+ "ms": "2.1.3",
1646
+ "on-finished": "2.4.1",
1647
+ "range-parser": "~1.2.1",
1648
+ "statuses": "2.0.1"
1649
+ },
1650
+ "engines": {
1651
+ "node": ">= 0.8.0"
1652
+ }
1653
+ },
1654
+ "node_modules/serve-static/node_modules/send/node_modules/encodeurl": {
1655
+ "version": "1.0.2",
1656
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
1657
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
1658
+ "license": "MIT",
1659
+ "engines": {
1660
+ "node": ">= 0.8"
1661
+ }
1662
+ },
1663
+ "node_modules/serve-static/node_modules/statuses": {
1664
+ "version": "2.0.1",
1665
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
1666
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
1667
+ "license": "MIT",
1668
+ "engines": {
1669
+ "node": ">= 0.8"
1670
+ }
1671
+ },
1672
+ "node_modules/setprototypeof": {
1673
+ "version": "1.2.0",
1674
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
1675
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
1676
+ "license": "ISC"
1677
+ },
1678
+ "node_modules/side-channel": {
1679
+ "version": "1.1.0",
1680
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
1681
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
1682
+ "license": "MIT",
1683
+ "dependencies": {
1684
+ "es-errors": "^1.3.0",
1685
+ "object-inspect": "^1.13.3",
1686
+ "side-channel-list": "^1.0.0",
1687
+ "side-channel-map": "^1.0.1",
1688
+ "side-channel-weakmap": "^1.0.2"
1689
+ },
1690
+ "engines": {
1691
+ "node": ">= 0.4"
1692
+ },
1693
+ "funding": {
1694
+ "url": "https://github.com/sponsors/ljharb"
1695
+ }
1696
+ },
1697
+ "node_modules/side-channel-list": {
1698
+ "version": "1.0.0",
1699
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
1700
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
1701
+ "license": "MIT",
1702
+ "dependencies": {
1703
+ "es-errors": "^1.3.0",
1704
+ "object-inspect": "^1.13.3"
1705
+ },
1706
+ "engines": {
1707
+ "node": ">= 0.4"
1708
+ },
1709
+ "funding": {
1710
+ "url": "https://github.com/sponsors/ljharb"
1711
+ }
1712
+ },
1713
+ "node_modules/side-channel-map": {
1714
+ "version": "1.0.1",
1715
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
1716
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
1717
+ "license": "MIT",
1718
+ "dependencies": {
1719
+ "call-bound": "^1.0.2",
1720
+ "es-errors": "^1.3.0",
1721
+ "get-intrinsic": "^1.2.5",
1722
+ "object-inspect": "^1.13.3"
1723
+ },
1724
+ "engines": {
1725
+ "node": ">= 0.4"
1726
+ },
1727
+ "funding": {
1728
+ "url": "https://github.com/sponsors/ljharb"
1729
+ }
1730
+ },
1731
+ "node_modules/side-channel-weakmap": {
1732
+ "version": "1.0.2",
1733
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
1734
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
1735
+ "license": "MIT",
1736
+ "dependencies": {
1737
+ "call-bound": "^1.0.2",
1738
+ "es-errors": "^1.3.0",
1739
+ "get-intrinsic": "^1.2.5",
1740
+ "object-inspect": "^1.13.3",
1741
+ "side-channel-map": "^1.0.1"
1742
+ },
1743
+ "engines": {
1744
+ "node": ">= 0.4"
1745
+ },
1746
+ "funding": {
1747
+ "url": "https://github.com/sponsors/ljharb"
1748
+ }
1749
+ },
1750
+ "node_modules/sift": {
1751
+ "version": "17.1.3",
1752
+ "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz",
1753
+ "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==",
1754
+ "license": "MIT"
1755
+ },
1756
+ "node_modules/simple-update-notifier": {
1757
+ "version": "2.0.0",
1758
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
1759
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
1760
+ "dev": true,
1761
+ "license": "MIT",
1762
+ "dependencies": {
1763
+ "semver": "^7.5.3"
1764
+ },
1765
+ "engines": {
1766
+ "node": ">=10"
1767
+ }
1768
+ },
1769
+ "node_modules/socket.io": {
1770
+ "version": "4.8.1",
1771
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
1772
+ "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
1773
+ "license": "MIT",
1774
+ "dependencies": {
1775
+ "accepts": "~1.3.4",
1776
+ "base64id": "~2.0.0",
1777
+ "cors": "~2.8.5",
1778
+ "debug": "~4.3.2",
1779
+ "engine.io": "~6.6.0",
1780
+ "socket.io-adapter": "~2.5.2",
1781
+ "socket.io-parser": "~4.2.4"
1782
+ },
1783
+ "engines": {
1784
+ "node": ">=10.2.0"
1785
+ }
1786
+ },
1787
+ "node_modules/socket.io-adapter": {
1788
+ "version": "2.5.5",
1789
+ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
1790
+ "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
1791
+ "license": "MIT",
1792
+ "dependencies": {
1793
+ "debug": "~4.3.4",
1794
+ "ws": "~8.17.1"
1795
+ }
1796
+ },
1797
+ "node_modules/socket.io-adapter/node_modules/debug": {
1798
+ "version": "4.3.7",
1799
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
1800
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
1801
+ "license": "MIT",
1802
+ "dependencies": {
1803
+ "ms": "^2.1.3"
1804
+ },
1805
+ "engines": {
1806
+ "node": ">=6.0"
1807
+ },
1808
+ "peerDependenciesMeta": {
1809
+ "supports-color": {
1810
+ "optional": true
1811
+ }
1812
+ }
1813
+ },
1814
+ "node_modules/socket.io-parser": {
1815
+ "version": "4.2.4",
1816
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
1817
+ "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
1818
+ "license": "MIT",
1819
+ "dependencies": {
1820
+ "@socket.io/component-emitter": "~3.1.0",
1821
+ "debug": "~4.3.1"
1822
+ },
1823
+ "engines": {
1824
+ "node": ">=10.0.0"
1825
+ }
1826
+ },
1827
+ "node_modules/socket.io-parser/node_modules/debug": {
1828
+ "version": "4.3.7",
1829
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
1830
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
1831
+ "license": "MIT",
1832
+ "dependencies": {
1833
+ "ms": "^2.1.3"
1834
+ },
1835
+ "engines": {
1836
+ "node": ">=6.0"
1837
+ },
1838
+ "peerDependenciesMeta": {
1839
+ "supports-color": {
1840
+ "optional": true
1841
+ }
1842
+ }
1843
+ },
1844
+ "node_modules/socket.io/node_modules/debug": {
1845
+ "version": "4.3.7",
1846
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
1847
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
1848
+ "license": "MIT",
1849
+ "dependencies": {
1850
+ "ms": "^2.1.3"
1851
+ },
1852
+ "engines": {
1853
+ "node": ">=6.0"
1854
+ },
1855
+ "peerDependenciesMeta": {
1856
+ "supports-color": {
1857
+ "optional": true
1858
+ }
1859
+ }
1860
+ },
1861
+ "node_modules/sparse-bitfield": {
1862
+ "version": "3.0.3",
1863
+ "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
1864
+ "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
1865
+ "license": "MIT",
1866
+ "dependencies": {
1867
+ "memory-pager": "^1.0.2"
1868
+ }
1869
+ },
1870
+ "node_modules/statuses": {
1871
+ "version": "2.0.2",
1872
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
1873
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
1874
+ "license": "MIT",
1875
+ "engines": {
1876
+ "node": ">= 0.8"
1877
+ }
1878
+ },
1879
+ "node_modules/supports-color": {
1880
+ "version": "5.5.0",
1881
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
1882
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
1883
+ "dev": true,
1884
+ "license": "MIT",
1885
+ "dependencies": {
1886
+ "has-flag": "^3.0.0"
1887
+ },
1888
+ "engines": {
1889
+ "node": ">=4"
1890
+ }
1891
+ },
1892
+ "node_modules/to-regex-range": {
1893
+ "version": "5.0.1",
1894
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
1895
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
1896
+ "dev": true,
1897
+ "license": "MIT",
1898
+ "dependencies": {
1899
+ "is-number": "^7.0.0"
1900
+ },
1901
+ "engines": {
1902
+ "node": ">=8.0"
1903
+ }
1904
+ },
1905
+ "node_modules/toidentifier": {
1906
+ "version": "1.0.1",
1907
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1908
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
1909
+ "license": "MIT",
1910
+ "engines": {
1911
+ "node": ">=0.6"
1912
+ }
1913
+ },
1914
+ "node_modules/touch": {
1915
+ "version": "3.1.1",
1916
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
1917
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
1918
+ "dev": true,
1919
+ "license": "ISC",
1920
+ "bin": {
1921
+ "nodetouch": "bin/nodetouch.js"
1922
+ }
1923
+ },
1924
+ "node_modules/tr46": {
1925
+ "version": "5.1.1",
1926
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
1927
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
1928
+ "license": "MIT",
1929
+ "dependencies": {
1930
+ "punycode": "^2.3.1"
1931
+ },
1932
+ "engines": {
1933
+ "node": ">=18"
1934
+ }
1935
+ },
1936
+ "node_modules/type-is": {
1937
+ "version": "1.6.18",
1938
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
1939
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
1940
+ "license": "MIT",
1941
+ "dependencies": {
1942
+ "media-typer": "0.3.0",
1943
+ "mime-types": "~2.1.24"
1944
+ },
1945
+ "engines": {
1946
+ "node": ">= 0.6"
1947
+ }
1948
+ },
1949
+ "node_modules/uid-safe": {
1950
+ "version": "2.1.5",
1951
+ "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
1952
+ "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
1953
+ "license": "MIT",
1954
+ "dependencies": {
1955
+ "random-bytes": "~1.0.0"
1956
+ },
1957
+ "engines": {
1958
+ "node": ">= 0.8"
1959
+ }
1960
+ },
1961
+ "node_modules/uid2": {
1962
+ "version": "0.0.4",
1963
+ "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz",
1964
+ "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==",
1965
+ "license": "MIT"
1966
+ },
1967
+ "node_modules/undefsafe": {
1968
+ "version": "2.0.5",
1969
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
1970
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
1971
+ "dev": true,
1972
+ "license": "MIT"
1973
+ },
1974
+ "node_modules/undici-types": {
1975
+ "version": "7.16.0",
1976
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
1977
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
1978
+ "license": "MIT"
1979
+ },
1980
+ "node_modules/unpipe": {
1981
+ "version": "1.0.0",
1982
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
1983
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
1984
+ "license": "MIT",
1985
+ "engines": {
1986
+ "node": ">= 0.8"
1987
+ }
1988
+ },
1989
+ "node_modules/utils-merge": {
1990
+ "version": "1.0.1",
1991
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
1992
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
1993
+ "license": "MIT",
1994
+ "engines": {
1995
+ "node": ">= 0.4.0"
1996
+ }
1997
+ },
1998
+ "node_modules/vary": {
1999
+ "version": "1.1.2",
2000
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
2001
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
2002
+ "license": "MIT",
2003
+ "engines": {
2004
+ "node": ">= 0.8"
2005
+ }
2006
+ },
2007
+ "node_modules/webidl-conversions": {
2008
+ "version": "7.0.0",
2009
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
2010
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
2011
+ "license": "BSD-2-Clause",
2012
+ "engines": {
2013
+ "node": ">=12"
2014
+ }
2015
+ },
2016
+ "node_modules/whatwg-url": {
2017
+ "version": "14.2.0",
2018
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
2019
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
2020
+ "license": "MIT",
2021
+ "dependencies": {
2022
+ "tr46": "^5.1.0",
2023
+ "webidl-conversions": "^7.0.0"
2024
+ },
2025
+ "engines": {
2026
+ "node": ">=18"
2027
+ }
2028
+ },
2029
+ "node_modules/ws": {
2030
+ "version": "8.17.1",
2031
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
2032
+ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
2033
+ "license": "MIT",
2034
+ "engines": {
2035
+ "node": ">=10.0.0"
2036
+ },
2037
+ "peerDependencies": {
2038
+ "bufferutil": "^4.0.1",
2039
+ "utf-8-validate": ">=5.0.2"
2040
+ },
2041
+ "peerDependenciesMeta": {
2042
+ "bufferutil": {
2043
+ "optional": true
2044
+ },
2045
+ "utf-8-validate": {
2046
+ "optional": true
2047
+ }
2048
+ }
2049
+ }
2050
+ }
2051
+ }
backend/package.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "popcornping-backend",
3
+ "version": "2.0.0",
4
+ "description": "Backend for PopcornPing video calling app",
5
+ "main": "server.js",
6
+ "scripts": {
7
+ "start": "node server.js",
8
+ "dev": "nodemon server.js"
9
+ },
10
+ "dependencies": {
11
+ "bcryptjs": "^2.4.3",
12
+ "connect-mongo": "^5.1.0",
13
+ "cors": "^2.8.5",
14
+ "dotenv": "^16.6.1",
15
+ "express": "^4.18.2",
16
+ "express-session": "^1.17.3",
17
+ "mongoose": "^8.0.3",
18
+ "passport": "^0.7.0",
19
+ "passport-google-oauth20": "^2.0.0",
20
+ "passport-local": "^1.0.0",
21
+ "socket.io": "^4.6.0"
22
+ },
23
+ "devDependencies": {
24
+ "nodemon": "^3.0.2"
25
+ }
26
+ }
backend/routes/auth.js ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const passport = require('passport');
3
+ const User = require('../models/User');
4
+
5
+ const router = express.Router();
6
+
7
+ // --- LOCAL AUTHENTICATION ---
8
+
9
+ // Register with username/password
10
+ router.post('/register', async (req, res) => {
11
+ try {
12
+ const { email, username, password } = req.body;
13
+
14
+ const existingUser = await User.findOne({ email: email.toLowerCase() });
15
+
16
+ if (existingUser) {
17
+ return res.status(400).json({ message: 'Email already registered' });
18
+ }
19
+
20
+ const user = await User.create({
21
+ email: email.toLowerCase(),
22
+ username,
23
+ password,
24
+ });
25
+
26
+ req.login(user, (err) => {
27
+ if (err) {
28
+ return res.status(500).json({ message: 'Error logging in after registration' });
29
+ }
30
+ res.status(201).json({
31
+ user: {
32
+ id: user._id,
33
+ email: user.email,
34
+ username: user.username,
35
+ avatar: user.avatar,
36
+ },
37
+ });
38
+ });
39
+ } catch (error) {
40
+ res.status(500).json({ message: 'Server error', error: error.message });
41
+ }
42
+ });
43
+
44
+ // Login with username/password
45
+ router.post('/login', (req, res, next) => {
46
+ passport.authenticate('local', (err, user, info) => {
47
+ if (err) {
48
+ return res.status(500).json({ message: 'Server error' });
49
+ }
50
+
51
+ if (!user) {
52
+ return res.status(401).json({ message: info.message || 'Invalid credentials' });
53
+ }
54
+
55
+ req.login(user, (err) => {
56
+ if (err) {
57
+ return res.status(500).json({ message: 'Error logging in' });
58
+ }
59
+ res.json({
60
+ user: {
61
+ id: user._id,
62
+ email: user.email,
63
+ username: user.username,
64
+ avatar: user.avatar,
65
+ },
66
+ });
67
+ });
68
+ })(req, res, next);
69
+ });
70
+
71
+ // --- GOOGLE OAUTH ROUTES ---
72
+
73
+ // 1. Trigger Route - Starts the flow
74
+ // URL: /api/auth/google
75
+ router.get('/google', (req, res, next) => {
76
+ console.log('>>> Google Auth Trigger Route Hit');
77
+ passport.authenticate('google', {
78
+ scope: ['profile', 'email'],
79
+ prompt: 'select_account'
80
+ })(req, res, next);
81
+ });
82
+
83
+ // 2. Callback Route - Google sends the user back here
84
+ // URL: /api/auth/google/callback
85
+ router.get('/google/callback', (req, res, next) => {
86
+ console.log('>>> GOOGLE CALLBACK HIT: Processing code...');
87
+
88
+ passport.authenticate('google', (err, user, info) => {
89
+ // ERROR HANDLING
90
+ if (err) {
91
+ console.error('>>> AUTH ERROR:', err);
92
+ if (err.message && err.message.includes('timed out')) {
93
+ console.error('!!! CRITICAL: Database Timeout. CHECK ATLAS IP WHITELIST (0.0.0.0/0) !!!');
94
+ }
95
+ // Redirect to Vercel frontend with error
96
+ return res.redirect(`${process.env.FRONTEND_URL}/login?error=${encodeURIComponent(err.message)}`);
97
+ }
98
+
99
+ // NO USER FOUND
100
+ if (!user) {
101
+ console.error('>>> NO USER RETURNED:', info);
102
+ return res.redirect(`${process.env.FRONTEND_URL}/login?error=auth_failed`);
103
+ }
104
+
105
+ // LOGIN SUCCESS
106
+ console.log(`>>> USER FOUND: ${user.email}. Logging in session...`);
107
+ req.logIn(user, (err) => {
108
+ if (err) {
109
+ console.error('>>> SESSION LOGIN ERROR:', err);
110
+ return res.redirect(`${process.env.FRONTEND_URL}/login?error=session_error`);
111
+ }
112
+
113
+ // SAVE SESSION EXPLICITLY
114
+ // This ensures the cookie is written before we redirect
115
+ req.session.save((err) => {
116
+ if (err) {
117
+ console.error('>>> SESSION SAVE ERROR:', err);
118
+ return res.redirect(`${process.env.FRONTEND_URL}/login?error=save_error`);
119
+ }
120
+
121
+ console.log('>>> SESSION SAVED SUCCESSFULLY');
122
+ console.log('>>> Session ID:', req.sessionID);
123
+ console.log('>>> REDIRECTING TO DASHBOARD');
124
+
125
+ // CORRECTION: Redirect directly to Dashboard, not root ("/")
126
+ // This ensures the user sees the internal app immediately.
127
+ res.redirect(`${process.env.FRONTEND_URL}/dashboard`);
128
+ });
129
+ });
130
+ })(req, res, next);
131
+ });
132
+
133
+ // --- UTILITY ROUTES ---
134
+
135
+ // Logout
136
+ router.post('/logout', (req, res) => {
137
+ req.logout((err) => {
138
+ if (err) {
139
+ return res.status(500).json({ message: 'Error logging out' });
140
+ }
141
+ req.session.destroy();
142
+ res.json({ message: 'Logged out successfully' });
143
+ });
144
+ });
145
+
146
+ // Get current user (Used by Frontend to check login status)
147
+ router.get('/me', (req, res) => {
148
+ console.log('>>> /me endpoint hit. Authenticated:', req.isAuthenticated());
149
+ if (req.isAuthenticated()) {
150
+ res.json({
151
+ user: {
152
+ id: req.user._id,
153
+ email: req.user.email,
154
+ username: req.user.username,
155
+ avatar: req.user.avatar,
156
+ },
157
+ });
158
+ } else {
159
+ // Return 401 so frontend knows to show "Login" button
160
+ res.status(401).json({ message: 'Not authenticated' });
161
+ }
162
+ });
163
+
164
+ // Debug session endpoint (Useful for checking if cookies are working)
165
+ router.get('/check-session', (req, res) => {
166
+ res.json({
167
+ authenticated: req.isAuthenticated(),
168
+ sessionID: req.sessionID,
169
+ user: req.user || null,
170
+ cookie: req.session.cookie
171
+ });
172
+ });
173
+
174
+ module.exports = router;
backend/routes/room.js ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const Room = require('../models/Room');
3
+ const { isAuthenticated } = require('../middleware/auth');
4
+
5
+ const router = express.Router();
6
+
7
+ // Generate random room ID
8
+ const generateRoomId = () => {
9
+ return Math.random().toString(36).substring(2, 10).toUpperCase();
10
+ };
11
+
12
+ // Create a new room
13
+ router.post('/create', isAuthenticated, async (req, res) => {
14
+ try {
15
+ const { name } = req.body;
16
+
17
+ const roomId = generateRoomId();
18
+
19
+ const room = await Room.create({
20
+ roomId,
21
+ name: name || `${req.user.username}'s Room`,
22
+ host: req.user._id,
23
+ participants: [req.user._id],
24
+ });
25
+
26
+ res.status(201).json({
27
+ room: {
28
+ id: room._id,
29
+ roomId: room.roomId,
30
+ name: room.name,
31
+ host: room.host,
32
+ },
33
+ });
34
+ } catch (error) {
35
+ res.status(500).json({ message: 'Error creating room', error: error.message });
36
+ }
37
+ });
38
+
39
+ // Get room details
40
+ router.get('/:roomId', isAuthenticated, async (req, res) => {
41
+ try {
42
+ const room = await Room.findOne({ roomId: req.params.roomId })
43
+ .populate('host', 'username email avatar')
44
+ .populate('participants', 'username email avatar');
45
+
46
+ if (!room) {
47
+ return res.status(404).json({ message: 'Room not found' });
48
+ }
49
+
50
+ if (!room.isActive) {
51
+ return res.status(410).json({ message: 'Room is no longer active' });
52
+ }
53
+
54
+ res.json({ room });
55
+ } catch (error) {
56
+ res.status(500).json({ message: 'Error fetching room', error: error.message });
57
+ }
58
+ });
59
+
60
+ // Join a room
61
+ router.post('/:roomId/join', isAuthenticated, async (req, res) => {
62
+ try {
63
+ const room = await Room.findOne({ roomId: req.params.roomId });
64
+
65
+ if (!room) {
66
+ return res.status(404).json({ message: 'Room not found' });
67
+ }
68
+
69
+ if (!room.isActive) {
70
+ return res.status(410).json({ message: 'Room is no longer active' });
71
+ }
72
+
73
+ if (!room.participants.includes(req.user._id)) {
74
+ room.participants.push(req.user._id);
75
+ await room.save();
76
+ }
77
+
78
+ res.json({ message: 'Joined room successfully', roomId: room.roomId });
79
+ } catch (error) {
80
+ res.status(500).json({ message: 'Error joining room', error: error.message });
81
+ }
82
+ });
83
+
84
+ // End a room (only host can do this)
85
+ router.post('/:roomId/end', isAuthenticated, async (req, res) => {
86
+ try {
87
+ const room = await Room.findOne({ roomId: req.params.roomId });
88
+
89
+ if (!room) {
90
+ return res.status(404).json({ message: 'Room not found' });
91
+ }
92
+
93
+ if (room.host.toString() !== req.user._id.toString()) {
94
+ return res.status(403).json({ message: 'Only the host can end the room' });
95
+ }
96
+
97
+ room.isActive = false;
98
+ await room.save();
99
+
100
+ res.json({ message: 'Room ended successfully' });
101
+ } catch (error) {
102
+ res.status(500).json({ message: 'Error ending room', error: error.message });
103
+ }
104
+ });
105
+
106
+ // Get user's rooms
107
+ router.get('/user/rooms', isAuthenticated, async (req, res) => {
108
+ try {
109
+ const rooms = await Room.find({
110
+ $or: [
111
+ { host: req.user._id },
112
+ { participants: req.user._id }
113
+ ],
114
+ isActive: true,
115
+ }).populate('host', 'username email');
116
+
117
+ res.json({ rooms });
118
+ } catch (error) {
119
+ res.status(500).json({ message: 'Error fetching rooms', error: error.message });
120
+ }
121
+ });
122
+
123
+ module.exports = router;
backend/server.js ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ require('dotenv').config();
2
+ const express = require('express');
3
+ const session = require('express-session');
4
+ const MongoStore = require('connect-mongo');
5
+ const cors = require('cors');
6
+ const passport = require('./config/passport');
7
+ const connectDB = require('./config/database');
8
+ const authRoutes = require('./routes/auth');
9
+ const roomRoutes = require('./routes/room');
10
+
11
+ const app = express();
12
+ const http = require('http').createServer(app);
13
+
14
+ // 1. Trust Proxy (Required for Hugging Face HTTPS)
15
+ app.set('trust proxy', 1);
16
+
17
+ // Define allowed origins
18
+ const allowedOrigins = [
19
+ process.env.FRONTEND_URL, // https://popcorn-ping.vercel.app
20
+ "https://popcorn-ping.vercel.app",
21
+ "http://localhost:3000"
22
+ ].filter(Boolean);
23
+
24
+ // 2. Middleware
25
+ app.use(cors({
26
+ origin: allowedOrigins,
27
+ credentials: true,
28
+ }));
29
+ app.use(express.json());
30
+ app.use(express.urlencoded({ extended: true }));
31
+
32
+ // Debug Middleware: Log every request to see what's hitting the server
33
+ app.use((req, res, next) => {
34
+ console.log(`[REQUEST] ${req.method} ${req.path}`);
35
+ next();
36
+ });
37
+
38
+ // 3. Session Configuration
39
+ app.use(session({
40
+ secret: process.env.SESSION_SECRET || 'fallback_secret',
41
+ resave: false,
42
+ saveUninitialized: false,
43
+ store: MongoStore.create({
44
+ mongoUrl: process.env.MONGODB_URI,
45
+ mongoOptions: { serverSelectionTimeoutMS: 5000 }
46
+ }),
47
+ cookie: {
48
+ maxAge: 1000 * 60 * 60 * 24 * 7, // 1 week
49
+ httpOnly: true,
50
+ secure: true, // MUST be true for Hugging Face
51
+ sameSite: 'none' // MUST be 'none' for Google redirect
52
+ },
53
+ }));
54
+
55
+ // 4. Passport Middleware
56
+ app.use(passport.initialize());
57
+ app.use(passport.session());
58
+
59
+ // 5. Basic Routes
60
+ app.get('/', (req, res) => {
61
+ res.status(200).send(`<h1>🍿 PopcornPing Backend is Live!</h1><p>Running on: ${process.env.BACKEND_URL}</p>`);
62
+ });
63
+
64
+ app.get('/health', (req, res) => res.status(200).json({ status: 'OK' }));
65
+ app.get('/api/health', (req, res) => res.status(200).json({ status: 'OK' }));
66
+
67
+ // 6. API Routes
68
+ // Mount Auth Routes at /api/auth
69
+ // This matches: /api/auth/google/callback
70
+ console.log('Mounting Auth Routes at /api/auth');
71
+ app.use('/api/auth', authRoutes);
72
+
73
+ // Mount Room Routes at /api/rooms
74
+ console.log('Mounting Room Routes at /api/rooms');
75
+ app.use('/api/rooms', roomRoutes);
76
+
77
+ // 7. Socket Logic (Your existing logic)
78
+ const io = require('socket.io')(http, {
79
+ cors: { origin: allowedOrigins, methods: ["GET", "POST"], credentials: true },
80
+ });
81
+ const rooms = {};
82
+ io.on('connection', (socket) => {
83
+ /* ... Your socket logic (unchanged) ... */
84
+ socket.on('disconnect', () => {}); // Shortened for brevity
85
+ });
86
+
87
+ // 8. 404 Handler (MUST BE LAST)
88
+ // Only catch requests that didn't match any route above
89
+ app.use('/api/*', (req, res) => {
90
+ console.error(`[404] Route not found: ${req.method} ${req.originalUrl}`);
91
+ res.status(404).json({ message: `API endpoint not found: ${req.originalUrl}` });
92
+ });
93
+
94
+ // 9. Global Error Handler
95
+ app.use((err, req, res, next) => {
96
+ console.error('SERVER ERROR:', err.stack);
97
+ res.status(500).json({ message: 'Something went wrong!', error: err.message });
98
+ });
99
+
100
+ // 10. Start Server
101
+ const PORT = process.env.PORT || 7860;
102
+ connectDB().then(() => {
103
+ http.listen(PORT, '0.0.0.0', () => {
104
+ console.log(`🚀 Backend running on port ${PORT}`);
105
+ console.log(`🔗 Backend URL: ${process.env.BACKEND_URL}`);
106
+ });
107
+ }).catch(err => {
108
+ console.error('FATAL: MongoDB Connection Failed', err);
109
+ process.exit(1);
110
+ });
frontend/.gitignore ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ /node_modules
3
+ /jspm_packages/
4
+
5
+ # Production Build Artifacts
6
+ /dist/
7
+ /build/
8
+ /out/
9
+
10
+ # Environments and Credentials
11
+ # Environment variables file is often used for secrets and configuration
12
+ .env
13
+ .env.*
14
+ # For example: .env.development, .env.production
15
+
16
+ # Logs
17
+ npm-debug.log*
18
+ yarn-debug.log*
19
+ yarn-error.log*
20
+
21
+ # OS generated files
22
+ .DS_Store
23
+ Thumbs.db
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "popcornping-frontend",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "dependencies": {
6
+ "axios": "^1.6.2",
7
+ "framer-motion": "^12.23.26",
8
+ "lucide-react": "^0.560.0",
9
+ "react": "^18.2.0",
10
+ "react-dom": "^18.2.0",
11
+ "react-router-dom": "^6.20.1",
12
+ "simple-peer": "^9.11.1",
13
+ "socket.io-client": "^4.6.0"
14
+ },
15
+ "devDependencies": {
16
+ "autoprefixer": "^10.4.16",
17
+ "postcss": "^8.4.32",
18
+ "react-scripts": "5.0.1",
19
+ "tailwindcss": "^3.3.6"
20
+ },
21
+ "scripts": {
22
+ "start": "react-scripts start",
23
+ "build": "react-scripts build",
24
+ "test": "react-scripts test",
25
+ "eject": "react-scripts eject"
26
+ },
27
+ "eslintConfig": {
28
+ "extends": [
29
+ "react-app"
30
+ ]
31
+ },
32
+ "browserslist": {
33
+ "production": [
34
+ ">0.2%",
35
+ "not dead",
36
+ "not op_mini all"
37
+ ],
38
+ "development": [
39
+ "last 1 chrome version",
40
+ "last 1 firefox version",
41
+ "last 1 safari version"
42
+ ]
43
+ }
44
+ }
frontend/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
frontend/public/background.jpg ADDED

Git LFS Details

  • SHA256: ca5829372e2fd8650e8900aed6cfd5de4ddb9b965c49974ecb114871ebc3c8dd
  • Pointer size: 131 Bytes
  • Size of remote file: 829 kB
frontend/public/favicon.png ADDED

Git LFS Details

  • SHA256: a6de83397b283950f677499ce933272d76e974241beca90f0f28e85133926101
  • Pointer size: 132 Bytes
  • Size of remote file: 3.59 MB
frontend/public/index.html ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <link rel="icon" href="%PUBLIC_URL%/favicon.png" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <meta name="theme-color" content="#000000" />
8
+ <meta
9
+ name="description"
10
+ content="PopcornPing - Video calling with screen sharing made simple"
11
+ />
12
+ <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
13
+ <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
14
+ <title>PopcornPing - Think Better Together</title>
15
+ </head>
16
+ <body>
17
+ <noscript>You need to enable JavaScript to run this app.</noscript>
18
+ <div id="root"></div>
19
+ </body>
20
+ </html>
frontend/public/login-illustration.jpg ADDED

Git LFS Details

  • SHA256: 3ca0e00ce820eb6fdd9e50f1ed0d52646e6fc8ce80eda66a87e2cb66a78540a0
  • Pointer size: 130 Bytes
  • Size of remote file: 98.9 kB
frontend/public/logo.png ADDED

Git LFS Details

  • SHA256: a6de83397b283950f677499ce933272d76e974241beca90f0f28e85133926101
  • Pointer size: 132 Bytes
  • Size of remote file: 3.59 MB
frontend/public/manifest.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "short_name": "PopcornPing",
3
+ "name": "PopcornPing - Video Calling Platform",
4
+ "icons": [
5
+ {
6
+ "src": "favicon.png",
7
+ "sizes": "64x64 32x32 24x24 16x16",
8
+ "type": "image/x-icon"
9
+ }
10
+ ],
11
+ "start_url": ".",
12
+ "display": "standalone",
13
+ "theme_color": "#000000",
14
+ "background_color": "#ffffff"
15
+ }
frontend/public/snippets-bg.jpg ADDED

Git LFS Details

  • SHA256: 17ebb626143884378e85402a4e6c166d07db8a8203b25c47b547552db68c871a
  • Pointer size: 132 Bytes
  • Size of remote file: 1.8 MB
frontend/src/App.jsx ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect } from 'react';
2
+ import { BrowserRouter as Router, Routes, Route, useNavigate, useLocation } from 'react-router-dom';
3
+ import { AuthProvider, useAuth } from './context/AuthContext';
4
+ import Hero from './components/Hero';
5
+ import Dashboard from './components/Dashboard';
6
+ import VideoRoom from './components/VideoRoom';
7
+ import ProtectedRoute from './components/ProtectedRoute';
8
+ import Snippets from './components/Snippets';
9
+ import Footer from './components/Footer';
10
+ import Login from './components/Login';
11
+ import Navbar from './components/Navbar';
12
+
13
+ // OAuth Handler Component
14
+ function OAuthHandler() {
15
+ const navigate = useNavigate();
16
+ const location = useLocation();
17
+ const { checkAuth } = useAuth();
18
+
19
+ useEffect(() => {
20
+ const handleOAuthRedirect = async () => {
21
+ const params = new URLSearchParams(location.search);
22
+
23
+ // Check for successful OAuth
24
+ if (params.get('auth') === 'success') {
25
+ console.log('OAuth success detected, checking authentication...');
26
+
27
+ try {
28
+ // Verify authentication with backend
29
+ const response = await fetch('/api/auth/me', {
30
+ credentials: 'include'
31
+ });
32
+
33
+ const data = await response.json();
34
+
35
+ if (response.ok && data.user) {
36
+ console.log('User authenticated:', data.user);
37
+ // Update auth context if you have a method for it
38
+ if (checkAuth) {
39
+ await checkAuth();
40
+ }
41
+ // Navigate to dashboard
42
+ navigate('/dashboard', { replace: true });
43
+ } else {
44
+ console.error('Authentication verification failed');
45
+ navigate('/login?error=verification_failed', { replace: true });
46
+ }
47
+ } catch (error) {
48
+ console.error('Error verifying authentication:', error);
49
+ navigate('/login?error=verification_error', { replace: true });
50
+ }
51
+
52
+ // Clean up URL parameters
53
+ window.history.replaceState({}, '', location.pathname);
54
+ }
55
+
56
+ // Check for OAuth errors
57
+ if (params.get('error')) {
58
+ const errorMessage = params.get('error');
59
+ console.error('OAuth error:', errorMessage);
60
+ navigate(`/login?error=${errorMessage}`, { replace: true });
61
+ // Clean up URL parameters
62
+ window.history.replaceState({}, '', location.pathname);
63
+ }
64
+ };
65
+
66
+ handleOAuthRedirect();
67
+ }, [location, navigate, checkAuth]);
68
+
69
+ return null;
70
+ }
71
+
72
+ function AppRoutes() {
73
+ return (
74
+ <>
75
+ <OAuthHandler />
76
+ <Routes>
77
+ <Route path="/login" element={<Login />} />
78
+ <Route
79
+ path="/"
80
+ element={
81
+ <>
82
+ <Hero />
83
+ <Snippets />
84
+ <Footer />
85
+ </>
86
+ }
87
+ />
88
+ <Route
89
+ path="/dashboard"
90
+ element={
91
+ <ProtectedRoute>
92
+ <Dashboard />
93
+ </ProtectedRoute>
94
+ }
95
+ />
96
+ <Route
97
+ path="/room/:roomId"
98
+ element={
99
+ <ProtectedRoute>
100
+ <VideoRoom />
101
+ </ProtectedRoute>
102
+ }
103
+ />
104
+ </Routes>
105
+ </>
106
+ );
107
+ }
108
+
109
+ function App() {
110
+ return (
111
+ <AuthProvider>
112
+ <Router>
113
+ <div className="App">
114
+ <AppRoutes />
115
+ </div>
116
+ </Router>
117
+ </AuthProvider>
118
+ );
119
+ }
120
+
121
+ export default App;
frontend/src/components/Dashboard.jsx ADDED
@@ -0,0 +1,342 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { useAuth } from '../context/AuthContext';
4
+ import { roomAPI } from '../utils/api';
5
+ import Navbar from './Navbar';
6
+
7
+ // --- Icons ---
8
+ const CopyIcon = ({ className }) => (
9
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
10
+ );
11
+ const LinkIcon = ({ className }) => (
12
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
13
+ );
14
+ const LockIcon = ({ className }) => (
15
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
16
+ );
17
+ const MicIcon = ({ className }) => (
18
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" x2="12" y1="19" y2="22"/></svg>
19
+ );
20
+ const TrashIcon = ({ className }) => (
21
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>
22
+ );
23
+ const TrendingUpIcon = ({ className }) => (
24
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/></svg>
25
+ );
26
+
27
+ // --- Components ---
28
+ const Toggle = ({ enabled, onChange }) => (
29
+ <button
30
+ onClick={() => onChange(!enabled)}
31
+ className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none ${
32
+ enabled ? 'bg-white' : 'bg-gray-800'
33
+ }`}
34
+ >
35
+ <span
36
+ className={`${
37
+ enabled ? 'translate-x-6 bg-black' : 'translate-x-1 bg-gray-500'
38
+ } inline-block h-4 w-4 transform rounded-full transition-transform`}
39
+ />
40
+ </button>
41
+ );
42
+
43
+ const Dashboard = () => {
44
+ const [rooms, setRooms] = useState([]);
45
+ const [loading, setLoading] = useState(false);
46
+ const [generatedCode, setGeneratedCode] = useState('');
47
+ const [joinCode, setJoinCode] = useState(''); // New state for joining
48
+ const [isPasswordProtected, setIsPasswordProtected] = useState(false);
49
+ const [waitingRoom, setWaitingRoom] = useState(false);
50
+
51
+ const { user } = useAuth();
52
+ const navigate = useNavigate();
53
+
54
+ useEffect(() => {
55
+ fetchRooms();
56
+ generateNewCode();
57
+ }, []);
58
+
59
+ const generateNewCode = () => {
60
+ const code = `J-${Math.floor(100 + Math.random() * 900)}K-${Math.floor(10 + Math.random() * 90)}D`;
61
+ setGeneratedCode(code);
62
+ };
63
+
64
+ const fetchRooms = async () => {
65
+ try {
66
+ const response = await roomAPI.getUserRooms();
67
+ setRooms(response.data.rooms);
68
+ } catch (error) {
69
+ console.error('Error fetching rooms:', error);
70
+ }
71
+ };
72
+
73
+ const handleCreateRoom = async () => {
74
+ setLoading(true);
75
+ const roomName = `Room ${generatedCode}`;
76
+ try {
77
+ const response = await roomAPI.createRoom({
78
+ name: roomName,
79
+ settings: {
80
+ passwordProtected: isPasswordProtected,
81
+ muteOnEntry: waitingRoom
82
+ }
83
+ });
84
+ navigate(`/room/${response.data.room.roomId}`);
85
+ } catch (error) {
86
+ console.error('Error creating room:', error);
87
+ alert('Failed to create room');
88
+ } finally {
89
+ setLoading(false);
90
+ }
91
+ };
92
+
93
+ const handleJoinRoom = () => {
94
+ if (!joinCode.trim()) return;
95
+
96
+ // Check if it's a full URL or just a code
97
+ let roomId = joinCode;
98
+ try {
99
+ if (joinCode.includes('http')) {
100
+ const url = new URL(joinCode);
101
+ // Assuming path is /room/:id, grab the last segment
102
+ const pathParts = url.pathname.split('/');
103
+ if (pathParts.length > 0) {
104
+ roomId = pathParts[pathParts.length - 1];
105
+ }
106
+ }
107
+ } catch (e) {
108
+ // Not a valid URL, treat as code
109
+ console.log("Input is not a URL, using as code");
110
+ }
111
+
112
+ // Handle "room/XYZ" case if pasted without protocol
113
+ if (roomId.includes('/room/')) {
114
+ roomId = roomId.split('/room/')[1];
115
+ }
116
+
117
+ navigate(`/room/${roomId}`);
118
+ };
119
+
120
+ const copyToClipboard = (text) => {
121
+ navigator.clipboard.writeText(text);
122
+ alert("Copied to clipboard!");
123
+ };
124
+
125
+ const handleDeleteRoom = async (e, roomId) => {
126
+ e.stopPropagation();
127
+ if(window.confirm("End this session?")) {
128
+ try {
129
+ await roomAPI.endRoom(roomId);
130
+ fetchRooms();
131
+ alert("Room ended successfully");
132
+ } catch (error) {
133
+ console.error("Error ending room:", error);
134
+ }
135
+ }
136
+ };
137
+
138
+ return (
139
+ <div className="min-h-screen bg-black text-white font-sans selection:bg-gray-700">
140
+
141
+ <Navbar />
142
+
143
+ <main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-24 pb-12">
144
+
145
+ <div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
146
+
147
+ {/* --- LEFT COLUMN --- */}
148
+ <div className="lg:col-span-8 flex flex-col gap-6">
149
+
150
+ {/* Active Rooms & Create Button */}
151
+ <div className="bg-[#111] border border-[#222] rounded-3xl p-6 md:p-8 flex flex-col md:flex-row gap-8">
152
+
153
+ <div className="flex-1 flex flex-col">
154
+ <div className="flex justify-between items-center mb-6">
155
+ <h2 className="text-gray-400 text-sm uppercase tracking-wider font-bold">Active Rooms</h2>
156
+ <button onClick={fetchRooms} className="text-xs text-gray-500 hover:text-white transition-colors">
157
+ Refresh
158
+ </button>
159
+ </div>
160
+
161
+ <div className="flex-1 space-y-3 pr-2 overflow-y-auto max-h-[200px] custom-scrollbar">
162
+ {rooms.length > 0 ? (
163
+ rooms.map((room) => (
164
+ <div key={room._id} className="group flex items-center justify-between p-3 bg-[#1a1a1a] rounded-xl border border-[#2a2a2a] hover:border-gray-500 transition-all cursor-pointer">
165
+ <div className="flex items-center gap-3">
166
+ <div className="w-8 h-8 rounded-full bg-[#222] border border-[#333] flex items-center justify-center">
167
+ <span className="text-gray-500 text-[10px] font-bold">RM</span>
168
+ </div>
169
+ <div className="max-w-[120px] sm:max-w-xs overflow-hidden text-ellipsis whitespace-nowrap">
170
+ <h3 className="font-medium text-gray-200 text-sm truncate">{room.name}</h3>
171
+ </div>
172
+ </div>
173
+ <button
174
+ onClick={(e) => handleDeleteRoom(e, room.roomId)}
175
+ className="text-gray-600 hover:text-red-400 transition-colors p-1"
176
+ >
177
+ <TrashIcon className="w-4 h-4" />
178
+ </button>
179
+ </div>
180
+ ))
181
+ ) : (
182
+ <div className="flex flex-col items-center justify-center h-full min-h-[120px] text-gray-600 border border-dashed border-[#222] rounded-xl bg-[#151515]">
183
+ <p className="text-sm">No active rooms</p>
184
+ </div>
185
+ )}
186
+ </div>
187
+ </div>
188
+
189
+ <div className="w-full md:w-64 flex-shrink-0">
190
+ <button
191
+ onClick={handleCreateRoom}
192
+ disabled={loading}
193
+ className="w-full h-full min-h-[160px] rounded-2xl bg-gradient-to-b from-[#2a2a2a] to-black border border-gray-700 shadow-lg flex flex-col items-center justify-center gap-3 group hover:border-white/40 transition-all"
194
+ >
195
+ <div className="w-12 h-12 bg-white rounded-full flex items-center justify-center text-black group-hover:scale-110 transition-transform">
196
+ {loading ? (
197
+ <div className="animate-spin w-5 h-5 border-2 border-black border-t-transparent rounded-full"></div>
198
+ ) : (
199
+ <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v16m8-8H4" /></svg>
200
+ )}
201
+ </div>
202
+ <div className="text-center">
203
+ <span className="block text-white font-bold text-lg">Create Room</span>
204
+ <span className="text-gray-500 text-xs">Private Session</span>
205
+ </div>
206
+ </button>
207
+ </div>
208
+ </div>
209
+
210
+ {/* Share or Join Code Section */}
211
+ <div className="bg-[#111] border border-[#222] rounded-3xl p-8 flex flex-col gap-8">
212
+
213
+ <div className="flex flex-col md:flex-row items-center justify-between gap-8 border-b border-[#222] pb-8">
214
+ <div className="space-y-3 text-center md:text-left w-full">
215
+ <h2 className="text-lg text-gray-300 font-medium">Share Your Code</h2>
216
+ <div>
217
+ <span className="text-5xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-gray-100 to-gray-600 tracking-tight block">
218
+ {generatedCode}
219
+ </span>
220
+ </div>
221
+
222
+ <div className="flex flex-wrap gap-3 mt-4 justify-center md:justify-start">
223
+ <button
224
+ onClick={() => copyToClipboard(generatedCode)}
225
+ className="flex items-center gap-2 px-4 py-2 bg-[#1a1a1a] rounded-lg text-xs text-gray-400 hover:text-white border border-[#333] transition-colors"
226
+ >
227
+ <CopyIcon className="w-3 h-3" /> Copy Code
228
+ </button>
229
+ <button
230
+ onClick={() => copyToClipboard(`${window.location.origin}/room/${generatedCode}`)}
231
+ className="flex items-center gap-2 px-4 py-2 bg-[#1a1a1a] rounded-lg text-xs text-gray-400 hover:text-white border border-[#333] transition-colors"
232
+ >
233
+ <LinkIcon className="w-3 h-3" /> Copy Link
234
+ </button>
235
+ </div>
236
+ </div>
237
+
238
+ <div className="w-full md:w-auto flex-shrink-0">
239
+ <button
240
+ onClick={handleCreateRoom}
241
+ className="w-full md:w-48 py-4 bg-white hover:bg-gray-200 text-black font-bold text-lg rounded-xl shadow-lg transition-all"
242
+ >
243
+ Start Meeting
244
+ </button>
245
+ </div>
246
+ </div>
247
+
248
+ {/* Join Existing Room Input */}
249
+ <div className="flex flex-col md:flex-row items-center gap-4">
250
+ <div className="flex-1 w-full">
251
+ <label className="text-gray-400 text-xs font-bold uppercase tracking-wider block mb-2">Have a link or code?</label>
252
+ <input
253
+ type="text"
254
+ value={joinCode}
255
+ onChange={(e) => setJoinCode(e.target.value)}
256
+ placeholder="Enter room code or paste link here"
257
+ className="w-full bg-[#1a1a1a] border border-[#333] rounded-xl px-4 py-3 text-white focus:outline-none focus:border-white/20 transition-all placeholder:text-gray-600"
258
+ />
259
+ </div>
260
+ <button
261
+ onClick={handleJoinRoom}
262
+ className="w-full md:w-auto px-8 py-3 mt-6 bg-[#2a2a2a] hover:bg-[#3a3a3a] text-white font-bold rounded-xl border border-[#333] transition-all"
263
+ >
264
+ Join Room
265
+ </button>
266
+ </div>
267
+
268
+ </div>
269
+
270
+ </div>
271
+
272
+ {/* --- RIGHT COLUMN --- */}
273
+ <div className="lg:col-span-4 flex flex-col gap-6">
274
+
275
+ {/* Room Configuration */}
276
+ <div className="bg-[#111] border border-[#222] rounded-3xl p-8 flex flex-col justify-center min-h-[240px]">
277
+ <h2 className="text-gray-400 text-sm mb-6 font-bold uppercase tracking-wider">Room Configuration</h2>
278
+
279
+ <div className="space-y-6">
280
+ <div className="flex items-center justify-between">
281
+ <div className="flex items-center gap-3">
282
+ <LockIcon className="w-5 h-5 text-gray-500" />
283
+ <div className="flex flex-col">
284
+ <span className="text-white text-sm font-medium">Require Password</span>
285
+ </div>
286
+ </div>
287
+ <Toggle enabled={isPasswordProtected} onChange={setIsPasswordProtected} />
288
+ </div>
289
+
290
+ <div className="h-px bg-[#222] w-full"></div>
291
+
292
+ <div className="flex items-center justify-between">
293
+ <div className="flex items-center gap-3">
294
+ <MicIcon className="w-5 h-5 text-gray-500" />
295
+ <div className="flex flex-col">
296
+ <span className="text-white text-sm font-medium">Waiting Room</span>
297
+ </div>
298
+ </div>
299
+ <Toggle enabled={waitingRoom} onChange={setWaitingRoom} />
300
+ </div>
301
+ </div>
302
+ </div>
303
+
304
+ {/* Meeting Analytics */}
305
+ <div className="bg-[#111] border border-[#222] rounded-3xl p-8">
306
+ <h2 className="text-gray-400 text-sm font-bold uppercase tracking-wider mb-1">Statistics</h2>
307
+ <p className="text-[11px] text-gray-600 mb-6">Your meeting activity</p>
308
+
309
+ <div className="space-y-4">
310
+ <div className="flex items-center justify-between">
311
+ <span className="text-sm text-gray-400">Active Rooms</span>
312
+ <span className="text-2xl font-bold text-white">{rooms.length}</span>
313
+ </div>
314
+
315
+ <div className="flex items-center justify-between">
316
+ <span className="text-sm text-gray-400">Total Created</span>
317
+ <span className="text-2xl font-bold text-white">{rooms.length}</span>
318
+ </div>
319
+
320
+ {rooms.length > 0 && (
321
+ <div className="mt-4 pt-4 border-t border-[#222]">
322
+ <div className="flex items-center justify-between text-xs">
323
+ <span className="text-gray-500">Status</span>
324
+ <span className="flex items-center gap-1 text-green-400">
325
+ <TrendingUpIcon className="w-3 h-3" />
326
+ Active
327
+ </span>
328
+ </div>
329
+ </div>
330
+ )}
331
+ </div>
332
+ </div>
333
+
334
+ </div>
335
+
336
+ </div>
337
+ </main>
338
+ </div>
339
+ );
340
+ };
341
+
342
+ export default Dashboard;
frontend/src/components/Footer.jsx ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ const Footer = () => {
4
+ return (
5
+ <footer className="bg-black text-white py-6 border-t border-gray-800">
6
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
7
+
8
+ {/* --- Top Section: Logo & Social Icons --- */}
9
+ <div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4">
10
+
11
+ {/* Logo & Company Name */}
12
+ <div className="flex items-center gap-3 mb-4 md:mb-0">
13
+ {/* Logo Image from Public Directory */}
14
+ <img
15
+ src="/logo.png"
16
+ alt="PopcornPing Logo"
17
+ className="w-8 h-8 object-contain"
18
+ />
19
+ <span className="text-xl font-bold tracking-wide">PopcornPing</span>
20
+ </div>
21
+
22
+ {/* Founder & Co-Founder Links */}
23
+ <div className="flex flex-col sm:flex-row gap-8 text-right">
24
+
25
+ {/* Founder Block */}
26
+ <div className="flex flex-col items-start md:items-end">
27
+ <span className="text-gray-500 font-semibold text-xs uppercase tracking-wider mb-2">Founder</span>
28
+ <div className="flex gap-3">
29
+ {/* GitHub Icon */}
30
+ <a href="https://github.com/AzhaanGlitch" target="_blank" rel="noreferrer" className="text-gray-400 hover:text-white transition-colors" aria-label="Founder Github">
31
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
32
+ <path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path>
33
+ </svg>
34
+ </a>
35
+ {/* LinkedIn Icon */}
36
+ <a href="https://www.linkedin.com/in/azhaanalisiddiqui/" target="_blank" rel="noreferrer" className="text-gray-400 hover:text-white transition-colors" aria-label="Founder LinkedIn">
37
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
38
+ <path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path>
39
+ <rect x="2" y="9" width="4" height="12"></rect>
40
+ <circle cx="4" cy="4" r="2"></circle>
41
+ </svg>
42
+ </a>
43
+ </div>
44
+ </div>
45
+
46
+ {/* Co-Founder Block */}
47
+ <div className="flex flex-col items-start md:items-end">
48
+ <span className="text-gray-500 font-semibold text-xs uppercase tracking-wider mb-2">Co-Founder</span>
49
+ <div className="flex gap-3">
50
+ {/* GitHub Icon */}
51
+ <a href="https://github.com/YashGoyal06" target="_blank" rel="noreferrer" className="text-gray-400 hover:text-white transition-colors" aria-label="Co-Founder Github">
52
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
53
+ <path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path>
54
+ </svg>
55
+ </a>
56
+ {/* LinkedIn Icon */}
57
+ <a href="https://www.linkedin.com/in/yashgoyal06/" target="_blank" rel="noreferrer" className="text-gray-400 hover:text-white transition-colors" aria-label="Co-Founder LinkedIn">
58
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
59
+ <path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path>
60
+ <rect x="2" y="9" width="4" height="12"></rect>
61
+ <circle cx="4" cy="4" r="2"></circle>
62
+ </svg>
63
+ </a>
64
+ </div>
65
+ </div>
66
+
67
+ </div>
68
+ </div>
69
+
70
+ {/* --- Bottom Section: Single Line Left Aligned --- */}
71
+ <div className="text-sm text-gray-500">
72
+ <span>© 2025 PopcornPing. All rights reserved.</span>
73
+ </div>
74
+
75
+ </div>
76
+ </footer>
77
+ );
78
+ };
79
+
80
+ export default Footer;
frontend/src/components/Hero.jsx ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+
4
+ const Hero = () => {
5
+ const navigate = useNavigate();
6
+
7
+ return (
8
+ <>
9
+ <div className="min-h-screen bg-black relative overflow-hidden">
10
+ {/* Background Image */}
11
+ <div className="absolute inset-0">
12
+ <img
13
+ src="/background.jpg"
14
+ alt="Background"
15
+ className="w-full h-full object-cover opacity-60"
16
+ onError={(e) => {
17
+ e.target.style.display = 'none';
18
+ }}
19
+ />
20
+ <div className="absolute inset-0 bg-gradient-to-b from-black/60 via-black/40 to-black/60"></div>
21
+ </div>
22
+
23
+ {/* Animated glowing orbs */}
24
+ <div className="absolute inset-0">
25
+ {/* Large central glow */}
26
+ <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-[750px] h-[750px] bg-purple-600/20 rounded-full blur-[130px] animate-pulse-slow"></div>
27
+
28
+ {/* Floating glowing orbs */}
29
+ <div className="absolute top-20 left-20 w-48 h-48 bg-blue-500/30 rounded-full blur-[80px] animate-float-slow"></div>
30
+ <div className="absolute top-40 right-32 w-40 h-40 bg-purple-500/25 rounded-full blur-[80px] animate-float-slower"></div>
31
+ <div className="absolute bottom-32 left-40 w-56 h-56 bg-pink-500/20 rounded-full blur-[90px] animate-float-slowest"></div>
32
+ <div className="absolute bottom-20 right-20 w-44 h-44 bg-cyan-500/25 rounded-full blur-[80px] animate-float-slow"></div>
33
+ <div className="absolute top-1/3 left-1/4 w-36 h-36 bg-indigo-500/30 rounded-full blur-[70px] animate-float-slower"></div>
34
+ <div className="absolute top-2/3 right-1/3 w-52 h-52 bg-violet-500/20 rounded-full blur-[85px] animate-float-slowest"></div>
35
+ </div>
36
+
37
+ {/* Animation Styles */}
38
+ <style jsx>{`
39
+ @keyframes float-slow {
40
+ 0%, 100% { transform: translate(0, 0); }
41
+ 50% { transform: translate(30px, -30px); }
42
+ }
43
+ @keyframes float-slower {
44
+ 0%, 100% { transform: translate(0, 0); }
45
+ 50% { transform: translate(-25px, 35px); }
46
+ }
47
+ @keyframes float-slowest {
48
+ 0%, 100% { transform: translate(0, 0); }
49
+ 50% { transform: translate(20px, 40px); }
50
+ }
51
+ @keyframes pulse-slow {
52
+ 0%, 100% { opacity: 0.2; transform: translate(-50%, -50%) scale(1); }
53
+ 50% { opacity: 0.3; transform: translate(-50%, -50%) scale(1.1); }
54
+ }
55
+ .animate-float-slow { animation: float-slow 15s ease-in-out infinite; }
56
+ .animate-float-slower { animation: float-slower 20s ease-in-out infinite; }
57
+ .animate-float-slowest { animation: float-slowest 25s ease-in-out infinite; }
58
+ .animate-pulse-slow { animation: pulse-slow 8s ease-in-out infinite; }
59
+ `}</style>
60
+
61
+ {/* Header Navigation */}
62
+ <header className="relative z-20 flex items-center justify-between px-12 py-8">
63
+ <div className="flex items-center">
64
+ <img
65
+ src="/logo.png"
66
+ alt="PopcornPing Logo"
67
+ className="h-10 w-10"
68
+ onError={(e) => {
69
+ e.target.outerHTML = '<div class="h-10 w-10 flex items-center justify-center text-white text-xl font-bold">P</div>';
70
+ }}
71
+ />
72
+ </div>
73
+
74
+ <nav className="flex items-center space-x-12">
75
+ <a href="#overview" className="text-gray-400 hover:text-white transition text-sm tracking-wider">
76
+ Overview
77
+ </a>
78
+ <a href="#snippets" className="text-gray-400 hover:text-white transition text-sm tracking-wider">
79
+ Snippets
80
+ </a>
81
+ <button
82
+ onClick={() => navigate('/login')}
83
+ className="text-gray-400 hover:text-white transition text-sm tracking-wider"
84
+ >
85
+ Login
86
+ </button>
87
+ </nav>
88
+ </header>
89
+
90
+ {/* Main Content */}
91
+ <div className="relative z-10 flex items-center justify-center min-h-[calc(100vh-100px)]">
92
+ {/* Center Content */}
93
+ <div className="text-center max-w-4xl px-4">
94
+ <h1 className="text-6xl md:text-7xl font-bold tracking-wider text-white mb-8 whitespace-nowrap" style={{ letterSpacing: '0.5em', fontWeight: 700 }}>
95
+ POPCORNPING
96
+ </h1>
97
+
98
+ <p className="text-gray-300 text-base md:text-lg tracking-wide mb-16 font-light leading-relaxed max-w-2xl mx-auto" style={{ letterSpacing: '0.05em' }}>
99
+ Never miss a note, idea or connection. Video call with screen sharing made simple.
100
+ </p>
101
+
102
+ <button
103
+ onClick={() => navigate('/login')}
104
+ className="px-12 py-4 border border-white text-white rounded-full hover:bg-white hover:text-black transition text-xs font-normal tracking-wider uppercase"
105
+ style={{ letterSpacing: '0.2em' }}
106
+ >
107
+ Start Your Scene
108
+ </button>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ </>
113
+ );
114
+ };
115
+
116
+ export default Hero;
frontend/src/components/Login.jsx ADDED
@@ -0,0 +1,290 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { useNavigate, useLocation } from 'react-router-dom';
4
+ import { useAuth } from '../context/AuthContext';
5
+
6
+ // --- Icons (Lucide React) ---
7
+ const ChevronLeftIcon = ({ className }) => (
8
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="m15 18-6-6 6-6"/></svg>
9
+ );
10
+ const AtSignIcon = ({ className }) => (
11
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-3.92 7.94"/></svg>
12
+ );
13
+ const GoogleIcon = ({ className }) => (
14
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}><path d="M12.479,14.265v-3.279h11.049c0.108,0.571,0.164,1.247,0.164,1.979c0,2.46-0.672,5.502-2.84,7.669 C18.744,22.829,16.051,24,12.483,24C5.869,24,0.308,18.613,0.308,12S5.869,0,12.483,0c3.659,0,6.265,1.436,8.223,3.307L18.392,5.62 c-1.404-1.317-3.307-2.341-5.913-2.341C7.65,3.279,3.873,7.171,3.873,12s3.777,8.721,8.606,8.721c3.132,0,4.916-1.258,6.059-2.401 c0.927-0.927,1.537-2.251,1.777-4.059L12.479,14.265z" /></svg>
15
+ );
16
+
17
+ // --- Background Animation Component ---
18
+ function FloatingPaths({ position }) {
19
+ const paths = Array.from({ length: 36 }, (_, i) => ({
20
+ id: i,
21
+ d: `M-${380 - i * 5 * position} -${189 + i * 6}C-${
22
+ 380 - i * 5 * position
23
+ } -${189 + i * 6} -${312 - i * 5 * position} ${216 - i * 6} ${
24
+ 152 - i * 5 * position
25
+ } ${343 - i * 6}C${616 - i * 5 * position} ${470 - i * 6} ${
26
+ 684 - i * 5 * position
27
+ } ${875 - i * 6} ${684 - i * 5 * position} ${875 - i * 6}`,
28
+ color: `rgba(255, 255, 255, ${0.1 + i * 0.03})`,
29
+ width: 0.5 + i * 0.03,
30
+ }));
31
+
32
+ return (
33
+ <div className="pointer-events-none absolute inset-0">
34
+ <svg
35
+ className="h-full w-full text-red-500"
36
+ viewBox="0 0 696 316"
37
+ fill="none"
38
+ >
39
+ <title>Background Paths</title>
40
+ {paths.map((path) => (
41
+ <motion.path
42
+ key={path.id}
43
+ d={path.d}
44
+ stroke="currentColor"
45
+ strokeWidth={path.width}
46
+ strokeOpacity={0.1 + path.id * 0.03}
47
+ initial={{ pathLength: 0.3, opacity: 0.6 }}
48
+ animate={{
49
+ pathLength: 1,
50
+ opacity: [0.3, 0.6, 0.3],
51
+ pathOffset: [0, 1, 0],
52
+ }}
53
+ transition={{
54
+ duration: 20 + Math.random() * 10,
55
+ repeat: Number.POSITIVE_INFINITY,
56
+ ease: 'linear',
57
+ }}
58
+ />
59
+ ))}
60
+ </svg>
61
+ </div>
62
+ );
63
+ }
64
+
65
+ // --- Main Login Page Component ---
66
+ export default function Login() {
67
+ const navigate = useNavigate();
68
+ const location = useLocation();
69
+ const { login, googleLogin, user } = useAuth();
70
+
71
+ const [email, setEmail] = useState('');
72
+ const [password, setPassword] = useState('');
73
+ const [error, setError] = useState('');
74
+ const [loading, setLoading] = useState(false);
75
+
76
+ const rightSideImagePath = '../login-illustration.jpg';
77
+
78
+ // Check for error in URL params
79
+ useEffect(() => {
80
+ const params = new URLSearchParams(location.search);
81
+ const errorParam = params.get('error');
82
+
83
+ if (errorParam) {
84
+ const errorMessages = {
85
+ 'auth_failed': 'Authentication failed. Please try again.',
86
+ 'session_error': 'Session error. Please try again.',
87
+ 'save_error': 'Failed to save session. Please try again.',
88
+ 'verification_failed': 'Could not verify your authentication.',
89
+ 'verification_error': 'Error verifying authentication.',
90
+ 'timed out': 'Database connection timed out. Please try again.',
91
+ };
92
+
93
+ setError(errorMessages[errorParam] || `Error: ${errorParam}`);
94
+ console.error('Login page error:', errorParam);
95
+
96
+ // Clean up URL
97
+ window.history.replaceState({}, '', location.pathname);
98
+ }
99
+ }, [location]);
100
+
101
+ // Redirect if already logged in
102
+ useEffect(() => {
103
+ if (user) {
104
+ console.log('User already logged in, redirecting to dashboard');
105
+ navigate('/dashboard');
106
+ }
107
+ }, [user, navigate]);
108
+
109
+ // Handle Email/Password Login
110
+ const handleEmailLogin = async (e) => {
111
+ e.preventDefault();
112
+ setError('');
113
+ setLoading(true);
114
+
115
+ try {
116
+ await login({ email, password });
117
+ navigate('/dashboard');
118
+ } catch (err) {
119
+ setError(err.response?.data?.message || 'Login failed. Please check your credentials.');
120
+ } finally {
121
+ setLoading(false);
122
+ }
123
+ };
124
+
125
+ // Handle Google Login
126
+ const handleGoogleLogin = () => {
127
+ console.log('Initiating Google OAuth...');
128
+ googleLogin();
129
+ };
130
+
131
+ return (
132
+ <main className="relative md:h-screen md:overflow-hidden lg:grid lg:grid-cols-2 bg-black text-white">
133
+
134
+ {/* --- Left Column: Art & Testimonials --- */}
135
+ <div className="relative hidden h-full flex-col border-r border-gray-800 p-10 lg:flex overflow-hidden">
136
+ {/* Gradient Overlay */}
137
+ <div className="absolute inset-0 z-10 bg-gradient-to-t from-black/80 via-black/20 to-transparent" />
138
+
139
+ {/* Logo */}
140
+ <div className="z-10 flex items-center gap-2">
141
+ <img
142
+ src="/logo.png"
143
+ alt="PopcornPing"
144
+ className="h-8 w-8 object-contain"
145
+ onError={(e) => { e.target.style.display = 'none'; }}
146
+ />
147
+ <span className="text-xl font-bold tracking-wide">PopcornPing</span>
148
+ </div>
149
+
150
+ {/* Testimonial */}
151
+ <div className="z-10 mt-auto max-w-lg">
152
+ <blockquote className="space-y-4">
153
+ <p className="text-xl font-medium leading-relaxed text-gray-200">
154
+ &ldquo;This platform has revolutionized how we conduct creative reviews. The screen sharing quality is unmatched.&rdquo;
155
+ </p>
156
+ </blockquote>
157
+ </div>
158
+
159
+ {/* Animated Background Paths */}
160
+ <div className="absolute inset-0 z-0">
161
+ <FloatingPaths position={1} />
162
+ <FloatingPaths position={-1} />
163
+ </div>
164
+ </div>
165
+
166
+ {/* --- Right Column: Auth Form --- */}
167
+ <div className="relative flex min-h-screen flex-col justify-center p-8 md:p-12 lg:p-16">
168
+
169
+ <div className="absolute inset-0 hidden lg:block z-0">
170
+ <img
171
+ src={rightSideImagePath}
172
+ alt="Login Illustration"
173
+ className="h-full w-full object-cover opacity-20"
174
+ onError={(e) => {
175
+ e.target.style.display = 'none';
176
+ }}
177
+ />
178
+ <div className="absolute inset-0 bg-black/30" />
179
+ </div>
180
+
181
+ {/* Background Glow Effects */}
182
+ <div aria-hidden="true" className="absolute inset-0 isolate -z-10 overflow-hidden">
183
+ <div className="absolute top-0 right-0 h-[500px] w-[500px] bg-purple-900/10 blur-[100px] rounded-full translate-x-1/2 -translate-y-1/2" />
184
+ <div className="absolute bottom-0 right-0 h-[400px] w-[400px] bg-blue-900/10 blur-[100px] rounded-full translate-x-1/3 translate-y-1/3" />
185
+ </div>
186
+
187
+ {/* Back to Home Button */}
188
+ <button
189
+ onClick={() => navigate('/')}
190
+ className="absolute top-8 left-8 md:left-12 inline-flex items-center text-sm font-medium text-gray-400 hover:text-white transition-colors z-20"
191
+ >
192
+ <ChevronLeftIcon className="w-4 h-4 mr-2" />
193
+ Home
194
+ </button>
195
+
196
+ {/* Form Container */}
197
+ <div className="mx-auto w-full max-w-sm space-y-8 relative z-20">
198
+
199
+ {/* Mobile Logo */}
200
+ <div className="flex items-center gap-2 lg:hidden mb-8">
201
+ <span className="text-xl font-bold">PopcornPing</span>
202
+ </div>
203
+
204
+ {/* Headers */}
205
+ <div className="flex flex-col space-y-2">
206
+ <h1 className="text-3xl font-bold tracking-tight text-white drop-shadow-lg">
207
+ Sign In or Join Now!
208
+ </h1>
209
+ <p className="text-gray-300 text-sm drop-shadow-md">
210
+ Login or create your PopcornPing account.
211
+ </p>
212
+ </div>
213
+
214
+ {/* Social Login Buttons */}
215
+ <div className="space-y-3">
216
+ <button
217
+ onClick={handleGoogleLogin}
218
+ className="w-full flex items-center justify-center gap-2 bg-white text-black hover:bg-gray-200 h-12 rounded-md font-medium transition-colors"
219
+ >
220
+ <GoogleIcon className="w-5 h-5" />
221
+ Continue with Google
222
+ </button>
223
+ </div>
224
+
225
+ {/* Separator */}
226
+ <div className="relative">
227
+ <div className="absolute inset-0 flex items-center">
228
+ <div className="w-full border-t border-gray-800" />
229
+ </div>
230
+ <div className="relative flex justify-center text-xs uppercase">
231
+ <span className="bg-black px-2 text-gray-500">Or</span>
232
+ </div>
233
+ </div>
234
+
235
+ {/* Email Form */}
236
+ <form onSubmit={handleEmailLogin} className="space-y-4">
237
+ {error && (
238
+ <div className="p-3 text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-md">
239
+ {error}
240
+ </div>
241
+ )}
242
+
243
+ <div className="space-y-4">
244
+ <div className="space-y-2">
245
+ <label className="text-xs font-medium text-gray-400 ml-1">Email Address</label>
246
+ <div className="relative">
247
+ <input
248
+ type="email"
249
+ placeholder="your.email@example.com"
250
+ value={email}
251
+ onChange={(e) => setEmail(e.target.value)}
252
+ required
253
+ className="w-full h-11 bg-gray-900/80 border border-gray-800 rounded-md pl-10 pr-4 text-white text-sm focus:outline-none focus:ring-2 focus:ring-white/20 transition-all placeholder:text-gray-600"
254
+ />
255
+ <div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none text-gray-500">
256
+ <AtSignIcon className="w-4 h-4" />
257
+ </div>
258
+ </div>
259
+ </div>
260
+
261
+ {/* Password Field */}
262
+ <div className="space-y-2">
263
+ <label className="text-xs font-medium text-gray-400 ml-1">Password</label>
264
+ <div className="relative">
265
+ <input
266
+ type="password"
267
+ placeholder="••••••••"
268
+ value={password}
269
+ onChange={(e) => setPassword(e.target.value)}
270
+ required
271
+ className="w-full h-11 bg-gray-900/80 border border-gray-800 rounded-md pl-4 pr-4 text-white text-sm focus:outline-none focus:ring-2 focus:ring-white/20 transition-all placeholder:text-gray-600"
272
+ />
273
+ </div>
274
+ </div>
275
+ </div>
276
+
277
+ <button
278
+ type="submit"
279
+ disabled={loading}
280
+ className="w-full h-12 bg-white text-black font-semibold rounded-md hover:bg-gray-200 transition-colors disabled:opacity-50 disabled:cursor-not-allowed mt-2"
281
+ >
282
+ {loading ? 'Processing...' : 'Continue With Email'}
283
+ </button>
284
+ </form>
285
+
286
+ </div>
287
+ </div>
288
+ </main>
289
+ );
290
+ }
frontend/src/components/LoginModal.jsx ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { useAuth } from '../context/AuthContext';
3
+ import { useNavigate } from 'react-router-dom';
4
+
5
+ // Icons
6
+ const CloseIcon = ({ className }) => (
7
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
8
+ );
9
+ const GoogleIcon = ({ className }) => (
10
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
11
+ <path d="M12.479,14.265v-3.279h11.049c0.108,0.571,0.164,1.247,0.164,1.979c0,2.46-0.672,5.502-2.84,7.669 C18.744,22.829,16.051,24,12.483,24C5.869,24,0.308,18.613,0.308,12S5.869,0,12.483,0c3.659,0,6.265,1.436,8.223,3.307L18.392,5.62 c-1.404-1.317-3.307-2.341-5.913-2.341C7.65,3.279,3.873,7.171,3.873,12s3.777,8.721,8.606,8.721c3.132,0,4.916-1.258,6.059-2.401 c0.927-0.927,1.537-2.251,1.777-4.059L12.479,14.265z" />
12
+ </svg>
13
+ );
14
+ const AtSignIcon = ({ className }) => (
15
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-3.92 7.94"/></svg>
16
+ );
17
+ const LockIcon = ({ className }) => (
18
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
19
+ );
20
+
21
+ const LoginModal = ({ onClose, onSignup }) => {
22
+ const [email, setEmail] = useState('');
23
+ const [password, setPassword] = useState('');
24
+ const [error, setError] = useState('');
25
+ const [loading, setLoading] = useState(false);
26
+ const { login, googleLogin } = useAuth();
27
+ const navigate = useNavigate();
28
+
29
+ const handleSubmit = async (e) => {
30
+ e.preventDefault();
31
+ setError('');
32
+ setLoading(true);
33
+
34
+ try {
35
+ await login({ email, password });
36
+ onClose();
37
+ navigate('/dashboard');
38
+ } catch (err) {
39
+ setError(err.response?.data?.message || 'Invalid email or password');
40
+ } finally {
41
+ setLoading(false);
42
+ }
43
+ };
44
+
45
+ const handleGoogleLogin = () => {
46
+ googleLogin();
47
+ };
48
+
49
+ return (
50
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
51
+ {/* Container echoing the AuthPage styling */}
52
+ <div className="relative w-full max-w-md bg-black border border-gray-800 rounded-xl overflow-hidden shadow-2xl">
53
+
54
+ {/* Background Gradients from provided code */}
55
+ <div className="absolute inset-0 pointer-events-none opacity-40">
56
+ <div className="absolute top-0 right-0 h-[300px] w-[300px] bg-purple-900/20 blur-[100px] rounded-full translate-x-1/3 -translate-y-1/3" />
57
+ <div className="absolute bottom-0 left-0 h-[300px] w-[300px] bg-blue-900/10 blur-[100px] rounded-full -translate-x-1/3 translate-y-1/3" />
58
+ </div>
59
+
60
+ {/* Close Button */}
61
+ <button onClick={onClose} className="absolute top-4 right-4 text-gray-400 hover:text-white transition z-20">
62
+ <CloseIcon className="w-5 h-5" />
63
+ </button>
64
+
65
+ <div className="relative z-10 p-8">
66
+
67
+ {/* Header */}
68
+ <div className="flex flex-col space-y-1 mb-6 text-center md:text-left">
69
+ <h1 className="text-2xl font-bold tracking-wide text-white">Sign In</h1>
70
+ <p className="text-gray-400 text-sm">
71
+ Welcome back to PopcornPing.
72
+ </p>
73
+ </div>
74
+
75
+ {/* Google Button */}
76
+ <div className="space-y-2 mb-6">
77
+ <button
78
+ onClick={handleGoogleLogin}
79
+ type="button"
80
+ className="w-full flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium h-10 px-4 py-2 border border-gray-700 bg-gray-900 hover:bg-gray-800 hover:text-white text-gray-200 transition-colors"
81
+ >
82
+ <GoogleIcon className="w-4 h-4 mr-2" />
83
+ Continue with Google
84
+ </button>
85
+ </div>
86
+
87
+ {/* Separator */}
88
+ <div className="flex w-full items-center justify-center mb-6">
89
+ <div className="bg-gray-800 h-px w-full" />
90
+ <span className="text-gray-500 px-2 text-xs uppercase">OR</span>
91
+ <div className="bg-gray-800 h-px w-full" />
92
+ </div>
93
+
94
+ {/* Error Message */}
95
+ {error && (
96
+ <div className="mb-4 p-3 bg-red-500/10 border border-red-500/50 rounded-md text-red-500 text-sm">
97
+ {error}
98
+ </div>
99
+ )}
100
+
101
+ {/* Email Form */}
102
+ <form onSubmit={handleSubmit} className="space-y-4">
103
+ <div className="space-y-2">
104
+ <div className="relative">
105
+ <input
106
+ type="email"
107
+ placeholder="name@example.com"
108
+ value={email}
109
+ onChange={(e) => setEmail(e.target.value)}
110
+ required
111
+ className="flex h-10 w-full rounded-md border border-gray-700 bg-black px-3 py-2 pl-9 text-sm text-white placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent transition-all"
112
+ />
113
+ <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-gray-500">
114
+ <AtSignIcon className="w-4 h-4" />
115
+ </div>
116
+ </div>
117
+ </div>
118
+
119
+ <div className="space-y-2">
120
+ <div className="relative">
121
+ <input
122
+ type="password"
123
+ placeholder="Password"
124
+ value={password}
125
+ onChange={(e) => setPassword(e.target.value)}
126
+ required
127
+ className="flex h-10 w-full rounded-md border border-gray-700 bg-black px-3 py-2 pl-9 text-sm text-white placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent transition-all"
128
+ />
129
+ <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-gray-500">
130
+ <LockIcon className="w-4 h-4" />
131
+ </div>
132
+ </div>
133
+ </div>
134
+
135
+ <button
136
+ type="submit"
137
+ disabled={loading}
138
+ className="w-full inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium h-10 px-4 py-2 bg-purple-600 text-white hover:bg-purple-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
139
+ >
140
+ {loading ? 'Signing in...' : 'Sign In'}
141
+ </button>
142
+ </form>
143
+
144
+ {/* Footer */}
145
+ <p className="mt-6 text-center text-sm text-gray-400">
146
+ Don't have an account?{' '}
147
+ <button
148
+ onClick={onSignup}
149
+ className="text-purple-400 hover:text-purple-300 font-medium underline underline-offset-4"
150
+ >
151
+ Sign Up
152
+ </button>
153
+ </p>
154
+ </div>
155
+ </div>
156
+ </div>
157
+ );
158
+ };
159
+
160
+ export default LoginModal;
frontend/src/components/Navbar.jsx ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { useAuth } from '../context/AuthContext';
4
+
5
+ // Default avatar for non-Google users
6
+ const DEFAULT_AVATAR = "https://api.dicebear.com/7.x/avataaars/svg?seed=PopcornPing";
7
+
8
+ const Navbar = () => {
9
+ const navigate = useNavigate();
10
+ const { user, logout } = useAuth();
11
+
12
+ const handleLogout = async () => {
13
+ try {
14
+ await logout();
15
+ navigate('/login');
16
+ } catch (error) {
17
+ console.error("Logout failed", error);
18
+ }
19
+ };
20
+
21
+ const getAvatarUrl = () => {
22
+ if (user && user.avatar && user.avatar.trim() !== '') {
23
+ return user.avatar;
24
+ }
25
+ // Otherwise use default avatar
26
+ return DEFAULT_AVATAR;
27
+ };
28
+
29
+ // Get display name
30
+ const getDisplayName = () => {
31
+ if (!user) return 'User';
32
+ return user.username || user.name || 'User';
33
+ };
34
+
35
+ return (
36
+ <header className="relative z-20 flex items-center justify-between px-12 py-8 w-full">
37
+ {/* Left: Logo Section */}
38
+ <div
39
+ className="flex items-center cursor-pointer"
40
+ onClick={() => navigate('/')}
41
+ >
42
+ <img
43
+ src="/logo.png"
44
+ alt="PopcornPing Logo"
45
+ className="h-10 w-10"
46
+ onError={(e) => {
47
+ e.target.style.display = 'none';
48
+ e.target.parentNode.innerHTML = '<div class="h-10 w-10 flex items-center justify-center text-white text-xl font-bold border border-white rounded-full">P</div>';
49
+ }}
50
+ />
51
+ <span className="ml-4 text-white font-bold tracking-[0.2em] text-sm uppercase">
52
+ PopcornPing
53
+ </span>
54
+ </div>
55
+
56
+ {/* Right: User Dashboard Details */}
57
+ {user ? (
58
+ <div className="flex items-center gap-6">
59
+
60
+ <div className="flex items-center gap-3">
61
+ <div className="flex flex-col items-end">
62
+ {/* Name from AuthContext */}
63
+ <span className="text-white text-sm font-semibold tracking-wider">
64
+ {getDisplayName()}
65
+ </span>
66
+ {/* Email from AuthContext */}
67
+ <span className="text-[10px] text-gray-300 uppercase tracking-widest">
68
+ {user.email}
69
+ </span>
70
+ </div>
71
+
72
+ {/* Dynamic Avatar */}
73
+ <div className="h-10 w-10 rounded-full border border-white/30 bg-white/10 flex items-center justify-center overflow-hidden">
74
+ <img
75
+ src={getAvatarUrl()}
76
+ alt="Profile"
77
+ className="h-full w-full object-cover"
78
+ onError={(e) => {
79
+ // Fallback if image fails to load (e.g., bad URL or network error)
80
+ console.error('Avatar failed to load, using default');
81
+ e.target.src = DEFAULT_AVATAR;
82
+ }}
83
+ />
84
+ </div>
85
+ </div>
86
+
87
+ <div className="h-6 w-px bg-white/20"></div>
88
+
89
+ <button
90
+ onClick={handleLogout}
91
+ className="text-xs text-white hover:text-gray-300 transition-colors uppercase tracking-widest"
92
+ >
93
+ Sign Out
94
+ </button>
95
+ </div>
96
+ ) : (
97
+ <button
98
+ onClick={() => navigate('/login')}
99
+ className="text-gray-400 hover:text-white transition text-sm tracking-wider"
100
+ >
101
+ Login
102
+ </button>
103
+ )}
104
+ </header>
105
+ );
106
+ };
107
+
108
+ export default Navbar;
frontend/src/components/ProtectedRoute.jsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { Navigate } from 'react-router-dom';
3
+ import { useAuth } from '../context/AuthContext';
4
+
5
+ const ProtectedRoute = ({ children }) => {
6
+ const { user, loading } = useAuth();
7
+
8
+ if (loading) {
9
+ return (
10
+ <div className="min-h-screen flex items-center justify-center bg-gray-900">
11
+ <div className="text-white text-xl">Loading...</div>
12
+ </div>
13
+ );
14
+ }
15
+
16
+ return user ? children : <Navigate to="/" />;
17
+ };
18
+
19
+ export default ProtectedRoute;
frontend/src/components/SignupModal.jsx ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { useAuth } from '../context/AuthContext';
3
+ import { useNavigate } from 'react-router-dom';
4
+
5
+ // Icons
6
+ const CloseIcon = ({ className }) => (
7
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
8
+ );
9
+ const GoogleIcon = ({ className }) => (
10
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
11
+ <path d="M12.479,14.265v-3.279h11.049c0.108,0.571,0.164,1.247,0.164,1.979c0,2.46-0.672,5.502-2.84,7.669 C18.744,22.829,16.051,24,12.483,24C5.869,24,0.308,18.613,0.308,12S5.869,0,12.483,0c3.659,0,6.265,1.436,8.223,3.307L18.392,5.62 c-1.404-1.317-3.307-2.341-5.913-2.341C7.65,3.279,3.873,7.171,3.873,12s3.777,8.721,8.606,8.721c3.132,0,4.916-1.258,6.059-2.401 c0.927-0.927,1.537-2.251,1.777-4.059L12.479,14.265z" />
12
+ </svg>
13
+ );
14
+ const AtSignIcon = ({ className }) => (
15
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-3.92 7.94"/></svg>
16
+ );
17
+ const UserIcon = ({ className }) => (
18
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
19
+ );
20
+ const LockIcon = ({ className }) => (
21
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
22
+ );
23
+
24
+ const SignupModal = ({ onClose, onLogin }) => {
25
+ const [formData, setFormData] = useState({
26
+ username: '',
27
+ email: '',
28
+ password: '',
29
+ });
30
+ const [error, setError] = useState('');
31
+ const [loading, setLoading] = useState(false);
32
+ const { register, googleLogin } = useAuth();
33
+ const navigate = useNavigate();
34
+
35
+ const handleChange = (e) => {
36
+ setFormData({
37
+ ...formData,
38
+ [e.target.name]: e.target.value,
39
+ });
40
+ };
41
+
42
+ const handleSubmit = async (e) => {
43
+ e.preventDefault();
44
+ setError('');
45
+
46
+ if (formData.password.length < 6) {
47
+ setError('Password must be at least 6 characters');
48
+ return;
49
+ }
50
+
51
+ setLoading(true);
52
+
53
+ try {
54
+ await register(formData);
55
+ onClose();
56
+ navigate('/dashboard');
57
+ } catch (err) {
58
+ setError(err.response?.data?.message || 'Registration failed');
59
+ } finally {
60
+ setLoading(false);
61
+ }
62
+ };
63
+
64
+ const handleGoogleLogin = () => {
65
+ googleLogin();
66
+ };
67
+
68
+ return (
69
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
70
+ {/* Container echoing the AuthPage styling */}
71
+ <div className="relative w-full max-w-md bg-black border border-gray-800 rounded-xl overflow-hidden shadow-2xl">
72
+
73
+ {/* Background Gradients */}
74
+ <div className="absolute inset-0 pointer-events-none opacity-40">
75
+ <div className="absolute top-0 right-0 h-[300px] w-[300px] bg-purple-900/20 blur-[100px] rounded-full translate-x-1/3 -translate-y-1/3" />
76
+ <div className="absolute bottom-0 left-0 h-[300px] w-[300px] bg-blue-900/10 blur-[100px] rounded-full -translate-x-1/3 translate-y-1/3" />
77
+ </div>
78
+
79
+ {/* Close Button */}
80
+ <button onClick={onClose} className="absolute top-4 right-4 text-gray-400 hover:text-white transition z-20">
81
+ <CloseIcon className="w-5 h-5" />
82
+ </button>
83
+
84
+ <div className="relative z-10 p-8">
85
+
86
+ {/* Header */}
87
+ <div className="flex flex-col space-y-1 mb-6 text-center md:text-left">
88
+ <h1 className="text-2xl font-bold tracking-wide text-white">Create Account</h1>
89
+ <p className="text-gray-400 text-sm">
90
+ Sign up to start your journey.
91
+ </p>
92
+ </div>
93
+
94
+ {/* Google Button */}
95
+ <div className="space-y-2 mb-6">
96
+ <button
97
+ onClick={handleGoogleLogin}
98
+ type="button"
99
+ className="w-full flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium h-10 px-4 py-2 border border-gray-700 bg-gray-900 hover:bg-gray-800 hover:text-white text-gray-200 transition-colors"
100
+ >
101
+ <GoogleIcon className="w-4 h-4 mr-2" />
102
+ Continue with Google
103
+ </button>
104
+ </div>
105
+
106
+ {/* Separator */}
107
+ <div className="flex w-full items-center justify-center mb-6">
108
+ <div className="bg-gray-800 h-px w-full" />
109
+ <span className="text-gray-500 px-2 text-xs uppercase">OR</span>
110
+ <div className="bg-gray-800 h-px w-full" />
111
+ </div>
112
+
113
+ {/* Error Message */}
114
+ {error && (
115
+ <div className="mb-4 p-3 bg-red-500/10 border border-red-500/50 rounded-md text-red-500 text-sm">
116
+ {error}
117
+ </div>
118
+ )}
119
+
120
+ {/* Signup Form */}
121
+ <form onSubmit={handleSubmit} className="space-y-4">
122
+
123
+ {/* Username */}
124
+ <div className="space-y-2">
125
+ <div className="relative">
126
+ <input
127
+ type="text"
128
+ name="username"
129
+ placeholder="Username"
130
+ value={formData.username}
131
+ onChange={handleChange}
132
+ required
133
+ className="flex h-10 w-full rounded-md border border-gray-700 bg-black px-3 py-2 pl-9 text-sm text-white placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent transition-all"
134
+ />
135
+ <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-gray-500">
136
+ <UserIcon className="w-4 h-4" />
137
+ </div>
138
+ </div>
139
+ </div>
140
+
141
+ {/* Email */}
142
+ <div className="space-y-2">
143
+ <div className="relative">
144
+ <input
145
+ type="email"
146
+ name="email"
147
+ placeholder="name@example.com"
148
+ value={formData.email}
149
+ onChange={handleChange}
150
+ required
151
+ className="flex h-10 w-full rounded-md border border-gray-700 bg-black px-3 py-2 pl-9 text-sm text-white placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent transition-all"
152
+ />
153
+ <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-gray-500">
154
+ <AtSignIcon className="w-4 h-4" />
155
+ </div>
156
+ </div>
157
+ </div>
158
+
159
+ {/* Password */}
160
+ <div className="space-y-2">
161
+ <div className="relative">
162
+ <input
163
+ type="password"
164
+ name="password"
165
+ placeholder="Password (min 6 chars)"
166
+ value={formData.password}
167
+ onChange={handleChange}
168
+ required
169
+ className="flex h-10 w-full rounded-md border border-gray-700 bg-black px-3 py-2 pl-9 text-sm text-white placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent transition-all"
170
+ />
171
+ <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-gray-500">
172
+ <LockIcon className="w-4 h-4" />
173
+ </div>
174
+ </div>
175
+ </div>
176
+
177
+ <button
178
+ type="submit"
179
+ disabled={loading}
180
+ className="w-full inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium h-10 px-4 py-2 bg-purple-600 text-white hover:bg-purple-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
181
+ >
182
+ {loading ? 'Creating account...' : 'Create Account'}
183
+ </button>
184
+ </form>
185
+
186
+ {/* Footer */}
187
+ <p className="mt-6 text-center text-sm text-gray-400">
188
+ Already have an account?{' '}
189
+ <button
190
+ onClick={onLogin}
191
+ className="text-purple-400 hover:text-purple-300 font-medium underline underline-offset-4"
192
+ >
193
+ Log In
194
+ </button>
195
+ </p>
196
+ </div>
197
+ </div>
198
+ </div>
199
+ );
200
+ };
201
+
202
+ export default SignupModal;
frontend/src/components/Snippets.jsx ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ const Snippets = () => {
4
+ // Images for the infinite scroll
5
+ const images = [
6
+ "https://images.unsplash.com/photo-1518495973542-4542c06a5843?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
7
+ "https://images.unsplash.com/photo-1472396961693-142e6e269027?q=80&w=2152&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
8
+ "https://images.unsplash.com/photo-1505142468610-359e7d316be0?q=80&w=2126&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
9
+ "https://images.unsplash.com/photo-1482881497185-d4a9ddbe4151?q=80&w=1965&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
10
+ "https://plus.unsplash.com/premium_photo-1673264933212-d78737f38e48?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
11
+ "https://plus.unsplash.com/premium_photo-1711434824963-ca894373272e?q=80&w=2030&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
12
+ "https://plus.unsplash.com/premium_photo-1675705721263-0bbeec261c49?q=80&w=1940&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
13
+ "https://images.unsplash.com/photo-1524799526615-766a9833dec0?q=80&w=1935&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
14
+ ];
15
+
16
+ // Duplicate images for seamless loop
17
+ const duplicatedImages = [...images, ...images];
18
+
19
+ return (
20
+ <>
21
+ <style jsx>{`
22
+ /* --- Scroll Animations Only --- */
23
+ @keyframes scroll-right {
24
+ 0% { transform: translateX(0); }
25
+ 100% { transform: translateX(-50%); }
26
+ }
27
+ .infinite-scroll {
28
+ animation: scroll-right 30s linear infinite;
29
+ }
30
+ .scroll-container {
31
+ mask: linear-gradient(90deg, transparent 0%, black 10%, black 90%, transparent 100%);
32
+ -webkit-mask: linear-gradient(90deg, transparent 0%, black 10%, black 90%, transparent 100%);
33
+ }
34
+ .image-item {
35
+ transition: transform 0.3s ease, filter 0.3s ease;
36
+ }
37
+ .image-item:hover {
38
+ transform: scale(1.05);
39
+ filter: brightness(1.1);
40
+ }
41
+ `}</style>
42
+
43
+ <div id="snippets" className="min-h-screen bg-black relative overflow-hidden flex flex-col items-center justify-center py-20">
44
+
45
+ {/* --- 1. Background Image Effect (No Glow) --- */}
46
+ <div className="absolute inset-0 z-0">
47
+ <img
48
+ src="/snippets-bg.jpg"
49
+ alt="Background"
50
+ className="w-full h-full object-cover opacity-60"
51
+ onError={(e) => { e.target.style.display = 'none'; }}
52
+ />
53
+ {/* Gradient Overlay matching Hero */}
54
+ <div className="absolute inset-0 bg-gradient-to-b from-black/60 via-black/40 to-black/60"></div>
55
+ </div>
56
+
57
+ {/* --- 2. Main Content (Snippets) --- */}
58
+ {/* Section Header */}
59
+ <div className="relative z-10 text-center mb-16 px-4">
60
+ <h2 className="text-4xl md:text-5xl font-bold text-white mb-4 tracking-wider drop-shadow-lg">
61
+ SNIPPETS
62
+ </h2>
63
+ <p className="text-gray-300 text-lg font-light tracking-wide drop-shadow-md">
64
+ Captured moments from our community
65
+ </p>
66
+ </div>
67
+
68
+ {/* Scrolling images container */}
69
+ <div className="relative z-10 w-full flex items-center justify-center">
70
+ <div className="scroll-container w-full">
71
+ <div className="infinite-scroll flex gap-6 w-max px-6">
72
+ {duplicatedImages.map((image, index) => (
73
+ <div
74
+ key={index}
75
+ className="image-item flex-shrink-0 w-64 h-64 md:w-72 md:h-72 lg:w-80 lg:h-80 rounded-2xl overflow-hidden shadow-2xl border border-gray-800/50"
76
+ >
77
+ <img
78
+ src={image}
79
+ alt={`Gallery image ${(index % images.length) + 1}`}
80
+ className="w-full h-full object-cover"
81
+ loading="lazy"
82
+ />
83
+ </div>
84
+ ))}
85
+ </div>
86
+ </div>
87
+ </div>
88
+
89
+ </div>
90
+ </>
91
+ );
92
+ };
93
+
94
+ export default Snippets;
frontend/src/components/VideoRoom.jsx ADDED
@@ -0,0 +1,534 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { useParams, useNavigate } from 'react-router-dom';
3
+ import io from 'socket.io-client';
4
+ import Peer from 'simple-peer';
5
+ import { roomAPI } from '../utils/api';
6
+ import { useAuth } from '../context/AuthContext';
7
+
8
+ // --- Icons ---
9
+ const MicIcon = ({ className }) => (
10
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" x2="12" y1="19" y2="22"/></svg>
11
+ );
12
+ const MicOffIcon = ({ className }) => (
13
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><line x1="1" x2="23" y1="1" y2="23" stroke="currentColor"/><path d="M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6" stroke="currentColor"/><path d="M17 16.95A7 7 0 0 1 5 12v-2m14 0v2a7 7 0 0 1-.11 1.23" stroke="currentColor"/><line x1="12" x2="12" y1="19" y2="23" stroke="currentColor"/></svg>
14
+ );
15
+ const VideoIcon = ({ className }) => (
16
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="m22 8-6 4 6 4V8Z"/><rect width="14" height="12" x="2" y="6" rx="2" ry="2"/></svg>
17
+ );
18
+ const VideoOffIcon = ({ className }) => (
19
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="M10.66 5H14a2 2 0 0 1 2 2v2.34l1 1L22 7v10"/><path d="M16 16a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2l10 10Z"/><line x1="2" x2="22" y1="2" y2="22"/></svg>
20
+ );
21
+ const MonitorIcon = ({ className }) => (
22
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/></svg>
23
+ );
24
+ const PhoneOffIcon = ({ className }) => (
25
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><path d="M10.68 13.31a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7 2 2 0 0 1 1.72 2v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.42 19.42 0 0 1-3.33-2.67m-2.67-3.34a19.79 19.79 0 0 1-3.07-8.63A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91"/><line x1="22" x2="2" y1="2" y2="22"/></svg>
26
+ );
27
+ const CopyIcon = ({ className }) => (
28
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
29
+ );
30
+ const PinIcon = ({ className, filled }) => (
31
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill={filled ? "currentColor" : "none"} stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}><line x1="12" x2="12" y1="17" y2="22"/><path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24Z"/></svg>
32
+ );
33
+
34
+ // --- Video Player Component ---
35
+ const VideoPlayer = ({ stream, isLocal = false, onPin, isPinned, label }) => {
36
+ const ref = useRef();
37
+
38
+ useEffect(() => {
39
+ if (ref.current && stream) {
40
+ ref.current.srcObject = stream;
41
+ }
42
+ }, [stream]);
43
+
44
+ return (
45
+ <div className={`relative bg-[#1a1a1a] rounded-xl overflow-hidden border border-gray-800 shadow-xl transition-all ${isPinned ? 'w-full h-full' : 'w-full h-full'}`}>
46
+ <video
47
+ ref={ref}
48
+ autoPlay
49
+ playsInline
50
+ muted={isLocal}
51
+ style={{ transform: isLocal && !label?.toLowerCase().includes('screen') ? 'scaleX(-1)' : 'none' }}
52
+ className="w-full h-full object-contain bg-black"
53
+ />
54
+
55
+ {/* Overlay Info */}
56
+ <div className="absolute top-0 left-0 right-0 p-3 bg-gradient-to-b from-black/70 to-transparent flex justify-between items-start opacity-0 hover:opacity-100 transition-opacity duration-300">
57
+ <div className="bg-black/40 backdrop-blur-md px-2 py-1 rounded text-xs font-medium text-white">
58
+ {label || 'User'}
59
+ </div>
60
+ <button
61
+ onClick={onPin}
62
+ className={`p-1.5 rounded-full backdrop-blur-md transition-colors ${isPinned ? 'bg-blue-600 text-white' : 'bg-black/40 text-gray-300 hover:bg-white/20'}`}
63
+ title={isPinned ? "Unpin" : "Pin"}
64
+ >
65
+ <PinIcon className="w-3.5 h-3.5" filled={isPinned} />
66
+ </button>
67
+ </div>
68
+ </div>
69
+ );
70
+ };
71
+
72
+ const VideoRoom = () => {
73
+ const { roomId } = useParams();
74
+ const { user } = useAuth();
75
+ const navigate = useNavigate();
76
+
77
+ // State
78
+ const [peers, setPeers] = useState([]); // Array of { peerID, peer }
79
+ const [remoteStreams, setRemoteStreams] = useState([]); // Array of { id, stream, peerID }
80
+ const [userStream, setUserStream] = useState(null);
81
+ const [screenStream, setScreenStream] = useState(null);
82
+ const [pinnedStreamId, setPinnedStreamId] = useState(null); // ID of pinned stream
83
+
84
+ // Controls
85
+ const [isMuted, setIsMuted] = useState(false);
86
+ const [isVideoOff, setIsVideoOff] = useState(false);
87
+ const [isScreenSharing, setIsScreenSharing] = useState(false);
88
+ const [roomInfo, setRoomInfo] = useState(null);
89
+ const [showCopied, setShowCopied] = useState(false);
90
+
91
+ // Refs
92
+ const socketRef = useRef();
93
+ const peersRef = useRef([]); // Keep track of peers for cleanup
94
+ const userStreamRef = useRef();
95
+ const screenStreamRef = useRef(); // Corrected declaration
96
+ const isMounted = useRef(true); // Track mount status
97
+
98
+ // --- Initialization & Cleanup ---
99
+ useEffect(() => {
100
+ isMounted.current = true;
101
+
102
+ // Prevent accidental refresh
103
+ const handleBeforeUnload = (e) => {
104
+ e.preventDefault();
105
+ e.returnValue = '';
106
+ };
107
+ window.addEventListener('beforeunload', handleBeforeUnload);
108
+
109
+ const init = async () => {
110
+ try {
111
+ await fetchRoomInfo();
112
+ if (isMounted.current) {
113
+ await initializeMedia();
114
+ }
115
+ } catch (err) {
116
+ console.error(err);
117
+ }
118
+ };
119
+
120
+ init();
121
+
122
+ return () => {
123
+ isMounted.current = false;
124
+ window.removeEventListener('beforeunload', handleBeforeUnload);
125
+ cleanupConnection();
126
+ };
127
+ }, [roomId]);
128
+
129
+ const cleanupConnection = () => {
130
+ console.log("Cleaning up connections...");
131
+
132
+ // 1. Stop Local Camera
133
+ if (userStreamRef.current) {
134
+ userStreamRef.current.getTracks().forEach(track => {
135
+ track.stop();
136
+ track.enabled = false;
137
+ });
138
+ userStreamRef.current = null;
139
+ }
140
+
141
+ // 2. Stop Screen Share
142
+ if (screenStreamRef.current) {
143
+ screenStreamRef.current.getTracks().forEach(track => track.stop());
144
+ screenStreamRef.current = null;
145
+ }
146
+
147
+ // 3. Destroy Peers
148
+ peersRef.current.forEach(({ peer }) => {
149
+ if (peer && !peer.destroyed) {
150
+ peer.destroy();
151
+ }
152
+ });
153
+ peersRef.current = [];
154
+
155
+ // Clear state only if mounted to avoid react warnings
156
+ if (isMounted.current) {
157
+ setPeers([]);
158
+ setRemoteStreams([]);
159
+ setUserStream(null);
160
+ setScreenStream(null);
161
+ }
162
+
163
+ // 4. Disconnect Socket
164
+ if (socketRef.current) {
165
+ socketRef.current.disconnect();
166
+ socketRef.current = null;
167
+ }
168
+ };
169
+
170
+ const fetchRoomInfo = async () => {
171
+ try {
172
+ const response = await roomAPI.getRoom(roomId);
173
+ if (isMounted.current) {
174
+ setRoomInfo(response.data.room);
175
+ await roomAPI.joinRoom(roomId);
176
+ }
177
+ } catch (error) {
178
+ console.error('Error fetching room:', error);
179
+ if (isMounted.current) navigate('/dashboard');
180
+ }
181
+ };
182
+
183
+ // --- WebRTC Logic ---
184
+ const initializeMedia = async () => {
185
+ try {
186
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
187
+
188
+ // CRITICAL: Check if still mounted after async call
189
+ if (!isMounted.current) {
190
+ // If unmounted during getUserMedia, immediately stop these tracks
191
+ stream.getTracks().forEach(t => t.stop());
192
+ return;
193
+ }
194
+
195
+ // If there was an old stream pending in ref (race condition), stop it
196
+ if (userStreamRef.current) {
197
+ userStreamRef.current.getTracks().forEach(t => t.stop());
198
+ }
199
+
200
+ userStreamRef.current = stream;
201
+ setUserStream(stream);
202
+
203
+ socketRef.current = io(process.env.REACT_APP_SOCKET_URL);
204
+ socketRef.current.emit('join-room', roomId, user.id);
205
+
206
+ // --- Socket Events ---
207
+
208
+ socketRef.current.on('user-connected', (userId) => {
209
+ if (!isMounted.current) return;
210
+ const peer = createPeer(userId, socketRef.current.id, stream);
211
+ peersRef.current.push({ peerID: userId, peer });
212
+ setPeers(prev => [...prev, { peerID: userId, peer }]);
213
+ });
214
+
215
+ socketRef.current.on('user-disconnected', (userId) => {
216
+ if (!isMounted.current) return;
217
+ const peerObj = peersRef.current.find(p => p.peerID === userId);
218
+ if (peerObj && peerObj.peer) peerObj.peer.destroy();
219
+
220
+ peersRef.current = peersRef.current.filter(p => p.peerID !== userId);
221
+ setPeers(prev => prev.filter(p => p.peerID !== userId));
222
+ setRemoteStreams(prev => prev.filter(s => s.peerID !== userId));
223
+ });
224
+
225
+ socketRef.current.on('offer', (offer, id) => {
226
+ if (!isMounted.current) return;
227
+ const peer = addPeer(offer, id, stream);
228
+ peersRef.current.push({ peerID: id, peer });
229
+ setPeers(prev => [...prev, { peerID: id, peer }]);
230
+ });
231
+
232
+ socketRef.current.on('answer', (answer, id) => {
233
+ const p = peersRef.current.find(p => p.peerID === id);
234
+ if (p) p.peer.signal(answer);
235
+ });
236
+
237
+ socketRef.current.on('ice-candidate', (candidate, id) => {
238
+ const p = peersRef.current.find(p => p.peerID === id);
239
+ if (p) p.peer.signal(candidate);
240
+ });
241
+
242
+ // Notification only - stream handling is done via peer.on('stream')
243
+ socketRef.current.on('screen-share-started', (id) => {
244
+ console.log(`User ${id} started screen sharing`);
245
+ });
246
+
247
+ socketRef.current.on('screen-share-stopped', (id) => {
248
+ console.log(`User ${id} stopped screen sharing`);
249
+ setRemoteStreams(prev => prev.filter(s => {
250
+ // Keep streams; simple-peer usually removes track automatically or we can add logic here if needed
251
+ return true;
252
+ }));
253
+ });
254
+
255
+ } catch (error) {
256
+ console.error('Media Error:', error);
257
+ if (isMounted.current) {
258
+ alert('Could not access camera/microphone');
259
+ navigate('/dashboard');
260
+ }
261
+ }
262
+ };
263
+
264
+ const createPeer = (userToSignal, callerID, stream) => {
265
+ const peer = new Peer({
266
+ initiator: true,
267
+ trickle: false,
268
+ stream,
269
+ });
270
+
271
+ peer.on('signal', signal => {
272
+ socketRef.current.emit('offer', signal, roomId);
273
+ });
274
+
275
+ peer.on('stream', remoteStream => {
276
+ handleRemoteStream(remoteStream, userToSignal);
277
+ });
278
+
279
+ return peer;
280
+ };
281
+
282
+ const addPeer = (incomingSignal, callerID, stream) => {
283
+ const peer = new Peer({
284
+ initiator: false,
285
+ trickle: false,
286
+ stream,
287
+ });
288
+
289
+ peer.on('signal', signal => {
290
+ socketRef.current.emit('answer', signal, roomId);
291
+ });
292
+
293
+ peer.on('stream', remoteStream => {
294
+ handleRemoteStream(remoteStream, callerID);
295
+ });
296
+
297
+ peer.signal(incomingSignal);
298
+ return peer;
299
+ };
300
+
301
+ const handleRemoteStream = (stream, peerID) => {
302
+ setRemoteStreams(prev => {
303
+ if (prev.some(s => s.id === stream.id)) return prev;
304
+ return [...prev, { id: stream.id, stream, peerID }];
305
+ });
306
+ };
307
+
308
+ // --- Controls ---
309
+
310
+ const toggleMute = () => {
311
+ if (userStreamRef.current) {
312
+ const track = userStreamRef.current.getAudioTracks()[0];
313
+ if (track) {
314
+ track.enabled = !track.enabled;
315
+ setIsMuted(!track.enabled);
316
+ }
317
+ }
318
+ };
319
+
320
+ const toggleVideo = () => {
321
+ if (userStreamRef.current) {
322
+ const track = userStreamRef.current.getVideoTracks()[0];
323
+ if (track) {
324
+ track.enabled = !track.enabled;
325
+ setIsVideoOff(!track.enabled);
326
+ }
327
+ }
328
+ };
329
+
330
+ const startScreenShare = async () => {
331
+ try {
332
+ const stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
333
+ screenStreamRef.current = stream;
334
+ setScreenStream(stream);
335
+ setIsScreenSharing(true);
336
+ setPinnedStreamId('local-screen'); // Auto-pin my screen
337
+
338
+ // Add stream to all peers
339
+ peersRef.current.forEach(({ peer }) => {
340
+ peer.addStream(stream);
341
+ });
342
+
343
+ // Handle native stop button
344
+ stream.getVideoTracks()[0].onended = () => stopScreenShare();
345
+
346
+ socketRef.current.emit('screen-share-started', roomId);
347
+ } catch (error) {
348
+ console.error("Screen share failed", error);
349
+ }
350
+ };
351
+
352
+ const stopScreenShare = () => {
353
+ if (screenStreamRef.current) {
354
+ // Remove stream from peers
355
+ peersRef.current.forEach(({ peer }) => {
356
+ peer.removeStream(screenStreamRef.current);
357
+ });
358
+ // Stop tracks
359
+ screenStreamRef.current.getTracks().forEach(t => t.stop());
360
+ screenStreamRef.current = null;
361
+ }
362
+ setScreenStream(null);
363
+ setIsScreenSharing(false);
364
+
365
+ // Unpin if necessary
366
+ setPinnedStreamId(prev => (prev === 'local-screen' ? null : prev));
367
+
368
+ socketRef.current.emit('screen-share-stopped', roomId);
369
+ };
370
+
371
+ const leaveRoom = () => {
372
+ cleanupConnection();
373
+ navigate('/dashboard');
374
+ };
375
+
376
+ const copyLink = () => {
377
+ const link = `${window.location.origin}/room/${roomId}`;
378
+ navigator.clipboard.writeText(link);
379
+ setShowCopied(true);
380
+ setTimeout(() => setShowCopied(false), 2000);
381
+ };
382
+
383
+ // --- Render Helpers ---
384
+
385
+ const getAllStreams = () => {
386
+ const streams = [];
387
+
388
+ // 1. Local Camera
389
+ if (userStream) {
390
+ streams.push({ id: 'local-cam', stream: userStream, isLocal: true, label: 'You' });
391
+ }
392
+
393
+ // 2. Local Screen
394
+ if (screenStream) {
395
+ streams.push({ id: 'local-screen', stream: screenStream, isLocal: true, label: 'Your Screen' });
396
+ }
397
+
398
+ // 3. Remote Streams
399
+ remoteStreams.forEach(rs => {
400
+ streams.push({
401
+ id: rs.id,
402
+ stream: rs.stream,
403
+ isLocal: false,
404
+ label: `Participant ${rs.peerID.substr(0,4)}`
405
+ });
406
+ });
407
+
408
+ return streams;
409
+ };
410
+
411
+ const allStreams = getAllStreams();
412
+ const pinnedStream = allStreams.find(s => s.id === pinnedStreamId);
413
+
414
+ return (
415
+ <div className="flex flex-col h-screen bg-black overflow-hidden">
416
+
417
+ {/* Header */}
418
+ <div className="h-16 border-b border-gray-800 bg-[#0a0a0f] flex items-center justify-between px-6 z-20">
419
+ <div className="flex items-center gap-3">
420
+ <img src="/logo.png" alt="Logo" className="h-8 w-8" onError={(e) => e.target.style.display='none'} />
421
+ <div>
422
+ <h1 className="text-white font-semibold text-sm">{roomInfo?.name || 'Meeting Room'}</h1>
423
+ <span className="text-gray-500 text-xs">{roomId}</span>
424
+ </div>
425
+ </div>
426
+ <button
427
+ onClick={copyLink}
428
+ className="flex items-center gap-2 bg-[#222] hover:bg-[#333] text-white text-xs px-3 py-1.5 rounded-full transition-colors"
429
+ >
430
+ <CopyIcon className="w-3 h-3" />
431
+ {showCopied ? "Copied" : "Copy Link"}
432
+ </button>
433
+ </div>
434
+
435
+ {/* Main Content Area */}
436
+ <div className="flex-1 flex overflow-hidden relative">
437
+
438
+ {pinnedStream ? (
439
+ // Pinned Layout
440
+ <div className="flex-1 flex flex-col md:flex-row w-full h-full p-4 gap-4">
441
+
442
+ {/* Main Stage */}
443
+ <div className="flex-1 relative bg-[#111] rounded-2xl overflow-hidden border border-gray-800 shadow-2xl">
444
+ <VideoPlayer
445
+ stream={pinnedStream.stream}
446
+ isLocal={pinnedStream.isLocal}
447
+ label={pinnedStream.label}
448
+ isPinned={true}
449
+ onPin={() => setPinnedStreamId(null)} // Unpin
450
+ />
451
+ </div>
452
+
453
+ {/* Side Filmstrip */}
454
+ <div className="md:w-64 w-full flex md:flex-col gap-3 overflow-x-auto md:overflow-y-auto custom-scrollbar">
455
+ {allStreams.filter(s => s.id !== pinnedStreamId).map(s => (
456
+ <div key={s.id} className="flex-shrink-0 w-48 md:w-full aspect-video">
457
+ <VideoPlayer
458
+ stream={s.stream}
459
+ isLocal={s.isLocal}
460
+ label={s.label}
461
+ isPinned={false}
462
+ onPin={() => setPinnedStreamId(s.id)}
463
+ />
464
+ </div>
465
+ ))}
466
+ </div>
467
+ </div>
468
+ ) : (
469
+ // Grid Layout
470
+ <div className="flex-1 p-4 overflow-y-auto custom-scrollbar">
471
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 auto-rows-fr h-full content-center">
472
+ {allStreams.map(s => (
473
+ <div key={s.id} className="aspect-video w-full h-full max-h-[400px]">
474
+ <VideoPlayer
475
+ stream={s.stream}
476
+ isLocal={s.isLocal}
477
+ label={s.label}
478
+ isPinned={false}
479
+ onPin={() => setPinnedStreamId(s.id)}
480
+ />
481
+ </div>
482
+ ))}
483
+ {allStreams.length === 0 && (
484
+ <div className="col-span-full flex items-center justify-center text-gray-500">
485
+ Waiting for others...
486
+ </div>
487
+ )}
488
+ </div>
489
+ </div>
490
+ )}
491
+ </div>
492
+
493
+ {/* Footer Controls */}
494
+ <div className="h-20 bg-[#0a0a0f] border-t border-gray-800 flex items-center justify-center gap-4 z-20">
495
+ <button
496
+ onClick={toggleMute}
497
+ className={`p-3 rounded-full transition-all ${isMuted ? 'bg-red-500 hover:bg-red-600' : 'bg-[#222] hover:bg-[#333]'}`}
498
+ title={isMuted ? "Unmute" : "Mute"}
499
+ >
500
+ {isMuted ? <MicOffIcon className="w-5 h-5 text-white" /> : <MicIcon className="w-5 h-5 text-white" />}
501
+ </button>
502
+
503
+ <button
504
+ onClick={toggleVideo}
505
+ className={`p-3 rounded-full transition-all ${isVideoOff ? 'bg-red-500 hover:bg-red-600' : 'bg-[#222] hover:bg-[#333]'}`}
506
+ title={isVideoOff ? "Turn On Camera" : "Turn Off Camera"}
507
+ >
508
+ {isVideoOff ? <VideoOffIcon className="w-5 h-5 text-white" /> : <VideoIcon className="w-5 h-5 text-white" />}
509
+ </button>
510
+
511
+ <button
512
+ onClick={isScreenSharing ? stopScreenShare : startScreenShare}
513
+ className={`p-3 rounded-full transition-all ${isScreenSharing ? 'bg-green-600 hover:bg-green-700' : 'bg-[#222] hover:bg-[#333]'}`}
514
+ title="Share Screen"
515
+ >
516
+ <MonitorIcon className="w-5 h-5 text-white" />
517
+ </button>
518
+
519
+ <div className="w-px h-8 bg-gray-700 mx-2"></div>
520
+
521
+ <button
522
+ onClick={leaveRoom}
523
+ className="px-6 py-3 bg-red-600 hover:bg-red-700 text-white font-medium rounded-full flex items-center gap-2 transition-all shadow-lg shadow-red-900/20"
524
+ >
525
+ <PhoneOffIcon className="w-5 h-5" />
526
+ <span>Leave</span>
527
+ </button>
528
+ </div>
529
+
530
+ </div>
531
+ );
532
+ };
533
+
534
+ export default VideoRoom;
frontend/src/context/AuthContext.jsx ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { createContext, useState, useEffect, useContext } from 'react';
2
+ import { authAPI } from '../utils/api';
3
+
4
+ const AuthContext = createContext();
5
+
6
+ export const useAuth = () => {
7
+ const context = useContext(AuthContext);
8
+ if (!context) {
9
+ throw new Error('useAuth must be used within AuthProvider');
10
+ }
11
+ return context;
12
+ };
13
+
14
+ export const AuthProvider = ({ children }) => {
15
+ const [user, setUser] = useState(null);
16
+ const [loading, setLoading] = useState(true);
17
+
18
+ useEffect(() => {
19
+ checkAuth();
20
+ }, []);
21
+
22
+ const checkAuth = async () => {
23
+ try {
24
+ console.log('🔍 Checking authentication status...');
25
+ const response = await authAPI.getCurrentUser();
26
+
27
+ if (response.data && response.data.user) {
28
+ const userData = response.data.user;
29
+ const formattedUser = {
30
+ ...userData,
31
+ username: userData.username || userData.name || 'User',
32
+ avatar: userData.avatar || ''
33
+ };
34
+
35
+ console.log('✅ User authenticated:', formattedUser.email);
36
+ setUser(formattedUser);
37
+ setLoading(false);
38
+ return true; // Return true if authenticated
39
+ } else {
40
+ console.log('❌ No user data in response');
41
+ setUser(null);
42
+ setLoading(false);
43
+ return false;
44
+ }
45
+ } catch (error) {
46
+ console.error('❌ Authentication check failed:', error.response?.status, error.message);
47
+ setUser(null);
48
+ setLoading(false);
49
+ return false; // Return false if not authenticated
50
+ }
51
+ };
52
+
53
+ const login = async (credentials) => {
54
+ try {
55
+ console.log('🔐 Logging in with email/password...');
56
+ const response = await authAPI.login(credentials);
57
+ const userData = response.data.user;
58
+ const formattedUser = {
59
+ ...userData,
60
+ username: userData.username || userData.name || 'User',
61
+ avatar: userData.avatar || ''
62
+ };
63
+
64
+ console.log('✅ Login successful:', formattedUser.email);
65
+ setUser(formattedUser);
66
+ return response.data;
67
+ } catch (error) {
68
+ console.error('❌ Login failed:', error.response?.data?.message || error.message);
69
+ throw error;
70
+ }
71
+ };
72
+
73
+ const register = async (userData) => {
74
+ try {
75
+ console.log('📝 Registering new user...');
76
+ const response = await authAPI.register(userData);
77
+ const user = response.data.user;
78
+ const formattedUser = {
79
+ ...user,
80
+ username: user.username || user.name || 'User',
81
+ avatar: user.avatar || ''
82
+ };
83
+
84
+ console.log('✅ Registration successful:', formattedUser.email);
85
+ setUser(formattedUser);
86
+ return response.data;
87
+ } catch (error) {
88
+ console.error('❌ Registration failed:', error.response?.data?.message || error.message);
89
+ throw error;
90
+ }
91
+ };
92
+
93
+ const logout = async () => {
94
+ try {
95
+ console.log('👋 Logging out...');
96
+ await authAPI.logout();
97
+ setUser(null);
98
+ console.log('✅ Logout successful');
99
+ } catch (error) {
100
+ console.error('❌ Logout failed:', error);
101
+ // Still clear user even if API call fails
102
+ setUser(null);
103
+ }
104
+ };
105
+
106
+ const googleLogin = () => {
107
+ console.log('🔗 Redirecting to Google OAuth...');
108
+ authAPI.googleLogin();
109
+ };
110
+
111
+ const value = {
112
+ user,
113
+ loading,
114
+ login,
115
+ register,
116
+ logout,
117
+ googleLogin,
118
+ checkAuth,
119
+ };
120
+
121
+ return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
122
+ };
frontend/src/index.css ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ * {
6
+ margin: 0;
7
+ padding: 0;
8
+ box-sizing: border-box;
9
+ }
10
+
11
+ body {
12
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
13
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
14
+ sans-serif;
15
+ -webkit-font-smoothing: antialiased;
16
+ -moz-osx-font-smoothing: grayscale;
17
+ background-color: #0a0a0f;
18
+ color: white;
19
+ }
20
+
21
+ code {
22
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
23
+ monospace;
24
+ }
25
+
26
+ /* --- Glassy Scrollbar Effect --- */
27
+ ::-webkit-scrollbar {
28
+ width: 12px; /* Slightly wider to show the glass effect */
29
+ }
30
+
31
+ ::-webkit-scrollbar-track {
32
+ background: transparent; /* Clear background */
33
+ }
34
+
35
+ ::-webkit-scrollbar-thumb {
36
+ /* Glassy look: Low opacity white + subtle border */
37
+ background-color: rgba(255, 255, 255, 0.1);
38
+ border: 1px solid rgba(255, 255, 255, 0.2);
39
+ border-radius: 100vh; /* Pill shape */
40
+ background-clip: padding-box;
41
+ }
42
+
43
+ ::-webkit-scrollbar-thumb:hover {
44
+ /* Light catches the glass on hover */
45
+ background-color: rgba(255, 255, 255, 0.2);
46
+ border: 1px solid rgba(255, 255, 255, 0.3);
47
+ }
frontend/src/index.js ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import './index.css';
4
+ import App from './App';
5
+
6
+ const root = ReactDOM.createRoot(document.getElementById('root'));
7
+ root.render(
8
+ <React.StrictMode>
9
+ <App />
10
+ </React.StrictMode>
11
+ );
frontend/src/utils/api.js ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios from 'axios';
2
+
3
+ // 1. Base URL set to relative '/api'
4
+ // This means if your site is at https://myspace.hf.space,
5
+ // requests automatically go to https://myspace.hf.space/api
6
+ const api = axios.create({
7
+ baseURL: '/api',
8
+ withCredentials: true,
9
+ headers: {
10
+ 'Content-Type': 'application/json',
11
+ },
12
+ });
13
+
14
+ export const authAPI = {
15
+ register: (data) => api.post('/auth/register', data),
16
+ login: (data) => api.post('/auth/login', data),
17
+ logout: () => api.post('/auth/logout'),
18
+ getCurrentUser: () => api.get('/auth/me'),
19
+ googleLogin: () => {
20
+ // 2. Google Login set to relative path
21
+ // Redirects to /api/auth/google on the current domain
22
+ window.location.href = '/api/auth/google';
23
+ },
24
+ };
25
+
26
+ export const roomAPI = {
27
+ createRoom: (data) => api.post('/rooms/create', data),
28
+ getRoom: (roomId) => api.get(`/rooms/${roomId}`),
29
+ joinRoom: (roomId) => api.post(`/rooms/${roomId}/join`),
30
+ endRoom: (roomId) => api.post(`/rooms/${roomId}/end`),
31
+ getUserRooms: () => api.get('/rooms/user/rooms'),
32
+ };
33
+
34
+ export default api;
frontend/tailwind.config.js ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ module.exports = {
3
+ content: [
4
+ "./src/**/*.{js,jsx,ts,tsx}",
5
+ "./public/index.html"
6
+ ],
7
+ theme: {
8
+ extend: {
9
+ colors: {
10
+ primary: {
11
+ 50: '#f0f0ff',
12
+ 100: '#e5e5ff',
13
+ 200: '#d0d0ff',
14
+ 300: '#b0b0ff',
15
+ 400: '#8080ff',
16
+ 500: '#6366f1',
17
+ 600: '#4f46e5',
18
+ 700: '#4338ca',
19
+ 800: '#3730a3',
20
+ 900: '#312e81',
21
+ },
22
+ },
23
+ backgroundImage: {
24
+ 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
25
+ },
26
+ animation: {
27
+ 'pulse-slow': 'pulse 4s cubic-bezier(0.4, 0, 0.6, 1) infinite',
28
+ },
29
+ },
30
+ },
31
+ plugins: [],
32
+ }