This commit is contained in:
2024-10-28 16:16:53 +05:00
parent 77aabed207
commit d6809ff538
30 changed files with 1250 additions and 658 deletions
+3 -1
View File
@@ -1,3 +1,5 @@
PORT=3001
MONGO_URI=mongodb://root:p62Z!ZatgY25@194.26.138.94:27017/
JWT_SECRET=yDcdWJgvlj2bJAuovYfQHTvtc3U9xQPw
JWT_SECRET=yDcdWJgvlj2bJAuovYfQHTvtc3U9xQPw
JWT_ACCESS_EXP=10m
JWT_REFRESH_EXP=7d
+18
View File
@@ -0,0 +1,18 @@
// interface ProcessEnv {
// readonly PORT: number;
// readonly MONGO_URI: string;
// readonly JWT_SECRET: string;
// readonly JWT_ACCESS_EXP: string;
// readonly JWT_REFRESH_EXP: string;
// }
declare namespace NodeJS {
interface ProcessEnv {
readonly PORT: number;
readonly MONGO_URI: string;
readonly JWT_SECRET: string;
readonly JWT_ACCESS_EXP: string;
readonly JWT_REFRESH_EXP: string;
// add more environment variables and their types here
}
}
+3
View File
@@ -10,10 +10,12 @@
},
"dependencies": {
"bcrypt": "^5.1.1",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"date-fns": "^2.30.0",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"jose": "^5.9.6",
"jsonwebtoken": "^9.0.2",
"mongoose": "^7.5.1",
"nodemailer": "^6.9.14",
@@ -21,6 +23,7 @@
},
"devDependencies": {
"@types/bcrypt": "^5.0.0",
"@types/cookie-parser": "^1.4.7",
"@types/cors": "^2.8.14",
"@types/express": "^4.17.17",
"@types/jsonwebtoken": "^9.0.2",
+19 -6
View File
@@ -2,11 +2,14 @@ import "dotenv/config";
import express, { json } from "express";
import connectDB from "./config/db.js";
import cors from "cors";
import loginRouter from "./routes/login.js";
import registrationRouter from "./routes/registration.js";
import cookieParser from "cookie-parser";
import authMiddleware from "./middlewares/auth.js";
import companiesRouter from "./routes/companies.js";
import registerRoute from "./routes/register.js";
import refreshRoute from "./routes/refresh.js";
import checkRoute from "./routes/check.js";
import loginRoute from "./routes/login.js";
import usersRouter from "./routes/users.js";
import companiesRouter from "./routes/companies.js";
import buildsRouter from "./routes/builds.js";
import actionsRouter from "./routes/actions.js";
import schedulesRouter from "./routes/schedules.js";
@@ -20,11 +23,21 @@ await connectDB();
const app = express();
const port = process.env.PORT || 3000;
app.use(
cors({
origin: (origin, cb) => {
cb(null, origin);
},
credentials: true,
})
);
app.use(json());
app.use(cors({ origin: "*" }));
app.use(cookieParser());
app.use("/login", loginRouter);
app.use("/registration", registrationRouter);
app.use("/login", loginRoute);
app.use("/check", checkRoute);
app.use("/refresh", refreshRoute);
app.use("/register", registerRoute);
app.use("/actions", actionsRouter);
app.use("/builds", buildsRouter);
app.use("/scheduled_sessions", scheduledSessionsRoute);
+10 -6
View File
@@ -1,30 +1,34 @@
import { Request, Response, NextFunction } from "express";
import jwt, { Secret } from "jsonwebtoken";
import Token from "../models/Token.js";
import User from "../models/User.js";
import { jwtVerify } from "jose";
import { createSecretKey } from "crypto";
async function authMiddleware(req: Request, res: Response, next: NextFunction) {
if (!req.headers.authorization || !req.headers.authorization.split(" ")[1]) {
return res.status(401).json({ error: 10 });
return res.status(401).json({ error: 1 });
}
const accessToken = req.headers.authorization.split(" ")[1];
try {
jwt.verify(accessToken, process.env.JWT_SECRET as Secret);
await jwtVerify(
accessToken,
createSecretKey(process.env.JWT_SECRET!, "utf8")
);
} catch (error) {
return res.status(401).json({ erorr: 20 });
return res.status(401).json({ error: 2 });
}
const foundAccessToken = await Token.findOne({ accessToken });
if (!foundAccessToken) {
return res.status(401).json({ error: 30 });
return res.status(401).json({ error: 3 });
}
const user = await User.findById(foundAccessToken.userId);
res.locals = { accessToken, user };
res.locals = { user, accessToken };
next();
}
+4
View File
@@ -11,6 +11,10 @@ const tokenSchema = new Schema(
type: String,
required: true,
},
refreshToken: {
type: String,
required: true,
},
},
{
timestamps: true,
+11 -8
View File
@@ -11,6 +11,11 @@ const userSchema = new Schema(
type: String,
required: true,
},
companyId: {
type: Schema.Types.ObjectId,
ref: "Company",
required: true,
},
role: {
type: String,
required: true,
@@ -19,14 +24,6 @@ const userSchema = new Schema(
type: String,
required: true,
},
avatar: {
type: String,
},
companyId: {
type: Schema.Types.ObjectId,
ref: "Company",
required: true,
},
buildIds: {
type: [Schema.Types.ObjectId],
ref: "Build",
@@ -39,6 +36,12 @@ const userSchema = new Schema(
}
);
userSchema.virtual("tokens", {
ref: "Token",
foreignField: "userId",
localField: "_id",
});
const User = model("User", userSchema);
export default User;
+11
View File
@@ -0,0 +1,11 @@
import { Router } from "express";
const router = Router();
router.get("/", (req, res) => {
res.json({ ok: 1 });
});
const checkRoute = router;
export default checkRoute;
+25 -19
View File
@@ -1,43 +1,49 @@
import { Router } from "express";
import jwt, { Secret } from "jsonwebtoken";
import bcrypt from "bcrypt";
import User from "../models/User.js";
import Token from "../models/Token.js";
import { decodeJwt, SignJWT } from "jose";
import { createSecretKey } from "crypto";
const router = Router();
router.post("/", async (req, res) => {
let { username, password } = req.body;
const { username, password } = req.body;
if (!username || !password) {
return res.json({
error: "You must pass the 'username' and 'password' parameters",
});
return res.json({ error: 1 });
}
username = username.toLowerCase();
const user = await User.findOne({ username });
const user = await User.findOne({ username }).lean();
if (!user) {
return res.json({ error: "A user with this name was not found" });
return res.json({ error: 2 });
}
if (!bcrypt.compareSync(password, user.password)) {
return res.json({ error: "Invalid username or password" });
if (!bcrypt.compareSync(password, user.password!)) {
return res.json({ error: 3 });
}
const accessToken = jwt.sign({ username }, process.env.JWT_SECRET as Secret, {
expiresIn: "365d",
});
const accessToken = await new SignJWT({ username })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime(process.env.JWT_ACCESS_EXP)
.sign(createSecretKey(process.env.JWT_SECRET, "utf8"));
const userId = user.id;
const refreshToken = await new SignJWT({ username })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime(process.env.JWT_REFRESH_EXP)
.sign(createSecretKey(process.env.JWT_SECRET, "utf8"));
await Token.create({ userId, accessToken });
await Token.create({ userId: user._id, accessToken, refreshToken });
res.json({ accessToken, user });
res
.cookie("refreshToken", refreshToken, {
httpOnly: true,
expires: new Date(decodeJwt(refreshToken).exp! * 1000),
})
.json({ accessToken, refreshToken, ...user });
});
const loginRouter = router;
const loginRoute = router;
export default loginRouter;
export default loginRoute;
+83
View File
@@ -0,0 +1,83 @@
import { createSecretKey } from "crypto";
import { Router } from "express";
import { jwtVerify, SignJWT } from "jose";
import Token from "../models/Token.js";
import User from "../models/User.js";
import IToken from "../types/IToken.js";
const router = Router();
router.post("/", async (req, res) => {
let _username;
if (req.cookies.refreshToken) {
try {
const {
payload: { username },
} = await jwtVerify(
req.cookies.refreshToken,
createSecretKey(process.env.JWT_SECRET!, "utf8")
);
_username = username;
} catch (error) {
return res.json({
error: `refreshToken jwtVerify - ${(error as Error).message}`,
});
}
const userWithTokens = await User.findOne({ username: _username }).populate(
{
path: "tokens",
}
);
if (!userWithTokens) {
return res.json({ error: "userWithTokens is null" });
}
if (!("tokens" in userWithTokens)) {
return res.json({ error: "tokens not found in userWithTokens" });
}
const foundRefreshToken = userWithTokens.tokens as IToken[];
if (
!foundRefreshToken.find(
(token) => token.refreshToken === req.cookies.refreshToken
)?.refreshToken
) {
console.log("refreshToken not found in DB");
return res.json({ error: "refreshToken not found in DB" });
}
const accessToken = await new SignJWT({ username: _username })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime(process.env.JWT_ACCESS_EXP)
.sign(createSecretKey(process.env.JWT_SECRET!, "utf8"));
await Token.findOneAndUpdate(
{ refreshToken: req.cookies.refreshToken },
{ accessToken }
);
return res.json({ accessToken });
}
// if (req.body.refreshToken) {
// try {
// jwtVerify(
// req.body.refreshToken,
// createSecretKey(process.env.JWT_SECRET!, "utf8")
// );
// } catch (error) {
// return res.json({ error: `refreshToken - ${(error as Error).message}` });
// }
// }
return res.json({ error: "refreshToken not found" });
});
const refreshRoute = router;
export default refreshRoute;
+22
View File
@@ -0,0 +1,22 @@
import bcrypt from "bcrypt";
import { Router } from "express";
import User from "../models/User.js";
const router = Router();
router.post("/", async (req, res) => {
const { username, password } = req.body;
try {
const passwordHash = bcrypt.hashSync(password, 12);
const result = await User.create({ username, password: passwordHash });
return res.json(result);
} catch (error) {
return res.json({ error: (error as Error).message });
}
});
const registerRoute = router;
export default registerRoute;
-40
View File
@@ -1,40 +0,0 @@
import { Router } from "express";
import jwt, { Secret } from "jsonwebtoken";
import bcrypt from "bcrypt";
import User from "../models/User.js";
import Token from "../models/Token.js";
const router = Router();
router.post("/", async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.json({ error: 1 });
}
if (await User.exists({ username })) {
return res.json({ error: 2 });
}
const passwordHash = bcrypt.hashSync(password, 12);
if (!passwordHash) {
return res.json({ error: 3 });
}
const accessToken = jwt.sign({ username }, process.env.JWT_SECRET as Secret, {
expiresIn: "365d",
});
const user = await User.create({ username, password: passwordHash });
const userId = user.id;
await Token.create({ userId, accessToken });
res.json({ accessToken, user });
});
const registerRouter = router;
export default registerRouter;
+35 -95
View File
@@ -77,6 +77,8 @@ router.get("/:buildId", async (req, res) => {
router.post("/", async (req, res) => {
const { companyId, buildId, slot, startAt, client, duration } = req.body;
console.log("client", client);
if (!companyId || !buildId || !startAt || !slot) {
return res.json({
status: "error",
@@ -148,109 +150,47 @@ router.post("/", async (req, res) => {
startAt: startAtISO,
duration,
endAt: endAtISO,
client,
});
const url = `https://stream.graff.tech/scheduled/${scheduledSession.id}`;
// <-- Send an mail
if (client?.email) {
console.log("client?.email", client?.email);
// create reusable transporter object using the default SMTP transport
let transporter = createTransport({
host: "mail.netangels.ru",
port: 587,
secure: false, // true for 465, false for other ports
auth: {
user: "stream@graff.tech", // generated ethereal user
pass: "zLUbt8Io7dh2F9KT", // generated ethereal password
},
});
// send mail with defined transport object
try {
await transporter.sendMail({
from: "stream@graff.tech", // sender address
to: client.email, // list of receivers
subject: "Приглашение на демонстрацию - stream.graff.tech", // Subject line
html: `<div>
Ссылка для подключения к демонстрации: <a href="${url}" target="_blank">${url}</a>
</div>`,
});
} catch (error) {
console.log("error", (error as Error).message);
}
}
return res.json({
status: "success",
scheduledSessionId: scheduledSession.id,
url: `https://stream.graff.tech/scheduled/${scheduledSession.id}`,
});
}
const schedule = await Schedule.findOne({
buildId,
startDate: { $lte: startAtISO },
// endDate: { $gte: startAtISO },
});
if (!schedule) {
return res.json({
status: "error",
message: "No matching schedule found", // Подходящего расписания не найдено
});
}
const scheduledSessions = await ScheduledSession.find({
buildId,
startAt: {
$gte: startOfDay(startAtISO),
$lte: endOfDay(startAtISO),
},
});
const endAtISO = addMinutes(startAtISO, schedule.sessionDuration);
if (scheduledSessions.length) {
const overlappingSessions = [];
for (const session of scheduledSessions) {
if (
areIntervalsOverlapping(
{
start: session.startAt,
end: addMinutes(session.endAt, schedule.sessionBreak),
},
{ start: startAtISO, end: endAtISO }
)
) {
overlappingSessions.push(session);
}
}
if (overlappingSessions.length >= build.sessionLimit) {
return res.json({
status: "error",
message:
"It is not possible to create a session because it overlaps with the time of another session", //Невозможно создать сеанс, поскольку он перекрывается со временем другого сеанса.
});
}
}
const scheduledSession = await ScheduledSession.create({
buildId,
client,
startAt: startAtISO,
endAt: endAtISO,
});
const url = `https://stream.graff.tech/scheduled/${scheduledSession.id}`;
// <-- Send an mail
if (client?.email) {
// create reusable transporter object using the default SMTP transport
let transporter = createTransport({
host: "mail.netangels.ru",
port: 587,
secure: false, // true for 465, false for other ports
auth: {
user: "stream@graff.tech", // generated ethereal user
pass: "zLUbt8Io7dh2F9KT", // generated ethereal password
},
});
// send mail with defined transport object
try {
await transporter.sendMail({
from: "stream@graff.tech", // sender address
to: client.email, // list of receivers
subject: "Приглашение на демонстрацию - stream.graff.tech", // Subject line
html: `<div>
Ссылка для подключения к демонстрации: <a href="${url}" target="_blank">${url}</a>
</div>`,
});
} catch (error) {
console.log("error", (error as Error).message);
}
}
// Send an mail -->
res.json({
status: "success",
scheduledSessionId: scheduledSession.id,
url,
});
});
router.put("/:id", async (req, res) => {
+5
View File
@@ -0,0 +1,5 @@
interface IToken {
refreshToken: string;
}
export default IToken;
+8
View File
@@ -0,0 +1,8 @@
interface IUser {
id: string;
accessToken: string;
username: string;
password?: string;
}
export default IUser;
-12
View File
@@ -1,12 +0,0 @@
declare global {
namespace NodeJS {
interface ProcessEnv {
PORT?: string;
NODE_ENV: "development" | "production";
MONGO_URI: string;
JWT_SECRET: string;
}
}
}
export {};
+5 -2
View File
@@ -55,7 +55,7 @@
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
"outDir": "dist" /* Specify an output folder for all emitted files. */,
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
@@ -105,5 +105,8 @@
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
},
"include": ["src/**/*.ts", "env.d.ts"],
"exclude": ["node_modules"],
"ts-node": { "transpileOnly": true }
}
+45
View File
@@ -98,6 +98,13 @@
dependencies:
"@types/node" "*"
"@types/cookie-parser@^1.4.7":
version "1.4.7"
resolved "https://registry.yarnpkg.com/@types/cookie-parser/-/cookie-parser-1.4.7.tgz#c874471f888c72423d78d2b3c32d1e8579cf3c8f"
integrity sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==
dependencies:
"@types/express" "*"
"@types/cors@^2.8.14":
version "2.8.17"
resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b"
@@ -115,6 +122,26 @@
"@types/range-parser" "*"
"@types/send" "*"
"@types/express-serve-static-core@^5.0.0":
version "5.0.1"
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.1.tgz#3c9997ae9d00bc236e45c6374e84f2596458d9db"
integrity sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==
dependencies:
"@types/node" "*"
"@types/qs" "*"
"@types/range-parser" "*"
"@types/send" "*"
"@types/express@*":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.0.tgz#13a7d1f75295e90d19ed6e74cab3678488eaa96c"
integrity sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==
dependencies:
"@types/body-parser" "*"
"@types/express-serve-static-core" "^5.0.0"
"@types/qs" "*"
"@types/serve-static" "*"
"@types/express@^4.17.17":
version "4.17.21"
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d"
@@ -394,6 +421,14 @@ content-type@~1.0.4:
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
cookie-parser@^1.4.7:
version "1.4.7"
resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.7.tgz#e2125635dfd766888ffe90d60c286404fa0e7b26"
integrity sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==
dependencies:
cookie "0.7.2"
cookie-signature "1.0.6"
cookie-signature@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
@@ -404,6 +439,11 @@ cookie@0.5.0:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b"
integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
cookie@0.7.2:
version "0.7.2"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
cors@^2.8.5:
version "2.8.5"
resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
@@ -766,6 +806,11 @@ is-number@^7.0.0:
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
jose@^5.9.6:
version "5.9.6"
resolved "https://registry.yarnpkg.com/jose/-/jose-5.9.6.tgz#77f1f901d88ebdc405e57cce08d2a91f47521883"
integrity sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==
jsonwebtoken@^9.0.2:
version "9.0.2"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3"