Skip to content

Commit

Permalink
feat: merge auth
Browse files Browse the repository at this point in the history
  • Loading branch information
naelob committed Nov 3, 2023
2 parents 1dc5a0a + 506df45 commit 0f13062
Show file tree
Hide file tree
Showing 32 changed files with 457 additions and 166 deletions.
6 changes: 5 additions & 1 deletion packages/api/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
ENV=dev
DATABASE_URL=
JWT_SECRET=
<<<<<<< HEAD
JWT_SECRET=
=======
JWT_SECRET="SECRET"
>>>>>>> feat/fix-auth
5 changes: 5 additions & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
},
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.0.0",
"@nestjs/jwt": "^10.1.1",
"@nestjs/mapped-types": "*",
Expand All @@ -30,7 +31,11 @@
"@nestjs/swagger": "^7.1.14",
"@prisma/client": "^5.4.2",
"axios": "^1.5.1",
"bcrypt": "^5.1.1",
"crypto": "^1.0.1",
"dotenv": "^16.3.1",
"passport": "^0.6.0",
"passport-headerapikey": "^1.2.2",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"reflect-metadata": "^0.1.13",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,28 @@
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from './users/users.module';
import { LocalStrategy } from './strategies/local.strategy';
import { PassportModule } from '@nestjs/passport';
import { JwtModule, JwtService } from '@nestjs/jwt';
import { jwtConstants } from './utils/constants';

Check warning on line 5 in packages/api/src/@core/auth/auth.module.ts

View workflow job for this annotation

GitHub Actions / build-api (16.x)

'jwtConstants' is defined but never used
import { JwtStrategy } from './strategies/jwt.strategy';
import { UsersService } from './users/users.service';
import { ApiKeyStrategy } from './strategies/auth-header-api-key.strategy';
import { PrismaService } from '../prisma/prisma.service';
import { ConfigService } from '@nestjs/config';

@Module({
providers: [
AuthService,
LocalStrategy,
JwtStrategy,
UsersService,
JwtService,
ApiKeyStrategy,
PrismaService,
ConfigService,
],
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' },
secret: process.env.JWT_SECRET,
}),
],
exports: [UsersService, JwtService],
exports: [PrismaService, JwtService],
})
export class AuthModule {}
File renamed without changes.
208 changes: 208 additions & 0 deletions packages/api/src/@core/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { CreateUserDto, LoginCredentials } from './dto/create-user.dto';
import { PrismaService } from '../prisma/prisma.service';
import * as bcrypt from 'bcrypt';
import * as crypto from 'crypto';

//TODO: Ensure the JWT is used for user session authentication and that it's short-lived.
@Injectable()
export class AuthService {
constructor(private prisma: PrismaService, private jwtService: JwtService) {}

async register(user: CreateUserDto) {
try {
// Generate a salt and hash the password
const salt = await bcrypt.genSalt();
const hashedPassword = await bcrypt.hash(user.password_hash, salt);

const res = await this.prisma.users.create({
data: {
email: user.email,
password_hash: hashedPassword,
first_name: user.first_name,
last_name: user.last_name,
},
});
if (!res) {
throw new UnauthorizedException('registering issue');
}
const { password_hash, ...resp_user } = res;

Check warning on line 30 in packages/api/src/@core/auth/auth.service.ts

View workflow job for this annotation

GitHub Actions / build-api (16.x)

'password_hash' is assigned a value but never used
return resp_user;
} catch (error) {
console.log(error);
}
}

async login(user: LoginCredentials): Promise<{ access_token: string }> {
try {
const foundUser = await this.prisma.users.findUnique({
where: { id_user: user.id_user },
});
//TODO: if not founder
if (
foundUser &&
(await bcrypt.compare(user.password_hash, foundUser.password_hash))
) {
const { password_hash, ...result } = user;

Check warning on line 47 in packages/api/src/@core/auth/auth.service.ts

View workflow job for this annotation

GitHub Actions / build-api (16.x)

'password_hash' is assigned a value but never used

if (!result) {
throw new UnauthorizedException('Invalid credentials.');
}
}
const payload = {
email: foundUser.email,
sub: foundUser.id_user,
};

return {
access_token: this.jwtService.sign(payload, {
secret: process.env.JWT_SECRET,
}), // token used to generate api keys
};
} catch (error) {
console.log(error);
}
}

hashApiKey(apiKey: string): string {
console.log('hey hashing...');
return crypto.createHash('sha256').update(apiKey).digest('hex');
}

/*hashApiKey(apiKey: string): string {
return crypto.createHash('sha256').update(apiKey).digest('hex');
}
async generateApiKey1(projectId: number): Promise<string> {
const api_key = 'PROD_' + crypto.randomUUID();
// Store the API key in the database associated with the user
const new_api_key = await this.prisma.api_keys.create({
data: {
api_key_hash: this.hashApiKey(api_key),
id_project: projectId,
},
});
return api_key;
}
async validateApiKey1(apiKey: string): Promise<boolean> {
const hash = this.hashApiKey(apiKey);
const api_key = await this.prisma.api_keys.findUnique({
where: {
api_key_hash: hash,
},
});
return Boolean(api_key);
}*/

async generateApiKey(
projectId: number,
userId: number,
): Promise<{ access_token: string }> {
console.log("'ddddd");
const secret = process.env.JWT_SECRET;

Check warning on line 105 in packages/api/src/@core/auth/auth.service.ts

View workflow job for this annotation

GitHub Actions / build-api (16.x)

'secret' is assigned a value but never used
const jwtPayload = {
sub: userId,
projectId: projectId,
};
return {
access_token: this.jwtService.sign(jwtPayload, {
secret: process.env.JWT_SECRET,
expiresIn: '1y',
}),
};
}

async generateApiKeyForUser(
userId: number,
projectId: number,
): Promise<{ api_key: string }> {
try {
console.log('here is my userId ', userId);
//tmp:
const resp = await this.prisma.organizations.create({
data: {
name: 'org1',
stripe_customer_id: 'oneone',
},
});
await this.prisma.projects.create({
data: {
name: 'proj',
id_organization: resp.id_organization,
},
});
//TODO: CHECK IF PROJECT_ID IS EXISTENT
//fetch user_id
const foundUser = await this.prisma.users.findUnique({
where: { id_user: userId },
});
if (!foundUser) {
throw new UnauthorizedException(
'user not found inside api key function generation',
);
}
//TODO: check if user is indeed inside the project

// Generate a new API key (use a secure method for generation)
const { access_token } = await this.generateApiKey(projectId, userId);
console.log('hey');
// Store the API key in the database associated with the user
const hashed_token = this.hashApiKey(access_token);
console.log('hey2');
const new_api_key = await this.prisma.api_keys.create({
data: {
api_key_hash: hashed_token,
id_project: projectId,
id_user: userId,
},
});
if (!new_api_key) {
throw new UnauthorizedException('api keys issue to add to db');
}
console.log('.....');

return { api_key: access_token };
} catch (error) {
console.log(error);
}
}

async validateApiKey(apiKey: string): Promise<boolean> {
try {
// Decode the JWT to verify if it's valid and get the payload
const decoded = this.jwtService.verify(apiKey, {
secret: process.env.JWT_SECRET,
});

const hashed_api_key = this.hashApiKey(apiKey);
const saved_api_key = await this.prisma.api_keys.findUnique({
where: {
api_key_hash: hashed_api_key,
},
});
if (!saved_api_key) {
throw new UnauthorizedException('Failed to fetch API key from DB');
}

if (decoded.projectId !== saved_api_key.id_project) {
throw new UnauthorizedException(
'Failed to validate API key: projectId invalid.',
);
}

// Validate that the JWT payload matches the provided userId and projectId
if (decoded.sub !== saved_api_key.id_user) {
throw new UnauthorizedException(
'Failed to validate API key: userId invalid.',
);
}
return true;
} catch (error) {
console.error('validateApiKey error:', error);
throw new UnauthorizedException('Failed to validate API key.');
}
}
}
12 changes: 12 additions & 0 deletions packages/api/src/@core/auth/dto/create-user.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export class CreateUserDto {
first_name: string;
last_name: string;
email: string;
password_hash: string;
}

export type LoginCredentials = {
id_user?: number;
email?: string;
password_hash: string;
};
5 changes: 5 additions & 0 deletions packages/api/src/@core/auth/guards/api-key.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class ApiKeyAuthGuard extends AuthGuard('api-key') {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { HeaderAPIKeyStrategy } from 'passport-headerapikey';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from '../auth.service';

@Injectable()
export class ApiKeyStrategy extends PassportStrategy(
HeaderAPIKeyStrategy,
'api-key',
) {
constructor(private authService: AuthService) {
super(
{ header: 'Authorization', prefix: '' },
true,
async (apikey: string, done, req) => {
try {
const isValid = await this.authService.validateApiKey(apikey);
if (!isValid) {
return done(new UnauthorizedException('Invalid API Key'), null);
}
console.log('validating api request... : ' + req.user);
// If the API key is valid, attach the user to the request object
req.user = { ...req.user, apiKeyValidated: true };

// If valid, we now have the user info from the API key validation process
return done(null, req.user);
} catch (error) {
return done(error, false);
}
},
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,21 @@ import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from '../utils/constants';

Check warning on line 4 in packages/api/src/@core/auth/strategies/jwt.strategy.ts

View workflow job for this annotation

GitHub Actions / build-api (16.x)

'jwtConstants' is defined but never used
import { ConfigService } from '@nestjs/config';

Check warning on line 5 in packages/api/src/@core/auth/strategies/jwt.strategy.ts

View workflow job for this annotation

GitHub Actions / build-api (16.x)

'ConfigService' is defined but never used
import * as dotenv from 'dotenv';
dotenv.config();

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret,
secretOrKey: process.env.JWT_SECRET,
});
}

async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
return { userId: payload.sub, email: payload.email };
}
}
5 changes: 5 additions & 0 deletions packages/api/src/@core/auth/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface ApiKey {
projectId: number;
api_key_name: string;
token: string;
}
File renamed without changes.
7 changes: 7 additions & 0 deletions packages/api/src/@core/organisations/organisations.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { OrganisationsService } from './organisations.service';

@Module({
providers: [OrganisationsService],
})
export class OrganisationsModule {}
18 changes: 18 additions & 0 deletions packages/api/src/@core/organisations/organisations.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { OrganisationsService } from './organisations.service';

describe('OrganisationsService', () => {
let service: OrganisationsService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [OrganisationsService],
}).compile();

service = module.get<OrganisationsService>(OrganisationsService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
9 changes: 9 additions & 0 deletions packages/api/src/@core/organisations/organisations.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Injectable } from '@nestjs/common';

@Injectable()
export class OrganisationsService {
//TODO
async createOrganization() {
return;
}
}
File renamed without changes.
Loading

0 comments on commit 0f13062

Please sign in to comment.