Skip to content

Commit

Permalink
Merge pull request #6 from Selleo/jw/user-auth
Browse files Browse the repository at this point in the history
feat: user auth
  • Loading branch information
typeWolffo authored Jul 18, 2024
2 parents 21b29ee + 03fcabb commit 6dbc6d0
Show file tree
Hide file tree
Showing 61 changed files with 9,343 additions and 7,674 deletions.
2 changes: 2 additions & 0 deletions examples/common_nestjs_remix/apps/api/.env.example
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
DATABASE_URL="postgres://postgres:guidebook@localhost:5432/guidebook"
JWT_SECRET=
JWT_REFRESH_SECRET=
26 changes: 15 additions & 11 deletions examples/common_nestjs_remix/apps/api/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
module.exports = {
parser: '@typescript-eslint/parser',
parser: "@typescript-eslint/parser",
parserOptions: {
project: 'tsconfig.json',
project: "tsconfig.json",
tsconfigRootDir: __dirname,
sourceType: 'module',
sourceType: "module",
},
plugins: ['@typescript-eslint/eslint-plugin'],
plugins: ["@typescript-eslint/eslint-plugin"],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
ignorePatterns: [".eslintrc.js"],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
},
};
20 changes: 20 additions & 0 deletions examples/common_nestjs_remix/apps/api/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Config } from "jest";

const config: Config = {
moduleFileExtensions: ["js", "json", "ts"],
rootDir: ".",
testRegex: ".*\\.spec\\.ts$",
transform: {
"^.+\\.(t|j)s$": "ts-jest",
},
collectCoverageFrom: ["**/*.(t|j)s"],
coverageDirectory: "./coverage",
testEnvironment: "node",
setupFilesAfterEnv: ["<rootDir>/test/jest-setup.ts"],
moduleNameMapper: {
"^src/(.*)$": "<rootDir>/src/$1",
},
modulePaths: ["."],
};

export default config;
42 changes: 24 additions & 18 deletions examples/common_nestjs_remix/apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"test:e2e:watch": "jest --config ./test/jest-e2e.json --watch",
"db:migrate": "drizzle-kit migrate",
"db:generate": "drizzle-kit generate"
},
Expand All @@ -28,55 +29,60 @@
"@nestjs/config": "^3.2.3",
"@nestjs/core": "^10.0.0",
"@nestjs/cqrs": "^10.2.7",
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.4.0",
"@sinclair/typebox": "^0.32.34",
"add": "^2.0.6",
"bcrypt": "^5.1.1",
"cookie": "^0.6.0",
"cookie-parser": "^1.4.6",
"drizzle-kit": "^0.22.8",
"drizzle-orm": "^0.31.2",
"drizzle-typebox": "^0.1.1",
"lodash": "^4.17.21",
"nestjs-typebox": "3.0.0-next.8",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"postgres": "^3.4.4",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1"
"rxjs": "^7.8.1",
"uuid": "^10.0.0"
},
"devDependencies": {
"@faker-js/faker": "^8.4.1",
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/bcrypt": "^5.0.2",
"@types/cookie": "^0.6.0",
"@types/cookie-parser": "^1.4.7",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/lodash": "^4.17.6",
"@types/node": "^20.3.1",
"@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38",
"@types/supertest": "^6.0.0",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.42.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"faker": "link:@types/@faker-js/faker",
"fishery": "^2.2.2",
"jest": "^29.5.0",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",
"testcontainers": "^10.10.3",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
34 changes: 30 additions & 4 deletions examples/common_nestjs_remix/apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@ import { DrizzlePostgresModule } from "@knaadh/nestjs-drizzle-postgres";
import database from "./common/configuration/database";
import { ConfigModule, ConfigService } from "@nestjs/config";
import * as schema from "./storage/schema";
import { ManagementModule } from "./management/management.module";
import { AuthModule } from "./auth/auth.module";
import { UsersModule } from "./users/users.module";
import { JwtModule } from "@nestjs/jwt";
import jwtConfig from "./common/configuration/jwt";
import { APP_GUARD } from "@nestjs/core";
import { JwtAuthGuard } from "./common/guards/jwt-auth-guard";

@Module({
imports: [
ConfigModule.forRoot({
load: [database],
load: [database, jwtConfig],
isGlobal: true,
}),
DrizzlePostgresModule.registerAsync({
Expand All @@ -25,9 +30,30 @@ import { ManagementModule } from "./management/management.module";
},
inject: [ConfigService],
}),
ManagementModule,
JwtModule.registerAsync({
useFactory(configService: ConfigService) {
return {
secret: configService.get<string>("jwt.secret")!,
signOptions: {
expiresIn: configService.get<string>(
"JWT_EXPIRATION_TIME",
"15min",
),
},
};
},
inject: [ConfigService],
global: true,
}),
AuthModule,
UsersModule,
],
controllers: [],
providers: [],
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],
})
export class AppModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { DatabasePg } from "../../common/index";
import { INestApplication } from "@nestjs/common";
import { isArray } from "lodash";
import request from "supertest";
import { createUserFactory } from "../../../test/factory/user.factory";
import { createE2ETest } from "../../../test/create-e2e-test";
import { AuthService } from "../auth.service";
import * as cookie from "cookie";

