@intranefr/superbackend 1.7.7 → 1.7.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.beads/.br_history/issues.20260314_212352_900045509.jsonl +0 -0
- package/.beads/.br_history/issues.20260314_212352_900045509.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_212353_087140743.jsonl +1 -0
- package/.beads/.br_history/issues.20260314_212353_087140743.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_212353_285881504.jsonl +2 -0
- package/.beads/.br_history/issues.20260314_212353_285881504.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_212353_473915419.jsonl +3 -0
- package/.beads/.br_history/issues.20260314_212353_473915419.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_212353_659476307.jsonl +4 -0
- package/.beads/.br_history/issues.20260314_212353_659476307.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_212353_869998925.jsonl +5 -0
- package/.beads/.br_history/issues.20260314_212353_869998925.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_212354_054785029.jsonl +6 -0
- package/.beads/.br_history/issues.20260314_212354_054785029.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_213336_175893691.jsonl +7 -0
- package/.beads/.br_history/issues.20260314_213336_175893691.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_213336_338509797.jsonl +7 -0
- package/.beads/.br_history/issues.20260314_213336_338509797.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_213336_515443192.jsonl +7 -0
- package/.beads/.br_history/issues.20260314_213336_515443192.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_213336_676417592.jsonl +7 -0
- package/.beads/.br_history/issues.20260314_213336_676417592.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_213336_839182422.jsonl +7 -0
- package/.beads/.br_history/issues.20260314_213336_839182422.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_213337_004349113.jsonl +7 -0
- package/.beads/.br_history/issues.20260314_213337_004349113.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_213337_179824080.jsonl +7 -0
- package/.beads/.br_history/issues.20260314_213337_179824080.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_213701_705075332.jsonl +7 -0
- package/.beads/.br_history/issues.20260314_213701_705075332.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_213706_783128702.jsonl +8 -0
- package/.beads/.br_history/issues.20260314_213706_783128702.jsonl.meta.json +1 -0
- package/.beads/config.yaml +4 -0
- package/.beads/issues.jsonl +8 -0
- package/.beads/metadata.json +4 -0
- package/.env.example +8 -0
- package/autochangelog/.env.example +36 -0
- package/autochangelog/README.md +412 -0
- package/autochangelog/config/database.js +27 -0
- package/autochangelog/package.json +47 -0
- package/autochangelog/public/landing.html +581 -0
- package/autochangelog/server.js +104 -0
- package/autochangelog/src/app.js +181 -0
- package/autochangelog/src/config/database.js +26 -0
- package/autochangelog/src/controllers/auth.js +488 -0
- package/autochangelog/src/controllers/changelog.js +682 -0
- package/autochangelog/src/controllers/project.js +580 -0
- package/autochangelog/src/controllers/repository.js +780 -0
- package/autochangelog/src/middleware/auth.js +386 -0
- package/autochangelog/src/models/Changelog.js +443 -0
- package/autochangelog/src/models/Project.js +226 -0
- package/autochangelog/src/models/Repository.js +366 -0
- package/autochangelog/src/models/User.js +223 -0
- package/autochangelog/src/routes/auth.routes.js +32 -0
- package/autochangelog/src/routes/changelog.routes.js +42 -0
- package/autochangelog/src/routes/github-auth.routes.js +102 -0
- package/autochangelog/src/routes/project.routes.js +50 -0
- package/autochangelog/src/routes/repository.routes.js +54 -0
- package/autochangelog/src/services/changelog.js +722 -0
- package/autochangelog/src/services/github.js +243 -0
- package/autochangelog/utils/logger.js +77 -0
- package/autochangelog/views/404.ejs +18 -0
- package/autochangelog/views/dashboard.ejs +596 -0
- package/autochangelog/views/index.ejs +231 -0
- package/autochangelog/views/layouts/main.ejs +44 -0
- package/autochangelog/views/login.ejs +104 -0
- package/autochangelog/views/partials/footer.ejs +20 -0
- package/autochangelog/views/partials/navbar.ejs +51 -0
- package/autochangelog/views/register.ejs +109 -0
- package/autochangelog-cli/README.md +266 -0
- package/autochangelog-cli/bin/autochangelog +120 -0
- package/autochangelog-cli/package.json +46 -0
- package/autochangelog-cli/src/cli/commands/auth.js +291 -0
- package/autochangelog-cli/src/cli/commands/changelog.js +619 -0
- package/autochangelog-cli/src/cli/commands/project.js +427 -0
- package/autochangelog-cli/src/cli/commands/repo.js +557 -0
- package/autochangelog-cli/src/cli/commands/stats.js +706 -0
- package/autochangelog-cli/src/cli/utils/config.js +277 -0
- package/autochangelog-cli/src/cli/utils/errors.js +307 -0
- package/autochangelog-cli/src/cli/utils/logger.js +75 -0
- package/autochangelog-cli/src/cli/utils/output.js +357 -0
- package/package.json +8 -3
- package/plugins/supercli/README.md +108 -0
- package/plugins/supercli/plugin.json +123 -0
- package/server.js +1 -1
- package/src/cli/api.js +380 -0
- package/src/cli/direct/agent-utils.js +61 -0
- package/src/cli/direct/cli-utils.js +112 -0
- package/src/cli/direct/data-seeding.js +307 -0
- package/src/cli/direct/db-admin.js +84 -0
- package/src/cli/direct/db-advanced.js +372 -0
- package/src/cli/direct/db-utils.js +558 -0
- package/src/cli/direct/help.js +195 -0
- package/src/cli/direct/migration.js +107 -0
- package/src/cli/direct/rbac-advanced.js +132 -0
- package/src/cli/direct/resources-additional.js +400 -0
- package/src/cli/direct/resources-cms-advanced.js +173 -0
- package/src/cli/direct/resources-cms.js +247 -0
- package/src/cli/direct/resources-core.js +253 -0
- package/src/cli/direct/resources-execution.js +367 -0
- package/src/cli/direct/resources-health.js +152 -0
- package/src/cli/direct/resources-integrations.js +182 -0
- package/src/cli/direct/resources-logs.js +204 -0
- package/src/cli/direct/resources-org-rbac.js +187 -0
- package/src/cli/direct/resources-system.js +236 -0
- package/src/cli/direct.js +556 -0
- package/src/controllers/admin.controller.js +4 -0
- package/src/controllers/auth.controller.js +148 -1
- package/src/controllers/waitingList.controller.js +130 -1
- package/src/models/RbacRole.js +1 -1
- package/src/models/User.js +39 -5
- package/src/routes/auth.routes.js +6 -0
- package/src/routes/waitingList.routes.js +12 -2
- package/src/routes/waitingListAdmin.routes.js +3 -0
- package/src/services/email.service.js +1 -0
- package/src/services/github.service.js +255 -0
- package/src/services/rateLimiter.service.js +29 -1
- package/src/services/waitingListJson.service.js +32 -3
- package/views/admin-waiting-list.ejs +386 -3
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
const express = require("express");
|
|
2
|
+
const cors = require("cors");
|
|
3
|
+
const helmet = require("helmet");
|
|
4
|
+
const morgan = require("morgan");
|
|
5
|
+
const rateLimit = require("express-rate-limit");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const logger = require("../utils/logger");
|
|
8
|
+
|
|
9
|
+
// Import autochangelog-specific routes
|
|
10
|
+
const projectRoutes = require("./routes/project.routes");
|
|
11
|
+
const repositoryRoutes = require("./routes/repository.routes");
|
|
12
|
+
const changelogRoutes = require("./routes/changelog.routes");
|
|
13
|
+
const githubAuthRoutes = require("./routes/github-auth.routes"); // GitHub OAuth routes
|
|
14
|
+
|
|
15
|
+
const app = express();
|
|
16
|
+
|
|
17
|
+
// Security middleware
|
|
18
|
+
app.use(
|
|
19
|
+
helmet({
|
|
20
|
+
crossOriginEmbedderPolicy: false,
|
|
21
|
+
contentSecurityPolicy: {
|
|
22
|
+
directives: {
|
|
23
|
+
defaultSrc: ["'self'"],
|
|
24
|
+
styleSrc: [
|
|
25
|
+
"'self'",
|
|
26
|
+
"'unsafe-inline'",
|
|
27
|
+
"https://cdn.tailwindcss.com",
|
|
28
|
+
"https://cdn.jsdelivr.net",
|
|
29
|
+
],
|
|
30
|
+
scriptSrc: [
|
|
31
|
+
"'self'",
|
|
32
|
+
"'unsafe-inline'",
|
|
33
|
+
"'unsafe-eval'",
|
|
34
|
+
"https://cdn.tailwindcss.com",
|
|
35
|
+
"https://unpkg.com",
|
|
36
|
+
"https://cdn.jsdelivr.net",
|
|
37
|
+
],
|
|
38
|
+
imgSrc: ["'self'", "data:", "https:", "http:"],
|
|
39
|
+
connectSrc: ["'self'", "https://api.github.com"],
|
|
40
|
+
fontSrc: [
|
|
41
|
+
"'self'",
|
|
42
|
+
"https://fonts.gstatic.com",
|
|
43
|
+
"https://cdn.jsdelivr.net",
|
|
44
|
+
],
|
|
45
|
+
objectSrc: ["'none'"],
|
|
46
|
+
mediaSrc: ["'self'"],
|
|
47
|
+
frameSrc: ["'none'"],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// CORS configuration
|
|
54
|
+
app.use(
|
|
55
|
+
cors({
|
|
56
|
+
origin: process.env.FRONTEND_URL || "http://localhost:3000",
|
|
57
|
+
credentials: true,
|
|
58
|
+
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
59
|
+
allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With"],
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Rate limiting
|
|
64
|
+
const globalRateLimit = rateLimit({
|
|
65
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
66
|
+
max: 1000, // limit each IP to 1000 requests per windowMs
|
|
67
|
+
message: {
|
|
68
|
+
success: false,
|
|
69
|
+
message: "Too many requests from this IP, please try again later.",
|
|
70
|
+
},
|
|
71
|
+
standardHeaders: true,
|
|
72
|
+
legacyHeaders: false,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
app.use(globalRateLimit);
|
|
76
|
+
|
|
77
|
+
// Body parsing middleware
|
|
78
|
+
app.use(express.json({ limit: "10mb" }));
|
|
79
|
+
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
|
|
80
|
+
|
|
81
|
+
// Static files
|
|
82
|
+
app.use(express.static(path.join(__dirname, "../public")));
|
|
83
|
+
|
|
84
|
+
// View engine setup
|
|
85
|
+
app.set("view engine", "ejs");
|
|
86
|
+
app.set("views", path.join(__dirname, "../views"));
|
|
87
|
+
|
|
88
|
+
// Logging middleware
|
|
89
|
+
if (process.env.NODE_ENV === "development") {
|
|
90
|
+
app.use(morgan("dev"));
|
|
91
|
+
} else {
|
|
92
|
+
app.use(
|
|
93
|
+
morgan("combined", {
|
|
94
|
+
stream: {
|
|
95
|
+
write: (message) => logger.info(message.trim()),
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Request logging middleware
|
|
102
|
+
app.use((req, res, next) => {
|
|
103
|
+
logger.info(`${req.method} ${req.path} - ${req.ip}`);
|
|
104
|
+
next();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Health check endpoint
|
|
108
|
+
app.get("/health", (req, res) => {
|
|
109
|
+
res.json({
|
|
110
|
+
success: true,
|
|
111
|
+
message: "AutoChangelog API is running",
|
|
112
|
+
timestamp: new Date().toISOString(),
|
|
113
|
+
uptime: process.uptime(),
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Landing page route
|
|
118
|
+
app.get("/", (req, res) => {
|
|
119
|
+
res.render("index", {
|
|
120
|
+
title: "AutoChangelog - Automated Changelog Generation",
|
|
121
|
+
user: req.user || null,
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Auth pages (EJS views - API calls go to /api/auth/* provided by SuperBackend)
|
|
126
|
+
app.get("/auth/login", (req, res) => {
|
|
127
|
+
res.render("login", { title: "Login - AutoChangelog" });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
app.get("/auth/register", (req, res) => {
|
|
131
|
+
res.render("register", { title: "Create Account - AutoChangelog" });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
app.get("/auth/logout", (req, res) => {
|
|
135
|
+
res.redirect("/auth/login");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Dashboard route
|
|
139
|
+
app.get("/dashboard", (req, res) => {
|
|
140
|
+
res.render("dashboard", {
|
|
141
|
+
title: "Dashboard - AutoChangelog",
|
|
142
|
+
user: req.user || null,
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// AutoChangelog-specific API routes
|
|
147
|
+
// Note: SuperBackend provides /api/auth/*, /api/users/*, /api/orgs/*, /api/billing/*
|
|
148
|
+
// SuperBackend middleware is mounted in server.js AFTER this app is built,
|
|
149
|
+
// so we must NOT have a catch-all 404 here — it's registered in server.js instead
|
|
150
|
+
app.use("/api/projects", projectRoutes);
|
|
151
|
+
app.use("/api/repositories", repositoryRoutes);
|
|
152
|
+
app.use("/api/changelogs", changelogRoutes);
|
|
153
|
+
app.use("/auth", githubAuthRoutes); // GitHub OAuth: /auth/github and /auth/github/callback
|
|
154
|
+
|
|
155
|
+
// GitHub webhook endpoint (public)
|
|
156
|
+
app.post("/api/webhooks/github", (req, res) => {
|
|
157
|
+
logger.info("GitHub webhook received");
|
|
158
|
+
res.status(200).json({ success: true, message: "Webhook received" });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Global error handler
|
|
162
|
+
app.use((err, req, res, next) => {
|
|
163
|
+
logger.error("Unhandled error:", err);
|
|
164
|
+
|
|
165
|
+
const errorResponse = {
|
|
166
|
+
success: false,
|
|
167
|
+
message:
|
|
168
|
+
process.env.NODE_ENV === "production"
|
|
169
|
+
? "Internal server error"
|
|
170
|
+
: err.message,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
if (process.env.NODE_ENV !== "production") {
|
|
174
|
+
errorResponse.stack = err.stack;
|
|
175
|
+
errorResponse.details = err;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
res.status(err.status || 500).json(errorResponse);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
module.exports = app;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const mongoose = require("mongoose");
|
|
2
|
+
const logger = require("../../utils/logger");
|
|
3
|
+
|
|
4
|
+
const connectDB = async () => {
|
|
5
|
+
try {
|
|
6
|
+
const conn = await mongoose.connect(process.env.MONGODB_URI);
|
|
7
|
+
|
|
8
|
+
logger.info(`MongoDB Connected: ${conn.connection.host}`);
|
|
9
|
+
} catch (error) {
|
|
10
|
+
logger.error("Database connection error:", error);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const disconnectDB = async () => {
|
|
16
|
+
try {
|
|
17
|
+
await mongoose.disconnect();
|
|
18
|
+
logger.info("MongoDB disconnected");
|
|
19
|
+
} catch (error) {
|
|
20
|
+
logger.error("Error disconnecting from MongoDB:", error);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
module.exports = connectDB;
|
|
25
|
+
module.exports.connectDB = connectDB;
|
|
26
|
+
module.exports.disconnectDB = disconnectDB;
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
const bcrypt = require('bcryptjs');
|
|
2
|
+
const jwt = require('jsonwebtoken');
|
|
3
|
+
const User = require('../models/User');
|
|
4
|
+
const GitHubService = require('../services/github');
|
|
5
|
+
const logger = require('../../utils/logger');
|
|
6
|
+
|
|
7
|
+
// Register a new user
|
|
8
|
+
const register = async (req, res) => {
|
|
9
|
+
try {
|
|
10
|
+
const { email, password, name } = req.body;
|
|
11
|
+
|
|
12
|
+
// Validation
|
|
13
|
+
if (!email || !password) {
|
|
14
|
+
return res.status(400).json({
|
|
15
|
+
success: false,
|
|
16
|
+
message: 'Email and password are required.',
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (password.length < 6) {
|
|
21
|
+
return res.status(400).json({
|
|
22
|
+
success: false,
|
|
23
|
+
message: 'Password must be at least 6 characters long.',
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Check if user already exists
|
|
28
|
+
const existingUser = await User.findOne({ email: email.toLowerCase() });
|
|
29
|
+
if (existingUser) {
|
|
30
|
+
return res.status(400).json({
|
|
31
|
+
success: false,
|
|
32
|
+
message: 'User with this email already exists.',
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Create new user
|
|
37
|
+
const user = new User({
|
|
38
|
+
email: email.toLowerCase(),
|
|
39
|
+
password,
|
|
40
|
+
name,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
await user.save();
|
|
44
|
+
|
|
45
|
+
// Generate JWT token
|
|
46
|
+
const token = jwt.sign(
|
|
47
|
+
{ userId: user._id },
|
|
48
|
+
process.env.JWT_SECRET,
|
|
49
|
+
{ expiresIn: process.env.JWT_EXPIRES_IN || '7d' }
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
logger.info(`User registered successfully: ${user.email}`);
|
|
53
|
+
|
|
54
|
+
res.status(201).json({
|
|
55
|
+
success: true,
|
|
56
|
+
message: 'User registered successfully.',
|
|
57
|
+
data: {
|
|
58
|
+
user: user.toJSON(),
|
|
59
|
+
token,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
} catch (error) {
|
|
63
|
+
logger.error('Registration error:', error);
|
|
64
|
+
res.status(500).json({
|
|
65
|
+
success: false,
|
|
66
|
+
message: 'Internal server error.',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Login user
|
|
72
|
+
const login = async (req, res) => {
|
|
73
|
+
try {
|
|
74
|
+
const { email, password } = req.body;
|
|
75
|
+
|
|
76
|
+
// Validation
|
|
77
|
+
if (!email || !password) {
|
|
78
|
+
return res.status(400).json({
|
|
79
|
+
success: false,
|
|
80
|
+
message: 'Email and password are required.',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Find user
|
|
85
|
+
const user = await User.findOne({ email: email.toLowerCase() });
|
|
86
|
+
if (!user) {
|
|
87
|
+
return res.status(401).json({
|
|
88
|
+
success: false,
|
|
89
|
+
message: 'Invalid email or password.',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check if account is active
|
|
94
|
+
if (!user.isActive) {
|
|
95
|
+
return res.status(401).json({
|
|
96
|
+
success: false,
|
|
97
|
+
message: 'Account is inactive.',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check password
|
|
102
|
+
const isPasswordValid = await user.comparePassword(password);
|
|
103
|
+
if (!isPasswordValid) {
|
|
104
|
+
// Increment login attempts
|
|
105
|
+
await user.incLoginAttempts();
|
|
106
|
+
return res.status(401).json({
|
|
107
|
+
success: false,
|
|
108
|
+
message: 'Invalid email or password.',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Reset login attempts on successful login
|
|
113
|
+
await user.resetLoginAttempts();
|
|
114
|
+
|
|
115
|
+
// Update last login
|
|
116
|
+
await user.updateOne({ lastLogin: new Date() });
|
|
117
|
+
|
|
118
|
+
// Generate JWT token
|
|
119
|
+
const token = jwt.sign(
|
|
120
|
+
{ userId: user._id },
|
|
121
|
+
process.env.JWT_SECRET,
|
|
122
|
+
{ expiresIn: process.env.JWT_EXPIRES_IN || '7d' }
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
logger.info(`User logged in successfully: ${user.email}`);
|
|
126
|
+
|
|
127
|
+
res.json({
|
|
128
|
+
success: true,
|
|
129
|
+
message: 'Login successful.',
|
|
130
|
+
data: {
|
|
131
|
+
user: user.toJSON(),
|
|
132
|
+
token,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
} catch (error) {
|
|
136
|
+
logger.error('Login error:', error);
|
|
137
|
+
res.status(500).json({
|
|
138
|
+
success: false,
|
|
139
|
+
message: 'Internal server error.',
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// GitHub OAuth login
|
|
145
|
+
const githubLogin = async (req, res) => {
|
|
146
|
+
try {
|
|
147
|
+
const authUrl = GitHubService.getAuthURL();
|
|
148
|
+
res.json({
|
|
149
|
+
success: true,
|
|
150
|
+
data: {
|
|
151
|
+
authUrl,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
} catch (error) {
|
|
155
|
+
logger.error('GitHub login error:', error);
|
|
156
|
+
res.status(500).json({
|
|
157
|
+
success: false,
|
|
158
|
+
message: 'Internal server error.',
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// GitHub OAuth callback
|
|
164
|
+
const githubCallback = async (req, res) => {
|
|
165
|
+
try {
|
|
166
|
+
const { code, state } = req.query;
|
|
167
|
+
|
|
168
|
+
if (!code) {
|
|
169
|
+
return res.status(400).json({
|
|
170
|
+
success: false,
|
|
171
|
+
message: 'Authorization code not provided.',
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Exchange code for access token
|
|
176
|
+
const tokenResponse = await GitHubService.getAccessToken(code);
|
|
177
|
+
|
|
178
|
+
if (!tokenResponse.access_token) {
|
|
179
|
+
return res.status(400).json({
|
|
180
|
+
success: false,
|
|
181
|
+
message: 'Failed to get access token from GitHub.',
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const accessToken = tokenResponse.access_token;
|
|
186
|
+
|
|
187
|
+
// Get user information from GitHub
|
|
188
|
+
const githubUser = await GitHubService.getUser(accessToken);
|
|
189
|
+
|
|
190
|
+
// Find or create user in our database
|
|
191
|
+
let user = await User.findOne({ githubId: githubUser.id });
|
|
192
|
+
|
|
193
|
+
if (!user) {
|
|
194
|
+
// Check if user exists with email
|
|
195
|
+
user = await User.findOne({ email: githubUser.email });
|
|
196
|
+
|
|
197
|
+
if (user) {
|
|
198
|
+
// Update existing user with GitHub info
|
|
199
|
+
user.githubId = githubUser.id;
|
|
200
|
+
user.githubUsername = githubUser.login;
|
|
201
|
+
user.githubAccessToken = accessToken;
|
|
202
|
+
user.githubRefreshToken = tokenResponse.refresh_token;
|
|
203
|
+
user.githubEmail = githubUser.email;
|
|
204
|
+
user.name = user.name || githubUser.name || githubUser.login;
|
|
205
|
+
user.avatar = githubUser.avatar_url;
|
|
206
|
+
} else {
|
|
207
|
+
// Create new user
|
|
208
|
+
user = new User({
|
|
209
|
+
email: githubUser.email,
|
|
210
|
+
name: githubUser.name || githubUser.login,
|
|
211
|
+
githubId: githubUser.id,
|
|
212
|
+
githubUsername: githubUser.login,
|
|
213
|
+
githubAccessToken: accessToken,
|
|
214
|
+
githubRefreshToken: tokenResponse.refresh_token,
|
|
215
|
+
githubEmail: githubUser.email,
|
|
216
|
+
avatar: githubUser.avatar_url,
|
|
217
|
+
// Password is not required for GitHub auth, but we'll set a random one
|
|
218
|
+
password: Math.random().toString(36).slice(-8),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
await user.save();
|
|
223
|
+
} else {
|
|
224
|
+
// Update existing GitHub user
|
|
225
|
+
user.githubAccessToken = accessToken;
|
|
226
|
+
user.githubRefreshToken = tokenResponse.refresh_token;
|
|
227
|
+
user.name = user.name || githubUser.name || githubUser.login;
|
|
228
|
+
user.avatar = githubUser.avatar_url;
|
|
229
|
+
await user.save();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Generate JWT token
|
|
233
|
+
const token = jwt.sign(
|
|
234
|
+
{ userId: user._id },
|
|
235
|
+
process.env.JWT_SECRET,
|
|
236
|
+
{ expiresIn: process.env.JWT_EXPIRES_IN || '7d' }
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
logger.info(`User logged in via GitHub: ${user.email}`);
|
|
240
|
+
|
|
241
|
+
res.json({
|
|
242
|
+
success: true,
|
|
243
|
+
message: 'GitHub authentication successful.',
|
|
244
|
+
data: {
|
|
245
|
+
user: user.toJSON(),
|
|
246
|
+
token,
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
} catch (error) {
|
|
250
|
+
logger.error('GitHub callback error:', error);
|
|
251
|
+
res.status(500).json({
|
|
252
|
+
success: false,
|
|
253
|
+
message: 'Internal server error.',
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// Get current user profile
|
|
259
|
+
const getProfile = async (req, res) => {
|
|
260
|
+
try {
|
|
261
|
+
const user = await User.findById(req.user._id).select('-password -githubAccessToken -githubRefreshToken');
|
|
262
|
+
|
|
263
|
+
if (!user) {
|
|
264
|
+
return res.status(404).json({
|
|
265
|
+
success: false,
|
|
266
|
+
message: 'User not found.',
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
res.json({
|
|
271
|
+
success: true,
|
|
272
|
+
data: {
|
|
273
|
+
user: user.toJSON(),
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
} catch (error) {
|
|
277
|
+
logger.error('Get profile error:', error);
|
|
278
|
+
res.status(500).json({
|
|
279
|
+
success: false,
|
|
280
|
+
message: 'Internal server error.',
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// Update user profile
|
|
286
|
+
const updateProfile = async (req, res) => {
|
|
287
|
+
try {
|
|
288
|
+
const { name, email, preferences } = req.body;
|
|
289
|
+
|
|
290
|
+
const user = await User.findById(req.user._id);
|
|
291
|
+
if (!user) {
|
|
292
|
+
return res.status(404).json({
|
|
293
|
+
success: false,
|
|
294
|
+
message: 'User not found.',
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Update fields
|
|
299
|
+
if (name) user.name = name;
|
|
300
|
+
if (email && email !== user.email) {
|
|
301
|
+
// Check if email is already taken
|
|
302
|
+
const existingUser = await User.findOne({ email: email.toLowerCase(), _id: { $ne: user._id } });
|
|
303
|
+
if (existingUser) {
|
|
304
|
+
return res.status(400).json({
|
|
305
|
+
success: false,
|
|
306
|
+
message: 'Email is already taken.',
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
user.email = email.toLowerCase();
|
|
310
|
+
}
|
|
311
|
+
if (preferences) {
|
|
312
|
+
user.preferences = { ...user.preferences, ...preferences };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
await user.save();
|
|
316
|
+
|
|
317
|
+
logger.info(`User profile updated: ${user.email}`);
|
|
318
|
+
|
|
319
|
+
res.json({
|
|
320
|
+
success: true,
|
|
321
|
+
message: 'Profile updated successfully.',
|
|
322
|
+
data: {
|
|
323
|
+
user: user.toJSON(),
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
} catch (error) {
|
|
327
|
+
logger.error('Update profile error:', error);
|
|
328
|
+
res.status(500).json({
|
|
329
|
+
success: false,
|
|
330
|
+
message: 'Internal server error.',
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// Change password
|
|
336
|
+
const changePassword = async (req, res) => {
|
|
337
|
+
try {
|
|
338
|
+
const { currentPassword, newPassword } = req.body;
|
|
339
|
+
|
|
340
|
+
if (!currentPassword || !newPassword) {
|
|
341
|
+
return res.status(400).json({
|
|
342
|
+
success: false,
|
|
343
|
+
message: 'Current password and new password are required.',
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (newPassword.length < 6) {
|
|
348
|
+
return res.status(400).json({
|
|
349
|
+
success: false,
|
|
350
|
+
message: 'New password must be at least 6 characters long.',
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const user = await User.findById(req.user._id);
|
|
355
|
+
if (!user) {
|
|
356
|
+
return res.status(404).json({
|
|
357
|
+
success: false,
|
|
358
|
+
message: 'User not found.',
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Check current password
|
|
363
|
+
const isCurrentPasswordValid = await user.comparePassword(currentPassword);
|
|
364
|
+
if (!isCurrentPasswordValid) {
|
|
365
|
+
return res.status(400).json({
|
|
366
|
+
success: false,
|
|
367
|
+
message: 'Current password is incorrect.',
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Update password
|
|
372
|
+
user.password = newPassword;
|
|
373
|
+
await user.save();
|
|
374
|
+
|
|
375
|
+
logger.info(`Password changed for user: ${user.email}`);
|
|
376
|
+
|
|
377
|
+
res.json({
|
|
378
|
+
success: true,
|
|
379
|
+
message: 'Password changed successfully.',
|
|
380
|
+
});
|
|
381
|
+
} catch (error) {
|
|
382
|
+
logger.error('Change password error:', error);
|
|
383
|
+
res.status(500).json({
|
|
384
|
+
success: false,
|
|
385
|
+
message: 'Internal server error.',
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// Logout (client-side token removal, server-side we can implement token blacklisting if needed)
|
|
391
|
+
const logout = async (req, res) => {
|
|
392
|
+
try {
|
|
393
|
+
// In a more sophisticated implementation, you might want to blacklist the token
|
|
394
|
+
// For now, we'll just return success and let the client handle token removal
|
|
395
|
+
|
|
396
|
+
logger.info(`User logged out: ${req.user.email}`);
|
|
397
|
+
|
|
398
|
+
res.json({
|
|
399
|
+
success: true,
|
|
400
|
+
message: 'Logout successful.',
|
|
401
|
+
});
|
|
402
|
+
} catch (error) {
|
|
403
|
+
logger.error('Logout error:', error);
|
|
404
|
+
res.status(500).json({
|
|
405
|
+
success: false,
|
|
406
|
+
message: 'Internal server error.',
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
// Refresh token
|
|
412
|
+
const refreshToken = async (req, res) => {
|
|
413
|
+
try {
|
|
414
|
+
const user = await User.findById(req.user._id);
|
|
415
|
+
if (!user || !user.isActive) {
|
|
416
|
+
return res.status(401).json({
|
|
417
|
+
success: false,
|
|
418
|
+
message: 'Invalid user or inactive account.',
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Generate new token
|
|
423
|
+
const token = jwt.sign(
|
|
424
|
+
{ userId: user._id },
|
|
425
|
+
process.env.JWT_SECRET,
|
|
426
|
+
{ expiresIn: process.env.JWT_EXPIRES_IN || '7d' }
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
res.json({
|
|
430
|
+
success: true,
|
|
431
|
+
message: 'Token refreshed successfully.',
|
|
432
|
+
data: {
|
|
433
|
+
token,
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
} catch (error) {
|
|
437
|
+
logger.error('Refresh token error:', error);
|
|
438
|
+
res.status(500).json({
|
|
439
|
+
success: false,
|
|
440
|
+
message: 'Internal server error.',
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// Delete account
|
|
446
|
+
const deleteAccount = async (req, res) => {
|
|
447
|
+
try {
|
|
448
|
+
const user = await User.findById(req.user._id);
|
|
449
|
+
if (!user) {
|
|
450
|
+
return res.status(404).json({
|
|
451
|
+
success: false,
|
|
452
|
+
message: 'User not found.',
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// TODO: Handle cleanup of user data (projects, repositories, changelogs)
|
|
457
|
+
// For now, we'll just deactivate the account
|
|
458
|
+
user.isActive = false;
|
|
459
|
+
user.email = `deleted_${Date.now()}_${user.email}`;
|
|
460
|
+
await user.save();
|
|
461
|
+
|
|
462
|
+
logger.info(`User account deactivated: ${user.email}`);
|
|
463
|
+
|
|
464
|
+
res.json({
|
|
465
|
+
success: true,
|
|
466
|
+
message: 'Account deactivated successfully.',
|
|
467
|
+
});
|
|
468
|
+
} catch (error) {
|
|
469
|
+
logger.error('Delete account error:', error);
|
|
470
|
+
res.status(500).json({
|
|
471
|
+
success: false,
|
|
472
|
+
message: 'Internal server error.',
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
module.exports = {
|
|
478
|
+
register,
|
|
479
|
+
login,
|
|
480
|
+
githubLogin,
|
|
481
|
+
githubCallback,
|
|
482
|
+
getProfile,
|
|
483
|
+
updateProfile,
|
|
484
|
+
changePassword,
|
|
485
|
+
logout,
|
|
486
|
+
refreshToken,
|
|
487
|
+
deleteAccount,
|
|
488
|
+
};
|