Skip to content

Commit 29158dc

Browse files
committed
✨ pfp!
1 parent 42b7a0e commit 29158dc

16 files changed

+251
-23
lines changed

graphql/authentication.graphql

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ type User {
66
name: String!
77
email: String!
88
profilePicture: String
9+
profilePicturePublicId: String
910
description: String
1011
codeforcesUsername: String
1112
githubToken: String

graphql/profile.graphql

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ type Mutation {
7272
}
7373

7474
type Query {
75-
getUser: User!
75+
getUser: RestrictedUserSelf!
7676
search(input: SearchInput!): [RestrictedUserOther!]!
7777
getFollowers(input: PaginationInput): [RestrictedUserOther!]!
7878
getFollowing(input: PaginationInput): [RestrictedUserOther!]!

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@
3838
"@types/bcrypt": "^5.0.0",
3939
"apollo-server-express": "^3.10.3",
4040
"bcrypt": "^5.1.0",
41+
"buffer-to-stream": "^1.0.0",
4142
"class-validator": "^0.13.2",
43+
"cloudinary": "^1.32.0",
4244
"graphql": "^16.6.0",
4345
"graphql-scalars": "^1.19.0",
4446
"handlebars": "^4.7.7",
@@ -58,6 +60,7 @@
5860
"@types/express": "^4.17.13",
5961
"@types/jest": "28.1.8",
6062
"@types/jsonwebtoken": "^8.5.9",
63+
"@types/multer": "^1.4.7",
6164
"@types/node": "^16.0.0",
6265
"@types/nodemailer": "^6.4.6",
6366
"@types/supertest": "^2.0.11",

prisma/schema.prisma

+17-16
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,23 @@ datasource db {
88
}
99

1010
model User {
11-
id String @id @default(auto()) @map("_id") @db.ObjectId
12-
email String @unique
13-
name String
14-
password String
15-
isVerified Boolean @default(false)
16-
profilePicture String?
17-
description String?
18-
createdAt DateTime @default(now())
19-
updatedAt DateTime @updatedAt
20-
githubToken String?
21-
leetcodeUsername String?
22-
codeforcesUsername String?
23-
following User[] @relation("FollowingUser", fields: [followingIds], references: [id])
24-
followingIds String[] @db.ObjectId
25-
followedBy User[] @relation("FollowingUser", fields: [followedByIds], references: [id])
26-
followedByIds String[] @db.ObjectId
11+
id String @id @default(auto()) @map("_id") @db.ObjectId
12+
email String @unique
13+
name String
14+
password String
15+
isVerified Boolean @default(false)
16+
profilePicture String?
17+
profilePicturePublicId String?
18+
description String?
19+
createdAt DateTime @default(now())
20+
updatedAt DateTime @updatedAt
21+
githubToken String?
22+
leetcodeUsername String?
23+
codeforcesUsername String?
24+
following User[] @relation("FollowingUser", fields: [followingIds], references: [id])
25+
followingIds String[] @db.ObjectId
26+
followedBy User[] @relation("FollowingUser", fields: [followedByIds], references: [id])
27+
followedByIds String[] @db.ObjectId
2728
}
2829

2930
model Token {

src/app.controller.ts

+34-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
1-
import { Controller, Get, Query, Redirect, Render } from '@nestjs/common';
1+
import {
2+
Controller,
3+
Get,
4+
Query,
5+
Redirect,
6+
Render,
7+
Post,
8+
Headers,
9+
UploadedFile,
10+
UseInterceptors,
11+
UnauthorizedException,
12+
BadRequestException,
13+
} from '@nestjs/common';
14+
import { FileInterceptor } from '@nestjs/platform-express';
215
import { AppService } from './app.service';
316
import { GithubCallbackQuery } from './constants/profile.types';
417

@@ -33,4 +46,24 @@ export class AppController {
3346
return { url: `/api/error?message=${error.message}` };
3447
}
3548
}
49+
50+
@Post('/upload/profile-picture')
51+
@UseInterceptors(FileInterceptor('file'))
52+
async uploadProfilePicture(
53+
@UploadedFile() file: Express.Multer.File,
54+
@Headers('authorization') authorization: string,
55+
) {
56+
console.log('called');
57+
const token = authorization?.split(' ')[1];
58+
if (!token) {
59+
throw new UnauthorizedException('Invalid request, token not found');
60+
}
61+
if (!file) {
62+
throw new BadRequestException('Invalid request, file not found');
63+
}
64+
if (!file.mimetype.startsWith('image')) {
65+
throw new BadRequestException('Invalid request, file is not an image');
66+
}
67+
return await this.appService.uploadProfilePicture(file, token);
68+
}
3669
}

src/app.module.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { ProfileModule } from './profile/profile.module';
1212
import { PrismaService } from 'prisma/prisma.service';
1313
import { DashboardService } from './dashboard/dashboard.service';
1414
import { DashboardModule } from './dashboard/dashboard.module';
15+
import { CloudinaryModule } from './cloudinary/cloudinary.module';
16+
import { CloudinaryService } from './cloudinary/cloudinary.service';
1517

1618
@Module({
1719
imports: [
@@ -27,8 +29,15 @@ import { DashboardModule } from './dashboard/dashboard.module';
2729
MailModule,
2830
ProfileModule,
2931
DashboardModule,
32+
CloudinaryModule,
3033
],
3134
controllers: [AppController],
32-
providers: [AppService, ProfileService, PrismaService, DashboardService],
35+
providers: [
36+
AppService,
37+
ProfileService,
38+
PrismaService,
39+
DashboardService,
40+
CloudinaryService,
41+
],
3342
})
3443
export class AppModule {}

src/app.service.ts

+36-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { Injectable } from '@nestjs/common';
22
import axios from 'axios';
3-
import { BadRequestException } from '@nestjs/common';
3+
import {
4+
BadRequestException,
5+
InternalServerErrorException,
6+
} from '@nestjs/common';
47
import {
58
GITHUB_CLIENT_ID,
69
GITHUB_CLIENT_SECRET,
@@ -9,10 +12,15 @@ import {
912
import { PrismaService } from 'prisma/prisma.service';
1013
import { GithubCallbackQuery } from './constants/profile.types';
1114
import * as jwt from 'jsonwebtoken';
15+
import { decode } from './constants/decode';
16+
import { CloudinaryService } from './cloudinary/cloudinary.service';
1217

1318
@Injectable()
1419
export class AppService {
15-
constructor(private prisma: PrismaService) {}
20+
constructor(
21+
private prisma: PrismaService,
22+
private cloudinary: CloudinaryService,
23+
) {}
1624

1725
getHello(): string {
1826
return 'Hello from B-704';
@@ -88,4 +96,30 @@ export class AppService {
8896
throw new Error('Github code is either invalid or expired');
8997
}
9098
}
99+
100+
async uploadProfilePicture(
101+
file: Express.Multer.File,
102+
token: string,
103+
): Promise<string> {
104+
console.log(file);
105+
try {
106+
const user = await decode(token, this.prisma);
107+
const upload = await this.cloudinary.uploadImage(file);
108+
if (user.profilePicturePublicId) {
109+
await this.cloudinary.deleteImage(user.profilePicturePublicId);
110+
}
111+
await this.prisma.user.update({
112+
where: { id: user.id },
113+
data: {
114+
profilePicture: upload.secure_url,
115+
profilePicturePublicId: upload.public_id,
116+
},
117+
});
118+
return upload.secure_url;
119+
} catch (error) {
120+
throw new InternalServerErrorException(
121+
error.message || 'Internal error, could not upload profile picture',
122+
);
123+
}
124+
}
91125
}

src/cloudinary/cloudinary.module.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Module } from '@nestjs/common';
2+
import { CloudinaryProvider } from './cloudinary.provider';
3+
import { CloudinaryService } from './cloudinary.service';
4+
5+
@Module({
6+
providers: [CloudinaryService, CloudinaryProvider],
7+
})
8+
export class CloudinaryModule {}

src/cloudinary/cloudinary.provider.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { ConfigOptions, v2 as cloudinary } from 'cloudinary';
2+
import {
3+
CLOUDINARY_API_KEY,
4+
CLOUDINARY_API_SECRET,
5+
CLOUDINARY_CLOUD_NAME,
6+
} from 'src/constants/env';
7+
import { CLOUDINARY } from '../constants/constants';
8+
9+
export const CloudinaryProvider = {
10+
provide: CLOUDINARY,
11+
useFactory: (): ConfigOptions => {
12+
return cloudinary.config({
13+
cloud_name: CLOUDINARY_CLOUD_NAME,
14+
api_key: CLOUDINARY_API_KEY,
15+
api_secret: CLOUDINARY_API_SECRET,
16+
});
17+
},
18+
};
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { CloudinaryService } from './cloudinary.service';
3+
4+
describe('CloudinaryService', () => {
5+
let service: CloudinaryService;
6+
7+
beforeEach(async () => {
8+
const module: TestingModule = await Test.createTestingModule({
9+
providers: [CloudinaryService],
10+
}).compile();
11+
12+
service = module.get<CloudinaryService>(CloudinaryService);
13+
});
14+
15+
it('should be defined', () => {
16+
expect(service).toBeDefined();
17+
});
18+
});

src/cloudinary/cloudinary.service.ts

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Injectable, BadRequestException } from '@nestjs/common';
2+
import { UploadApiResponse, v2 as cloudinary } from 'cloudinary';
3+
import toStream = require('buffer-to-stream');
4+
5+
@Injectable()
6+
export class CloudinaryService {
7+
/**
8+
* It takes an image file, uploads it to Cloudinary, and returns a promise that resolves to the
9+
* response from Cloudinary
10+
* @param image - Express.Multer.File - This is the image that was uploaded by the user.
11+
* @returns A promise that resolves to an UploadApiResponse
12+
*/
13+
async uploadImage(image: Express.Multer.File): Promise<UploadApiResponse> {
14+
try {
15+
return new Promise((resolve, reject) => {
16+
const upload = cloudinary.uploader.upload_stream(
17+
{
18+
folder: 'programmer-profile-pfp',
19+
format: 'jpg',
20+
width: 500,
21+
height: 500,
22+
crop: 'fill',
23+
compression: 'high',
24+
},
25+
(error, result) => {
26+
if (error) reject(error);
27+
resolve(result);
28+
},
29+
);
30+
toStream(image.buffer).pipe(upload);
31+
// upload image with reduced size
32+
});
33+
} catch (error) {
34+
throw new BadRequestException(error.message);
35+
}
36+
}
37+
38+
/**
39+
* It takes a publicId as an argument, and then it deletes the image from cloudinary.
40+
* @param {string} publicId - The public ID of the image you want to delete.
41+
* @returns The response from the cloudinary API.
42+
*/
43+
async deleteImage(publicId: string): Promise<UploadApiResponse> {
44+
try {
45+
return cloudinary.uploader.destroy(publicId);
46+
} catch (error) {
47+
throw new BadRequestException(error.message);
48+
}
49+
}
50+
}

src/constants/constants.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export const PAGINATION_LIMIT = 10;
2+
export const CLOUDINARY = 'Cloudinary';

src/constants/env.ts

+3
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,7 @@ export const {
99
GITHUB_CLIENT_ID,
1010
GITHUB_CLIENT_SECRET,
1111
GITHUB_SECRET_KEY,
12+
CLOUDINARY_CLOUD_NAME,
13+
CLOUDINARY_API_KEY,
14+
CLOUDINARY_API_SECRET,
1215
} = process.env;

src/graphql.types.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export class User {
7474
name: string;
7575
email: string;
7676
profilePicture?: Nullable<string>;
77+
profilePicturePublicId?: Nullable<string>;
7778
description?: Nullable<string>;
7879
codeforcesUsername?: Nullable<string>;
7980
githubToken?: Nullable<string>;
@@ -139,7 +140,7 @@ export class ContributionGraph {
139140
export abstract class IQuery {
140141
abstract contributionGraph(input?: Nullable<FakeInput>): ContributionGraph | Promise<ContributionGraph>;
141142

142-
abstract getUser(): User | Promise<User>;
143+
abstract getUser(): RestrictedUserSelf | Promise<RestrictedUserSelf>;
143144

144145
abstract search(input: SearchInput): RestrictedUserOther[] | Promise<RestrictedUserOther[]>;
145146

src/main.ts

+9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NestFactory } from '@nestjs/core';
22
import { AppModule } from './app.module';
3+
import { json, urlencoded } from 'body-parser';
34
import { NestExpressApplication } from '@nestjs/platform-express';
45
import { join } from 'path';
56
import * as dotenv from 'dotenv';
@@ -9,6 +10,14 @@ const PORT = process.env.PORT || 8080;
910

1011
async function bootstrap() {
1112
const app = await NestFactory.create<NestExpressApplication>(AppModule);
13+
// app.use(
14+
// urlencoded({
15+
// limit: '10mb',
16+
// extended: true,
17+
// parameterLimit: 50000,
18+
// }),
19+
// );
20+
// app.use(json({ limit: '10mb' }));
1221
app.enableCors();
1322
app.setGlobalPrefix('api');
1423
app.setBaseViewsDir(join(__dirname, './', 'views'));

0 commit comments

Comments
 (0)