describe("AuthController (e2e)", () => {
let app: INestApplication;
let authService: AuthService;
let db: DatabasePg;
let userFactory: ReturnType<typeof createUserFactory>;

beforeAll(async () => {
const { app: testApp } = await createE2ETest();
app = testApp;
authService = app.get(AuthService);
db = app.get("DB");
userFactory = createUserFactory(db);
});

describe("POST /auth/register", () => {
it("should register a new user", async () => {
const user = await userFactory
.withCredentials({ password: "password123" })
.build();

const response = await request(app.getHttpServer())
.post("/auth/register")
.set("Accept", "application/json")
.set("Content-Type", "application/json")
.send({
email: user.email,
password: user.credentials?.password,
});

expect(response.status).toEqual(201);
expect(response.body.data).toHaveProperty("id");
expect(response.body.data.email).toBe(user.email);
});

it("should return 409 if user already exists", async () => {
const existingUser = {
email: "[email protected]",
password: "password123",
};

await authService.register(existingUser.email, existingUser.password);

await request(app.getHttpServer())
.post("/auth/register")
.send(existingUser)
.expect(409);
});
});

describe("POST /auth/login", () => {
it("should login and return user data with cookies", async () => {
const user = await userFactory
.withCredentials({
password: "password123",
})
.create({
email: "[email protected]",
});

const response = await request(app.getHttpServer())
.post("/auth/login")
.send({
email: user.email,
password: user.credentials?.password,
});

expect(response.status).toEqual(201);
expect(response.body.data).toHaveProperty("id");
expect(response.body.data.email).toBe(user.email);
expect(response.headers["set-cookie"]).toBeDefined();
expect(response.headers["set-cookie"].length).toBe(2);
});

it("should return 401 for invalid credentials", async () => {
await request(app.getHttpServer())
.post("/auth/login")
.send({
email: "[email protected]",
password: "wrongpassword",
})
.expect(401);
});
});

describe("POST /auth/logout", () => {
it("should clear token cookies for a logged-in user", async () => {
let accessToken = "";

const user = userFactory.build();
const password = "password123";
await authService.register(user.email, password);

const loginResponse = await request(app.getHttpServer())
.post("/auth/login")
.send({
email: user.email,
password: password,
});

const cookies = loginResponse.headers["set-cookie"];

if (Array.isArray(cookies)) {
cookies.forEach((cookieString) => {
const parsedCookie = cookie.parse(cookieString);
if ("access_token" in parsedCookie) {
accessToken = parsedCookie.access_token;
}
});
}

const logoutResponse = await request(app.getHttpServer())
.post("/auth/logout")
.set("Cookie", `access_token=${accessToken};`);

const logoutCookies = logoutResponse.headers["set-cookie"];

expect(loginResponse.status).toBe(201);
expect(logoutResponse.status).toBe(201);
expect(logoutResponse.headers["set-cookie"]).toBeDefined();
expect(logoutCookies.length).toBe(2);
expect(logoutCookies[0]).toContain("access_token=;");
expect(logoutCookies[1]).toContain("refresh_token=;");
});
});

describe("POST /auth/refresh", () => {
it("should refresh tokens", async () => {
const user = await userFactory.build();
const password = "password123";
await authService.register(user.email, password);

const loginResponse = await request(app.getHttpServer())
.post("/auth/login")
.send({
email: user.email,
password: password,
})
.expect(201);

const cookies = loginResponse.headers["set-cookie"];

let refreshToken = "";

if (isArray(cookies)) {
cookies.forEach((cookie) => {
if (cookie.startsWith("refresh_token=")) {
refreshToken = cookie;
}
});
}

const response = await request(app.getHttpServer())
.post("/auth/refresh")
.set("Cookie", [refreshToken])
.expect(201);

expect(response.headers["set-cookie"]).toBeDefined();
expect(response.headers["set-cookie"].length).toBe(2);
});

it("should return 401 for invalid refresh token", async () => {
await request(app.getHttpServer())
.post("/auth/refresh")
.set("Cookie", ["refreshToken=invalid_token"])
.expect(401);
});
});
});
Loading

0 comments on commit 6dbc6d0

Please sign in to comment.