mirror of
https://github.com/bubblecup-12/VogelSocialMedia.git
synced 2025-07-06 15:18:48 +00:00
add refresh token endpoints
This commit is contained in:
parent
9be66a0a2f
commit
a9bda19891
8 changed files with 235 additions and 35 deletions
|
@ -25,8 +25,9 @@ model User {
|
||||||
posts Post[]
|
posts Post[]
|
||||||
comments Comment[]
|
comments Comment[]
|
||||||
likes Like[]
|
likes Like[]
|
||||||
media Media[] @relation("UploadedMedia")
|
refreshToken RefreshToken[]
|
||||||
|
|
||||||
|
media Media[] @relation("UploadedMedia")
|
||||||
following Follow[] @relation("Following")
|
following Follow[] @relation("Following")
|
||||||
followers Follow[] @relation("Followers")
|
followers Follow[] @relation("Followers")
|
||||||
}
|
}
|
||||||
|
@ -125,3 +126,9 @@ model Follow {
|
||||||
|
|
||||||
@@id([followingUserId, followedUserId])
|
@@id([followingUserId, followedUserId])
|
||||||
}
|
}
|
||||||
|
model RefreshToken {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
expiresAt DateTime
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
userId String
|
||||||
|
}
|
|
@ -9,6 +9,7 @@
|
||||||
"requiredKeys": [
|
"requiredKeys": [
|
||||||
{ "name": "DATABASE_URL", "generated": true },
|
{ "name": "DATABASE_URL", "generated": true },
|
||||||
{ "name": "TOKEN_SECRET", "generated": true },
|
{ "name": "TOKEN_SECRET", "generated": true },
|
||||||
|
{ "name": "REFRESH_TOKEN_SECRET", "generated": true },
|
||||||
{
|
{
|
||||||
"name": "DB_USER",
|
"name": "DB_USER",
|
||||||
"generated": false,
|
"generated": false,
|
||||||
|
|
|
@ -54,9 +54,9 @@ if (fs.existsSync(".env")) {
|
||||||
);
|
);
|
||||||
} while (!input || input.length < setting.minLength);
|
} while (!input || input.length < setting.minLength);
|
||||||
process.env[setting.name] = input;
|
process.env[setting.name] = input;
|
||||||
} else if (setting.name === "TOKEN_SECRET") {
|
} else if (setting.name === "TOKEN_SECRET" || setting.name === "REFRESH_TOKEN_SECRET") {
|
||||||
// generating a random JWT secret
|
// generating a random JWT secret
|
||||||
const jwtSecret: string = crypto.randomBytes(32).toString("hex"); // 64 Zeichen
|
const jwtSecret: string = crypto.randomBytes(32).toString("hex"); // 64 character
|
||||||
process.env[setting.name] = jwtSecret;
|
process.env[setting.name] = jwtSecret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,63 @@
|
||||||
import express, { Request, Response } from "express";
|
import express, { Request, Response } from "express";
|
||||||
import { PrismaClient, User } from "../../prisma/app/generated/prisma/client";
|
import {
|
||||||
|
PrismaClient,
|
||||||
|
User,
|
||||||
|
RefreshToken,
|
||||||
|
} from "../../prisma/app/generated/prisma/client";
|
||||||
import { UserLoginDto, UserRegistrationDto } from "../schemas/userSchemas";
|
import { UserLoginDto, UserRegistrationDto } from "../schemas/userSchemas";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt, { TokenExpiredError } from "jsonwebtoken";
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
import bcrypt from "bcryptjs";
|
import bcrypt from "bcryptjs";
|
||||||
import { StatusCodes } from "http-status-codes";
|
import { StatusCodes } from "http-status-codes";
|
||||||
|
import { RefreshTokenPayload } from "../types/tokens";
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
// load environment variables from .env file
|
// load environment variables from .env file
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
const JWT_SECRET: string = process.env.TOKEN_SECRET!; // this secret is used to sign the JWT token
|
const JWT_SECRET: string = process.env.TOKEN_SECRET!; // this secret is used to sign the JWT token
|
||||||
|
const REFRESH_TOKEN_SECRET: string = process.env.REFRESH_TOKEN_SECRET!;
|
||||||
|
|
||||||
// Generate a JWT token with the username as payload and a secret from the environment variables which expires in 1800 seconds (30 minutes)
|
// Generate a JWT token with the username as payload and a secret from the environment variables which expires in 1800 seconds (30 minutes)
|
||||||
function generateAccessToken(username: string, userId: string, role: string) {
|
function generateAccessToken(
|
||||||
return jwt.sign({ username: username, role: role, sub: userId }, JWT_SECRET, {
|
username: string,
|
||||||
|
userId: string,
|
||||||
|
role: string,
|
||||||
|
refreshTokenId: string
|
||||||
|
) {
|
||||||
|
return jwt.sign(
|
||||||
|
{ username: username, role: role, sub: userId, jti: refreshTokenId },
|
||||||
|
JWT_SECRET,
|
||||||
|
{
|
||||||
expiresIn: "1800s",
|
expiresIn: "1800s",
|
||||||
issuer: "VogelApi",
|
issuer: "VogelApi",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateRefreshToken(
|
||||||
|
userId: string
|
||||||
|
): Promise<{ token: string; id: string }> {
|
||||||
|
const expiresAt = new Date(Date.now() + 100 * 60 * 60 * 1000); // 100 h
|
||||||
|
let refreshToken: RefreshToken;
|
||||||
|
{
|
||||||
|
refreshToken = await prisma.refreshToken.create({
|
||||||
|
data: {
|
||||||
|
expiresAt: expiresAt,
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
token: jwt.sign({ jti: refreshToken.id }, REFRESH_TOKEN_SECRET, {
|
||||||
|
expiresIn: "100h",
|
||||||
|
}),
|
||||||
|
id: refreshToken.id,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Endpoint to register a new user
|
// Endpoint to register a new user
|
||||||
|
@ -78,7 +117,14 @@ export const registerUser = async (req: Request, res: Response) => {
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const token: string = generateAccessToken(user.username, user.id, user.role); // generate a JWT token with the username and userId as payload
|
const refreshToken = await generateRefreshToken(user.id);
|
||||||
|
res.set("Refresh-Token", refreshToken.token);
|
||||||
|
const token: string = generateAccessToken(
|
||||||
|
user.username,
|
||||||
|
user.id,
|
||||||
|
user.role,
|
||||||
|
refreshToken.id
|
||||||
|
); // generate a JWT token with the username and userId as payload
|
||||||
res.set("Authorization", `Bearer ${token}`); // set the token in the response header
|
res.set("Authorization", `Bearer ${token}`); // set the token in the response header
|
||||||
res.status(StatusCodes.CREATED).json({
|
res.status(StatusCodes.CREATED).json({
|
||||||
message: "user created",
|
message: "user created",
|
||||||
|
@ -113,7 +159,14 @@ export const loginUser = async (req: Request, res: Response) => {
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const token: string = generateAccessToken(user.username, user.id, user.role); // generate a JWT token with the username and userId as payload
|
const refreshToken = await generateRefreshToken(user.id);
|
||||||
|
res.set("Refresh-Token", refreshToken.token);
|
||||||
|
const token: string = generateAccessToken(
|
||||||
|
user.username,
|
||||||
|
user.id,
|
||||||
|
user.role,
|
||||||
|
refreshToken.id
|
||||||
|
); // generate a JWT token with the username and userId as payload
|
||||||
res.set("Authorization", `Bearer ${token}`); // set the token in the response header
|
res.set("Authorization", `Bearer ${token}`); // set the token in the response header
|
||||||
res.status(StatusCodes.OK).json({ message: "User logged in successfully" });
|
res.status(StatusCodes.OK).json({ message: "User logged in successfully" });
|
||||||
};
|
};
|
||||||
|
@ -150,3 +203,90 @@ export const getUser = async (req: Request, res: Response) => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
export const refreshToken = async (req: Request, res: Response) => {
|
||||||
|
const refreshToken: string | undefined = req.headers[
|
||||||
|
"refresh-token"
|
||||||
|
] as string;
|
||||||
|
if (!refreshToken) {
|
||||||
|
res.status(StatusCodes.UNAUTHORIZED).json({
|
||||||
|
error: "Unauthorized",
|
||||||
|
details: [{ message: "No token provided" }],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await jwt.verify(
|
||||||
|
refreshToken,
|
||||||
|
REFRESH_TOKEN_SECRET,
|
||||||
|
async (err: any, decoded: any) => {
|
||||||
|
if (err) {
|
||||||
|
if (err instanceof jwt.TokenExpiredError) {
|
||||||
|
res.status(StatusCodes.UNAUTHORIZED).json({
|
||||||
|
error: "Refreshtoken expired",
|
||||||
|
details: [{ message: "Refreshtoken has expired" }],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(StatusCodes.FORBIDDEN).json({
|
||||||
|
error: "Invalid refreshtoken",
|
||||||
|
details: [{ message: "Refreshtoken is invalid" }],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = decoded as RefreshTokenPayload;
|
||||||
|
try {
|
||||||
|
const now = new Date();
|
||||||
|
const storedToken = await prisma.refreshToken.findUnique({
|
||||||
|
where: {
|
||||||
|
id: payload.jti,
|
||||||
|
expiresAt: {
|
||||||
|
gt: now, // expiresAt > now
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: { user: true },
|
||||||
|
});
|
||||||
|
if (!storedToken) {
|
||||||
|
res.status(StatusCodes.UNAUTHORIZED).json({
|
||||||
|
error: "InvalidRefreshToken",
|
||||||
|
details: [
|
||||||
|
{ message: "Refresh token is invalid or no longer exists" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await prisma.refreshToken.delete({
|
||||||
|
where: { id: payload.jti },
|
||||||
|
});
|
||||||
|
const refreshToken = await generateRefreshToken(storedToken.user.id);
|
||||||
|
res.set("Refresh-Token", refreshToken.token);
|
||||||
|
const token: string = generateAccessToken(
|
||||||
|
storedToken.user.username,
|
||||||
|
storedToken.user.id,
|
||||||
|
storedToken.user.role,
|
||||||
|
refreshToken.id
|
||||||
|
); // generate a JWT token with the username and userId as payload
|
||||||
|
res.set("Authorization", `Bearer ${token}`); // set the token in the response header
|
||||||
|
res.status(StatusCodes.OK).send();
|
||||||
|
} catch {
|
||||||
|
res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
|
||||||
|
error: "Server error",
|
||||||
|
details: [{ message: "Server Error" }],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logout = async (req: Request, res: Response) => {
|
||||||
|
const jti: string = req.query.jti as string;
|
||||||
|
try {
|
||||||
|
await prisma.refreshToken.delete({ where: { id: jti } });
|
||||||
|
res.removeHeader("Authorization");
|
||||||
|
res.removeHeader("Refresh-Token");
|
||||||
|
res.status(StatusCodes.NO_CONTENT).send();
|
||||||
|
} catch {
|
||||||
|
res.status(StatusCodes.INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -2,20 +2,14 @@ import express, { NextFunction, Request, Response } from "express";
|
||||||
import jwt, { TokenExpiredError } from "jsonwebtoken";
|
import jwt, { TokenExpiredError } from "jsonwebtoken";
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
import { StatusCodes } from "http-status-codes";
|
import { StatusCodes } from "http-status-codes";
|
||||||
|
import { JwtPayload } from "../types/tokens";
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
// Import the JWT secret from environment variables
|
// Import the JWT secret from environment variables
|
||||||
const JWT_SECRET: string = process.env.TOKEN_SECRET!;
|
const JWT_SECRET: string = process.env.TOKEN_SECRET!;
|
||||||
if (!JWT_SECRET) console.error("No JWT secret provided");
|
if (!JWT_SECRET) console.error("No JWT secret provided");
|
||||||
|
|
||||||
// Define the structure of the JWT payload
|
|
||||||
interface JwtPayload {
|
|
||||||
id: string;
|
|
||||||
username: string;
|
|
||||||
role: string;
|
|
||||||
iat: number;
|
|
||||||
exp: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extend the Express Request interface to include the user property
|
// Extend the Express Request interface to include the user property
|
||||||
declare global {
|
declare global {
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { upload } from "../middleware/fileUpload";
|
||||||
|
|
||||||
import { validateData } from "../middleware/validationMiddleware";
|
import { validateData } from "../middleware/validationMiddleware";
|
||||||
import { uploadPostSchema } from "../schemas/postSchemas";
|
import { uploadPostSchema } from "../schemas/postSchemas";
|
||||||
import { get } from "http";
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -80,27 +79,20 @@ router.post("/upload", upload, validateData(uploadPostSchema), uploadPost);
|
||||||
* 401:
|
* 401:
|
||||||
* description: not authenticated
|
* description: not authenticated
|
||||||
*/
|
*/
|
||||||
router.get("/getPost/:userId", getPost);
|
router.get("/getPost/:postId", getPost);
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/posts/getUserPosts/{userId}:
|
* /api/posts/getUserPosts/:
|
||||||
* get:
|
* get:
|
||||||
* summary: Get Post
|
* summary: Get Post
|
||||||
* tags: [posts]
|
* tags: [posts]
|
||||||
* security:
|
* security:
|
||||||
* - bearerAuth: []
|
* - bearerAuth: []
|
||||||
* parameters:
|
|
||||||
* - in: query
|
|
||||||
* name: postId
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: string
|
|
||||||
* description: The user id
|
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: Ok
|
* description: Ok
|
||||||
* 401:
|
* 401:
|
||||||
* description: not authenticated
|
* description: not authenticated
|
||||||
*/
|
*/
|
||||||
router.get("/getuserposts/:userId", getUserPosts);
|
router.get("/getuserposts/", getUserPosts);
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
userLoginSchema,
|
userLoginSchema,
|
||||||
} from "../schemas/userSchemas";
|
} from "../schemas/userSchemas";
|
||||||
import { authenticateToken } from "../middleware/authenticateToken";
|
import { authenticateToken } from "../middleware/authenticateToken";
|
||||||
|
import { logout, refreshToken } from "../controllers/userController";
|
||||||
const userRouter = express.Router();
|
const userRouter = express.Router();
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -50,7 +50,7 @@ import {
|
||||||
* /api/user/register:
|
* /api/user/register:
|
||||||
* post:
|
* post:
|
||||||
* summary: Register a new user
|
* summary: Register a new user
|
||||||
* tags: [User]
|
* tags: [Auth]
|
||||||
* requestBody:
|
* requestBody:
|
||||||
* required: true
|
* required: true
|
||||||
* content:
|
* content:
|
||||||
|
@ -73,7 +73,7 @@ userRouter.post(
|
||||||
* /api/user/login:
|
* /api/user/login:
|
||||||
* post:
|
* post:
|
||||||
* summary: Log in a user
|
* summary: Log in a user
|
||||||
* tags: [User]
|
* tags: [Auth]
|
||||||
* requestBody:
|
* requestBody:
|
||||||
* required: true
|
* required: true
|
||||||
* content:
|
* content:
|
||||||
|
@ -109,5 +109,57 @@ userRouter.post("/login", validateData(userLoginSchema), loginUser);
|
||||||
* description: Ungültige Anmeldedaten
|
* description: Ungültige Anmeldedaten
|
||||||
*/
|
*/
|
||||||
userRouter.get("/getUser/:username", authenticateToken(), getUser);
|
userRouter.get("/getUser/:username", authenticateToken(), getUser);
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/user/refreshToken:
|
||||||
|
* get:
|
||||||
|
* summary: Refresh JWT tokens
|
||||||
|
* description: |
|
||||||
|
* Verifiziert einen bereitgestellten Refresh-Token (im Header) und gibt neue Tokens im Header zurück.
|
||||||
|
* tags:
|
||||||
|
* - Auth
|
||||||
|
* parameters:
|
||||||
|
* - in: header
|
||||||
|
* name: Refresh-Token
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: Der gültige JWT-Refresh-Token
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Tokens erfolgreich erneuert
|
||||||
|
* headers:
|
||||||
|
* Authorization:
|
||||||
|
* description: Neuer Access-Token im Bearer-Format
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* Refresh-Token:
|
||||||
|
* description: Neuer Refresh-Token
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* 401:
|
||||||
|
* description: Ungültiger oder abgelaufener Refresh-Token
|
||||||
|
* 403:
|
||||||
|
* description: Fehlerhafte Signatur oder ungültiger Token
|
||||||
|
* 500:
|
||||||
|
* description: Serverfehler
|
||||||
|
*/
|
||||||
|
|
||||||
|
userRouter.get("/refreshToken", refreshToken);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/user/logout/:
|
||||||
|
* delete:
|
||||||
|
* summary: logout
|
||||||
|
* tags: [Auth]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* responses:
|
||||||
|
* 204:
|
||||||
|
* description: logged out
|
||||||
|
* 401:
|
||||||
|
* description: not authenticated
|
||||||
|
*/
|
||||||
|
userRouter.delete("/logout", authenticateToken(), logout);
|
||||||
export default userRouter;
|
export default userRouter;
|
||||||
|
|
14
code/backend/src/types/tokens.ts
Normal file
14
code/backend/src/types/tokens.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
export interface JwtPayload {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
role: string;
|
||||||
|
iat: number;
|
||||||
|
exp: number;
|
||||||
|
jti: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefreshTokenPayload {
|
||||||
|
jti: string;
|
||||||
|
iat: number;
|
||||||
|
exp: number;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue