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[]
|
||||
comments Comment[]
|
||||
likes Like[]
|
||||
media Media[] @relation("UploadedMedia")
|
||||
refreshToken RefreshToken[]
|
||||
|
||||
media Media[] @relation("UploadedMedia")
|
||||
following Follow[] @relation("Following")
|
||||
followers Follow[] @relation("Followers")
|
||||
}
|
||||
|
@ -125,3 +126,9 @@ model Follow {
|
|||
|
||||
@@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": [
|
||||
{ "name": "DATABASE_URL", "generated": true },
|
||||
{ "name": "TOKEN_SECRET", "generated": true },
|
||||
{ "name": "REFRESH_TOKEN_SECRET", "generated": true },
|
||||
{
|
||||
"name": "DB_USER",
|
||||
"generated": false,
|
||||
|
|
|
@ -54,9 +54,9 @@ if (fs.existsSync(".env")) {
|
|||
);
|
||||
} while (!input || input.length < setting.minLength);
|
||||
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
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +1,63 @@
|
|||
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 jwt from "jsonwebtoken";
|
||||
import jwt, { TokenExpiredError } from "jsonwebtoken";
|
||||
import dotenv from "dotenv";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
|
||||
import { RefreshTokenPayload } from "../types/tokens";
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
const prisma = new PrismaClient();
|
||||
// load environment variables from .env file
|
||||
dotenv.config();
|
||||
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)
|
||||
function generateAccessToken(username: string, userId: string, role: string) {
|
||||
return jwt.sign({ username: username, role: role, sub: userId }, JWT_SECRET, {
|
||||
function generateAccessToken(
|
||||
username: string,
|
||||
userId: string,
|
||||
role: string,
|
||||
refreshTokenId: string
|
||||
) {
|
||||
return jwt.sign(
|
||||
{ username: username, role: role, sub: userId, jti: refreshTokenId },
|
||||
JWT_SECRET,
|
||||
{
|
||||
expiresIn: "1800s",
|
||||
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
|
||||
|
@ -78,7 +117,14 @@ export const registerUser = async (req: Request, res: Response) => {
|
|||
});
|
||||
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.status(StatusCodes.CREATED).json({
|
||||
message: "user created",
|
||||
|
@ -113,7 +159,14 @@ export const loginUser = async (req: Request, res: Response) => {
|
|||
});
|
||||
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.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 dotenv from "dotenv";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import { JwtPayload } from "../types/tokens";
|
||||
dotenv.config();
|
||||
|
||||
// Import the JWT secret from environment variables
|
||||
const JWT_SECRET: string = process.env.TOKEN_SECRET!;
|
||||
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
|
||||
declare global {
|
||||
|
|
|
@ -8,7 +8,6 @@ import { upload } from "../middleware/fileUpload";
|
|||
|
||||
import { validateData } from "../middleware/validationMiddleware";
|
||||
import { uploadPostSchema } from "../schemas/postSchemas";
|
||||
import { get } from "http";
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
|
@ -80,27 +79,20 @@ router.post("/upload", upload, validateData(uploadPostSchema), uploadPost);
|
|||
* 401:
|
||||
* description: not authenticated
|
||||
*/
|
||||
router.get("/getPost/:userId", getPost);
|
||||
router.get("/getPost/:postId", getPost);
|
||||
/**
|
||||
* @swagger
|
||||
* /api/posts/getUserPosts/{userId}:
|
||||
* /api/posts/getUserPosts/:
|
||||
* get:
|
||||
* summary: Get Post
|
||||
* tags: [posts]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: postId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: The user id
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Ok
|
||||
* 401:
|
||||
* description: not authenticated
|
||||
*/
|
||||
router.get("/getuserposts/:userId", getUserPosts);
|
||||
router.get("/getuserposts/", getUserPosts);
|
||||
export default router;
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
userLoginSchema,
|
||||
} from "../schemas/userSchemas";
|
||||
import { authenticateToken } from "../middleware/authenticateToken";
|
||||
|
||||
import { logout, refreshToken } from "../controllers/userController";
|
||||
const userRouter = express.Router();
|
||||
|
||||
import {
|
||||
|
@ -50,7 +50,7 @@ import {
|
|||
* /api/user/register:
|
||||
* post:
|
||||
* summary: Register a new user
|
||||
* tags: [User]
|
||||
* tags: [Auth]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
|
@ -73,7 +73,7 @@ userRouter.post(
|
|||
* /api/user/login:
|
||||
* post:
|
||||
* summary: Log in a user
|
||||
* tags: [User]
|
||||
* tags: [Auth]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
|
@ -109,5 +109,57 @@ userRouter.post("/login", validateData(userLoginSchema), loginUser);
|
|||
* description: Ungültige Anmeldedaten
|
||||
*/
|
||||
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;
|
||||
|
|
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