-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #34 from WildCodeSchool/feat-3329-backend_authenti…
…cation Feat 3329 backend authentication
- Loading branch information
Showing
9 changed files
with
1,160 additions
and
122 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,29 +1,100 @@ | ||
import "reflect-metadata"; | ||
import { ApolloServer } from "@apollo/server"; | ||
import { startStandaloneServer } from "@apollo/server/standalone"; | ||
import type { Request } from "express"; | ||
import { GraphQLError } from "graphql"; | ||
import { parse } from "graphql"; | ||
import jwt from "jsonwebtoken"; | ||
import { buildSchema } from "type-graphql"; | ||
import AppDataSource from "./AppDataSource"; | ||
import { User } from "./entities/User"; | ||
import { UserModel } from "./models/UserModel"; | ||
import { resolvers } from "./resolvers"; | ||
import type { MyContext } from "./types/context"; | ||
|
||
import dotenv from "dotenv"; | ||
dotenv.config(); | ||
|
||
// Fonction pour décoder et vérifier le JWT | ||
const getUser = async (token: string): Promise<User | null> => { | ||
if (!token) return null; | ||
|
||
try { | ||
const decoded = jwt.verify( | ||
token, | ||
process.env.JWT_SECRET || "defaultSecret", | ||
) as jwt.JwtPayload; | ||
return await AppDataSource.manager.findOne(User, { | ||
where: { email: decoded.email }, | ||
}); | ||
} catch (error) { | ||
console.error("JWT verification error:", error); | ||
return null; | ||
} | ||
}; | ||
|
||
const startServer = async () => { | ||
await AppDataSource.initialize(); | ||
await AppDataSource.initialize(); | ||
|
||
const schema = await buildSchema({ | ||
resolvers, | ||
}); | ||
|
||
const server = new ApolloServer<MyContext>({ | ||
schema, | ||
introspection: true, | ||
}); | ||
|
||
const { url } = await startStandaloneServer(server, { | ||
listen: { port: 4000 }, | ||
context: async ({ req }): Promise<MyContext> => { | ||
const request = req as Request; | ||
|
||
try { | ||
if (request.body?.query) { | ||
const parsedQuery = parse(request.body.query); | ||
const operationDefinitions = parsedQuery.definitions.filter( | ||
(def) => def.kind === "OperationDefinition", | ||
); | ||
|
||
const isLoginMutation = operationDefinitions.some( | ||
(def) => | ||
def.kind === "OperationDefinition" && | ||
def.operation === "mutation" && | ||
"name" in def.selectionSet.selections[0] && | ||
(def.selectionSet.selections[0].name.value === "login" || | ||
def.selectionSet.selections[0].name.value === "createUser"), | ||
); | ||
|
||
if (isLoginMutation) { | ||
return { models: { User: UserModel } }; | ||
} | ||
} | ||
} catch (error) { | ||
console.error("Erreur lors du parsing de la requête GraphQL :", error); | ||
} | ||
|
||
const schema = await buildSchema({ | ||
resolvers, | ||
}); | ||
const token = req.headers.authorization?.split(" ")[1] || ""; | ||
const user = await getUser(token); | ||
|
||
const server = new ApolloServer({ | ||
schema, | ||
introspection: true, | ||
}); | ||
if (!user) { | ||
throw new GraphQLError("You must be logged in to query this schema", { | ||
extensions: { | ||
code: "UNAUTHENTICATED", | ||
}, | ||
}); | ||
} | ||
|
||
const { url } = await startStandaloneServer(server, { | ||
listen: { port: 4000 }, | ||
}); | ||
return { | ||
user, | ||
models: { User: UserModel }, | ||
}; | ||
}, | ||
}); | ||
|
||
console.log(`🚀 Server ready at: ${url}`); | ||
console.log(`🚀 Server ready at: ${url}`); | ||
}; | ||
|
||
startServer().catch((error) => { | ||
console.error("Error starting server:", error); | ||
console.error("❌ Error starting server:", error); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import AppDataSource from "../AppDataSource"; | ||
import { User } from "../entities/User"; | ||
import type { CreateUserInput, UpdateUserInput } from "../inputs/UsersInput"; | ||
|
||
export const UserModel = { | ||
// Récupérer tous les utilisateurs | ||
async getAll(): Promise<User[]> { | ||
return await AppDataSource.manager.find(User); | ||
}, | ||
|
||
// Récupérer un utilisateur par ID | ||
async getById(id: number): Promise<User | null> { | ||
return await AppDataSource.manager.findOne(User, { where: { id } }); | ||
}, | ||
|
||
// Récupérer un utilisateur par email | ||
async getByEmail(email: string): Promise<User | null> { | ||
console.log("Recherche d'utilisateur avec l'email:", email); // Vérifie que cette ligne est avant la requête | ||
return await AppDataSource.manager.findOne(User, { where: { email } }); | ||
}, | ||
|
||
// Créer un nouvel utilisateur | ||
async create(data: CreateUserInput): Promise<User> { | ||
const user = AppDataSource.manager.create(User, data); | ||
return await AppDataSource.manager.save(user); | ||
}, | ||
|
||
// Mettre à jour un utilisateur | ||
async update( | ||
id: number, | ||
data: Partial<UpdateUserInput>, | ||
): Promise<User | null> { | ||
await AppDataSource.manager.update(User, id, data); | ||
return await AppDataSource.manager.findOne(User, { where: { id } }); | ||
}, | ||
|
||
// Supprimer un utilisateur | ||
async delete(id: number): Promise<void> { | ||
await AppDataSource.manager.delete(User, id); | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,86 +1,162 @@ | ||
import * as argon2 from "argon2"; | ||
import { Arg, Mutation, Resolver } from "type-graphql"; | ||
import AppDataSource from "../../AppDataSource"; | ||
import { GraphQLError } from "graphql"; | ||
import jwt from "jsonwebtoken"; | ||
import { Arg, Ctx, Mutation, Resolver } from "type-graphql"; | ||
import { User } from "../../entities/User"; | ||
import { CreateUserInput, UpdateUserInput } from "../../inputs/UsersInput"; | ||
import type { MyContext } from "../../types/context"; | ||
|
||
@Resolver(User) | ||
export class UsersMutations { | ||
// Login | ||
@Mutation(() => String) | ||
async login( | ||
@Arg("email") email: string, | ||
@Arg("password") password: string, | ||
@Ctx() context: MyContext, | ||
): Promise<string> { | ||
const user = await context.models.User.getByEmail(email); | ||
if (!user) { | ||
throw new GraphQLError("User not found", { | ||
extensions: { code: "USER_NOT_FOUND" }, | ||
}); | ||
} | ||
|
||
// Vérifier si le mot de passe est correct | ||
const isValid = await argon2.verify(user.password, password); | ||
if (!isValid) { | ||
throw new GraphQLError("Invalid password", { | ||
extensions: { code: "INVALID_PASSWORD" }, | ||
}); | ||
} | ||
|
||
// Générer le JWT | ||
const jwtSecret = process.env.JWT_SECRET; | ||
if (!jwtSecret) { | ||
throw new Error("JWT secret is not defined"); | ||
} | ||
|
||
const token = jwt.sign( | ||
{ id: user.id, email: user.email, role: user.role }, | ||
jwtSecret, | ||
{ | ||
expiresIn: "24h", | ||
}, | ||
); | ||
return token; | ||
} | ||
|
||
// Mutation pour créer un nouvel utilisateur | ||
@Mutation(() => User) | ||
async createUser( | ||
@Arg("data", () => CreateUserInput) data: CreateUserInput, | ||
@Ctx() context: MyContext, | ||
): Promise<User> { | ||
const { User: UserModel } = context.models; | ||
|
||
// Vérifier si un utilisateur avec cet email existe déjà | ||
const existingUser = await AppDataSource.manager.findOne(User, { | ||
where: { email: data.email }, | ||
}); | ||
const existingUser = await UserModel.getByEmail(data.email); | ||
if (existingUser) { | ||
throw new Error("User with this email already exists"); | ||
throw new GraphQLError("User with this email already exists", { | ||
extensions: { code: "EMAIL_ALREADY_TAKEN" }, | ||
}); | ||
} | ||
|
||
// Hacher le mot de passe | ||
const hashedPassword = await argon2.hash(data.password); | ||
|
||
// Créer un nouvel utilisateur | ||
const user = new User( | ||
data.username, | ||
data.description, | ||
data.email, | ||
hashedPassword, | ||
data.image, | ||
data.birthday, | ||
data.gender, | ||
data.weight, | ||
data.height, | ||
data.createdAt, | ||
data.level, | ||
data.role, | ||
); | ||
return await user.save(); | ||
const newUser = await UserModel.create({ | ||
...data, | ||
password: hashedPassword, | ||
}); | ||
|
||
return newUser; | ||
} | ||
|
||
// Mutation pour mettre à jour un utilisateur | ||
// Mettre à jour un utilisateur (nécessite d'être admin ou soi-même) | ||
@Mutation(() => User) | ||
async updateUser( | ||
@Arg("data", () => UpdateUserInput) data: UpdateUserInput, | ||
@Ctx() context: MyContext, | ||
): Promise<User> { | ||
const existingUser = await AppDataSource.manager.findOne(User, { | ||
where: { id: data.id }, | ||
}); | ||
const { | ||
user, | ||
models: { User: UserModel }, | ||
} = context; | ||
|
||
// Vérifier l'authentification | ||
if (!user) { | ||
throw new GraphQLError("Unauthorized", { | ||
extensions: { code: "UNAUTHORIZED" }, | ||
}); | ||
} | ||
|
||
// Récupérer l'utilisateur existant | ||
const existingUser = await UserModel.getById(data.id); | ||
if (!existingUser) { | ||
throw new Error("User not found"); | ||
throw new GraphQLError("User not found", { | ||
extensions: { code: "USER_NOT_FOUND" }, | ||
}); | ||
} | ||
|
||
// Vérifier que l'utilisateur peut modifier ces données | ||
const isAdmin = user.role === "admin"; | ||
const isSelf = user.id === data.id; | ||
if (!isAdmin && !isSelf) { | ||
throw new GraphQLError("Permission denied", { | ||
extensions: { code: "FORBIDDEN" }, | ||
}); | ||
} | ||
|
||
// Mettre à jour l'utilisateur | ||
existingUser.username = data.username; | ||
existingUser.description = data.description; | ||
existingUser.email = data.email; | ||
existingUser.password = await argon2.hash(data.password); | ||
existingUser.image = data.image; | ||
existingUser.birthday = data.birthday; | ||
existingUser.gender = data.gender; | ||
existingUser.weight = data.weight; | ||
existingUser.height = data.height; | ||
existingUser.createdAt = data.createdAt; | ||
existingUser.role = data.role; | ||
existingUser.level = data.level; | ||
|
||
// Sauvegarder dans la base de données | ||
return await AppDataSource.manager.save(existingUser); | ||
// Mettre à jour les champs autorisés | ||
const updatedUser = await UserModel.update(data.id, { | ||
...data, | ||
password: data.password | ||
? await argon2.hash(data.password) | ||
: existingUser.password, | ||
}); | ||
|
||
return updatedUser; | ||
} | ||
|
||
// Mutation pour supprimer un utilisateur | ||
@Mutation(() => Boolean) | ||
async deleteUser(@Arg("id") id: number): Promise<boolean> { | ||
const user = await AppDataSource.manager.findOne(User, { where: { id } }); | ||
async deleteUser( | ||
@Arg("id") id: number, | ||
@Ctx() context: MyContext, | ||
): Promise<boolean> { | ||
const { | ||
user, | ||
models: { User: UserModel }, | ||
} = context; | ||
|
||
// Vérifier l'authentification | ||
if (!user) { | ||
throw new Error("User not found"); | ||
throw new GraphQLError("Unauthorized", { | ||
extensions: { code: "UNAUTHORIZED" }, | ||
}); | ||
} | ||
|
||
// Vérifier si l'utilisateur existe | ||
const existingUser = await UserModel.getById(id); | ||
if (!existingUser) { | ||
throw new GraphQLError("User not found", { | ||
extensions: { code: "USER_NOT_FOUND" }, | ||
}); | ||
} | ||
|
||
// Vérifier que l'utilisateur peut supprimer ce compte | ||
const isAdmin = user.role === "admin"; | ||
const isSelf = user.id === id; | ||
if (!isAdmin && !isSelf) { | ||
throw new GraphQLError("Permission denied", { | ||
extensions: { code: "FORBIDDEN" }, | ||
}); | ||
} | ||
|
||
// Supprimer l'utilisateur de la base de données | ||
await AppDataSource.manager.remove(User, user); | ||
// Supprimer l'utilisateur | ||
await UserModel.delete(id); | ||
return true; | ||
} | ||
} |
Oops, something went wrong.