Skip to content

Commit

Permalink
Merge pull request #34 from WildCodeSchool/feat-3329-backend_authenti…
Browse files Browse the repository at this point in the history
…cation

Feat 3329 backend authentication
  • Loading branch information
gael-pri authored Feb 6, 2025
2 parents 0bb5642 + 94352df commit 19fae6f
Show file tree
Hide file tree
Showing 9 changed files with 1,160 additions and 122 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ For a task on the backend, the number begins with 1xxx
For a task on the frontend, the number begins with 6xxx
For the other task, the number begins with 0xxx

After this prefix, your banch name should contain only lower case letters and dashes.
After this prefix, your banch name should contain only lower case letters and dashes.
4 changes: 2 additions & 2 deletions backend/src/entities/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { GroupList } from "./GroupList";
import { Tag } from "./Tag";

@ObjectType()
@Entity()
@Entity("user")
export class User extends BaseEntity {
@PrimaryGeneratedColumn()
@Field(() => ID)
Expand All @@ -32,7 +32,7 @@ export class User extends BaseEntity {
@Field()
email: string;

@Column({ length: 50 })
@Column({ length: 250 })
@Field()
password: string;

Expand Down
97 changes: 84 additions & 13 deletions backend/src/index.ts
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);
});
41 changes: 41 additions & 0 deletions backend/src/models/UserModel.ts
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);
},
};
170 changes: 123 additions & 47 deletions backend/src/resolvers/Users/UsersMutations.ts
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;
}
}
Loading

0 comments on commit 19fae6f

Please sign in to comment.