Skip to content

Commit 9b16c0d

Browse files
magrinjAdityaPimpalkar
authored andcommitted
feat: upload profile picture from google (twentyhq#964)
* feat: upload profile picture from google * fix: only add profile picture if user don't have any
1 parent df0295e commit 9b16c0d

File tree

6 files changed

+113
-6
lines changed

6 files changed

+113
-6
lines changed

server/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"class-transformer": "^0.5.1",
5555
"class-validator": "^0.14.0",
5656
"date-fns": "^2.30.0",
57+
"file-type": "13.0.0",
5758
"graphql": "^16.6.0",
5859
"graphql-type-json": "^0.3.2",
5960
"graphql-upload": "^13.0.0",

server/src/core/auth/auth.module.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { PrismaService } from 'src/database/prisma.service';
55
import { UserModule } from 'src/core/user/user.module';
66
import { EnvironmentService } from 'src/integrations/environment/environment.service';
77
import { WorkspaceModule } from 'src/core/workspace/workspace.module';
8+
import { FileModule } from 'src/core/file/file.module';
89

910
import { AuthResolver } from './auth.resolver';
1011

@@ -27,7 +28,7 @@ const jwtModule = JwtModule.registerAsync({
2728
});
2829

2930
@Module({
30-
imports: [jwtModule, UserModule, WorkspaceModule],
31+
imports: [jwtModule, UserModule, WorkspaceModule, FileModule],
3132
controllers: [GoogleAuthController, VerifyAuthController],
3233
providers: [
3334
AuthService,

server/src/core/auth/controllers/google-auth.controller.ts

+41-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common';
22

33
import { Response } from 'express';
4+
import FileType from 'file-type';
5+
import { v4 as uuidV4 } from 'uuid';
6+
7+
import { FileFolder } from 'src/core/file/interfaces/file-folder.interface';
48

59
import { GoogleRequest } from 'src/core/auth/strategies/google.auth.strategy';
610
import { UserService } from 'src/core/user/user.service';
@@ -9,6 +13,8 @@ import { GoogleProviderEnabledGuard } from 'src/core/auth/guards/google-provider
913
import { GoogleOauthGuard } from 'src/core/auth/guards/google-oauth.guard';
1014
import { WorkspaceService } from 'src/core/workspace/services/workspace.service';
1115
import { EnvironmentService } from 'src/integrations/environment/environment.service';
16+
import { getImageBufferFromUrl } from 'src/utils/image';
17+
import { FileUploadService } from 'src/core/file/services/file-upload.service';
1218

1319
@Controller('auth/google')
1420
export class GoogleAuthController {
@@ -17,6 +23,7 @@ export class GoogleAuthController {
1723
private readonly userService: UserService,
1824
private readonly workspaceService: WorkspaceService,
1925
private readonly environmentService: EnvironmentService,
26+
private readonly fileUploadService: FileUploadService,
2027
) {}
2128

2229
@Get()
@@ -29,7 +36,8 @@ export class GoogleAuthController {
2936
@Get('redirect')
3037
@UseGuards(GoogleProviderEnabledGuard, GoogleOauthGuard)
3138
async googleAuthRedirect(@Req() req: GoogleRequest, @Res() res: Response) {
32-
const { firstName, lastName, email, workspaceInviteHash } = req.user;
39+
const { firstName, lastName, email, picture, workspaceInviteHash } =
40+
req.user;
3341

3442
let workspaceId: string | undefined = undefined;
3543
if (workspaceInviteHash) {
@@ -48,7 +56,7 @@ export class GoogleAuthController {
4856
workspaceId = workspace.id;
4957
}
5058

51-
const user = await this.userService.createUser(
59+
let user = await this.userService.createUser(
5260
{
5361
data: {
5462
email,
@@ -65,6 +73,37 @@ export class GoogleAuthController {
6573
workspaceId,
6674
);
6775

76+
if (!user.avatarUrl) {
77+
let imagePath: string | undefined = undefined;
78+
79+
if (picture) {
80+
// Get image buffer from url
81+
const buffer = await getImageBufferFromUrl(picture);
82+
83+
// Extract mimetype and extension from buffer
84+
const type = await FileType.fromBuffer(buffer);
85+
86+
// Upload image
87+
const { paths } = await this.fileUploadService.uploadImage({
88+
file: buffer,
89+
filename: `${uuidV4()}.${type?.ext}`,
90+
mimeType: type?.mime,
91+
fileFolder: FileFolder.ProfilePicture,
92+
});
93+
94+
imagePath = paths[0];
95+
}
96+
97+
user = await this.userService.update({
98+
where: {
99+
id: user.id,
100+
},
101+
data: {
102+
avatarUrl: imagePath,
103+
},
104+
});
105+
}
106+
68107
const loginToken = await this.tokenService.generateLoginToken(user.email);
69108

70109
return res.redirect(this.tokenService.computeRedirectURI(loginToken.token));

server/src/core/auth/strategies/google.auth.strategy.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export type GoogleRequest = Request & {
1111
firstName?: string | null;
1212
lastName?: string | null;
1313
email: string;
14+
picture: string | null;
1415
workspaceInviteHash?: string;
1516
};
1617
};
@@ -45,16 +46,17 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
4546
profile: any,
4647
done: VerifyCallback,
4748
): Promise<void> {
48-
const { name, emails } = profile;
49+
const { name, emails, photos } = profile;
4950
const state =
5051
typeof request.query.state === 'string'
5152
? JSON.parse(request.query.state)
5253
: undefined;
5354

54-
const user = {
55+
const user: GoogleRequest['user'] = {
5556
email: emails[0].value,
5657
firstName: name.givenName,
5758
lastName: name.familyName,
59+
picture: photos?.[0]?.value,
5860
workspaceInviteHash: state.workspaceInviteHash,
5961
};
6062
done(null, user);

server/src/utils/image.ts

+10
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import axios from 'axios';
2+
13
const cropRegex = /([w|h])([0-9]+)/;
24

35
export type ShortCropSize = `${'w' | 'h'}${number}` | 'original';
@@ -19,3 +21,11 @@ export const getCropSize = (value: ShortCropSize): CropSize | null => {
1921
value: +match[2],
2022
};
2123
};
24+
25+
export const getImageBufferFromUrl = async (url: string): Promise<Buffer> => {
26+
const response = await axios.get(url, {
27+
responseType: 'arraybuffer',
28+
});
29+
30+
return Buffer.from(response.data, 'binary');
31+
};

server/yarn.lock

+55-1
Original file line numberDiff line numberDiff line change
@@ -2559,6 +2559,11 @@
25592559
resolved "https://registry.yarnpkg.com/@sqltools/formatter/-/formatter-1.2.5.tgz#3abc203c79b8c3e90fd6c156a0c62d5403520e12"
25602560
integrity sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==
25612561

2562+
"@tokenizer/token@^0.1.1":
2563+
version "0.1.1"
2564+
resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.1.1.tgz#f0d92c12f87079ddfd1b29f614758b9696bc29e3"
2565+
integrity sha512-XO6INPbZCxdprl+9qa/AAbFFOMzzwqYxpjPgLICrMD6C2FCw6qfJOPcBk6JqqPLSaZ/Qx87qn4rpPmPMwaAK6w==
2566+
25622567
"@ts-morph/common@~0.17.0":
25632568
version "0.17.0"
25642569
resolved "https://registry.npmjs.org/@ts-morph/common/-/common-0.17.0.tgz"
@@ -5063,6 +5068,16 @@ file-entry-cache@^6.0.1:
50635068
dependencies:
50645069
flat-cache "^3.0.4"
50655070

5071+
5072+
version "13.0.0"
5073+
resolved "https://registry.yarnpkg.com/file-type/-/file-type-13.0.0.tgz#00091b5b642f131d4cfe9f51192270e363e3d54c"
5074+
integrity sha512-fuTZAd04cbjCOcv+Y9erYUw92G2n7DSsMhIWHNunQz5ajIlDs+yamZ7QgtHe+k3XSFnO0NOUiLq9AA8w6ut+9g==
5075+
dependencies:
5076+
readable-web-to-node-stream "^2.0.0"
5077+
strtok3 "^5.0.1"
5078+
token-types "^2.0.0"
5079+
typedarray-to-buffer "^3.1.5"
5080+
50665081
filename-reserved-regex@^2.0.0:
50675082
version "2.0.0"
50685083
resolved "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz"
@@ -5659,7 +5674,7 @@ [email protected], iconv-lite@^0.4.24:
56595674
dependencies:
56605675
safer-buffer ">= 2.1.2 < 3"
56615676

5662-
ieee754@^1.1.13:
5677+
ieee754@^1.1.13, ieee754@^1.2.1:
56635678
version "1.2.1"
56645679
resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz"
56655680
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
@@ -5951,6 +5966,11 @@ is-typed-array@^1.1.10, is-typed-array@^1.1.9:
59515966
dependencies:
59525967
which-typed-array "^1.1.11"
59535968

5969+
is-typedarray@^1.0.0:
5970+
version "1.0.0"
5971+
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
5972+
integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==
5973+
59545974
is-unicode-supported@^0.1.0:
59555975
version "0.1.0"
59565976
resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz"
@@ -7341,6 +7361,11 @@ [email protected]:
73417361
resolved "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz"
73427362
integrity sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==
73437363

7364+
peek-readable@^3.1.0:
7365+
version "3.1.4"
7366+
resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-3.1.4.tgz#f5c3b41a4eeb63a1322c4131f0b5bac7105b892e"
7367+
integrity sha512-DX7ec7frSMtCWw+zMd27f66hcxIz/w9LQTY2RflB4WNHCVPAye1pJiP2t3gvaaOhu7IOhtPbHw8MemMj+F5lrg==
7368+
73447369
picocolors@^1.0.0:
73457370
version "1.0.0"
73467371
resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz"
@@ -7603,6 +7628,11 @@ readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0:
76037628
string_decoder "^1.1.1"
76047629
util-deprecate "^1.0.1"
76057630

7631+
readable-web-to-node-stream@^2.0.0:
7632+
version "2.0.0"
7633+
resolved "https://registry.yarnpkg.com/readable-web-to-node-stream/-/readable-web-to-node-stream-2.0.0.tgz#751e632f466552ac0d5c440cc01470352f93c4b7"
7634+
integrity sha512-+oZJurc4hXpaaqsN68GoZGQAQIA3qr09Or4fqEsargABnbe5Aau8hFn6ISVleT3cpY/0n/8drn7huyyEvTbghA==
7635+
76067636
readdir-glob@^1.0.0:
76077637
version "1.1.3"
76087638
resolved "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz"
@@ -8148,6 +8178,15 @@ strnum@^1.0.5:
81488178
resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db"
81498179
integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==
81508180

8181+
strtok3@^5.0.1:
8182+
version "5.0.2"
8183+
resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-5.0.2.tgz#bb81f1f56742e16f1a30ccce5dc3d9498aa5475a"
8184+
integrity sha512-EFeVpFC5qDsqPEJSrIYyS/ueFBknGhgSK9cW+YAJF/cgJG/KSjoK7X6rK5xnpcLe7y1LVkVFCXWbAb+ClNKzKQ==
8185+
dependencies:
8186+
"@tokenizer/token" "^0.1.1"
8187+
debug "^4.1.1"
8188+
peek-readable "^3.1.0"
8189+
81518190
81528191
version "0.11.0"
81538192
resolved "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.11.0.tgz"
@@ -8381,6 +8420,14 @@ [email protected]:
83818420
resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz"
83828421
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
83838422

8423+
token-types@^2.0.0:
8424+
version "2.1.1"
8425+
resolved "https://registry.yarnpkg.com/token-types/-/token-types-2.1.1.tgz#bd585d64902aaf720b8979d257b4b850b4d45c45"
8426+
integrity sha512-wnQcqlreS6VjthyHO3Y/kpK/emflxDBNhlNUPfh7wE39KnuDdOituXomIbyI79vBtF0Ninpkh72mcuRHo+RG3Q==
8427+
dependencies:
8428+
"@tokenizer/token" "^0.1.1"
8429+
ieee754 "^1.2.1"
8430+
83848431
tr46@~0.0.3:
83858432
version "0.0.3"
83868433
resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz"
@@ -8619,6 +8666,13 @@ typed-array-length@^1.0.4:
86198666
for-each "^0.3.3"
86208667
is-typed-array "^1.1.9"
86218668

8669+
typedarray-to-buffer@^3.1.5:
8670+
version "3.1.5"
8671+
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
8672+
integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
8673+
dependencies:
8674+
is-typedarray "^1.0.0"
8675+
86228676
typedarray@^0.0.6:
86238677
version "0.0.6"
86248678
resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz"

0 commit comments

Comments
 (0)