Skip to content
This repository has been archived by the owner on May 11, 2021. It is now read-only.

Commit

Permalink
Merge pull request #184 from igorkamyshev/social-login
Browse files Browse the repository at this point in the history
Social login
  • Loading branch information
igorkamyshev committed Aug 18, 2019
2 parents 09b572a + 3c7c1e1 commit 638b5b1
Show file tree
Hide file tree
Showing 30 changed files with 564 additions and 65 deletions.
5 changes: 4 additions & 1 deletion back/.env.dist
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@ PRODUCTION_READY=1

MANNY_API_KEY=Secret
TELEGRAM_BOT_TOKEN=Secret
API_PUBLIC_URL=https://api.checkmoney.space
API_PUBLIC_URL=https://api.checkmoney.space

GOOGLE_CLIENT_ID=Secret
GOOGLE_CLIENT_SECRET=Secret
11 changes: 11 additions & 0 deletions back/evolutions/12.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
ALTER TABLE public."user"
ADD "googleId" VARCHAR(255) DEFAULT NULL,
ADD "email" VARCHAR(255) DEFAULT NULL;

UPDATE public."user" SET email=login WHERE email IS NULL;

#DOWN

ALTER TABLE public."user"
DROP COLUMN "googleId",
DROP COLUMN "email";
6 changes: 4 additions & 2 deletions back/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"prestart:back:prod": "tsc && cp -a ./templates ../dist/back/back",
"start:back:prod": "pm2 start ./pm2.config.js",
"evolutions:init": "evolutions --init",
"evolutions:run": "evolutions"
"evolutions:run": "evolutions -y"
},
"dependencies": {
"@breadhead/detil-ts": "^1.0.1",
Expand All @@ -16,10 +16,12 @@
"@nestjs/jwt": "^0.2.1",
"@nestjs/swagger": "^2.5.1",
"@nestjs/typeorm": "^5.2.2",
"@solid-soda/config": "^1.1.6",
"@solid-soda/config": "^2.0.0",
"@solid-soda/evolutions": "^0.1.0",
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"fast-deep-equal": "^2.0.1",
"google-auth-library": "^5.2.0",
"handlebars": "^4.1.0",
"md5": "^2.2.1",
"morgan": "^1.9.1",
Expand Down
23 changes: 4 additions & 19 deletions back/src/user/application/Authenticator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,27 +28,12 @@ export class Authenticator {
}
}

public async signIn(login: string, password: string): Promise<string> {
const user = await this.userRepo.getOne(login);
const passwordValid = await user.isPasswordValid(
password,
this.passwordEncoder,
);

if (!passwordValid) {
throw new InvalidCredentialsException(login, password);
}

const payload = this.createTokenPayload(user);
const token = this.jwt.sign(payload);

return token;
}

private createTokenPayload(user: User): TokenPayloadModel {
return {
public async encode(user: User): Promise<string> {
const payload: TokenPayloadModel = {
login: user.login,
isManager: user.isManager,
};

return this.jwt.sign(payload);
}
}
17 changes: 1 addition & 16 deletions back/src/user/application/Registrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,7 @@ export class Registrator {
const user = new User(login);

await user.changePassword(password, this.passwordEncoder);

await this.entitySaver.save(user);
}

public async addTelegramAccount(
login: string,
telegramId: number,
): Promise<void> {
const attachedUser = await this.userRepo.findOneByTelegram(telegramId);
if (attachedUser.nonEmpty()) {
throw new LoginAlreadyTakenException(login);
}

const user = await this.userRepo.getOne(login);

user.attachTelegram(telegramId);
user.email = login;

await this.entitySaver.save(user);
}
Expand Down
59 changes: 59 additions & 0 deletions back/src/user/application/SignInProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Injectable } from '@nestjs/common';

import { EntitySaver } from '&back/db/EntitySaver';
import { GoogleProfile } from '&shared/models/user/external/GoogleProfile';

import { User } from '../domain/User.entity';
import { UserRepository } from '../domain/UserRepository';
import { PasswordEncoder } from '../infrastructure/PasswordEncoder/PasswordEncoder';
import { InvalidCredentialsException } from './exception/InvalidCredentialsException';
import { InvalidSocialRequestException } from './exception/InvalidSocialRequestException';
import { GoogleValidator } from './social/GoogleValidator';

@Injectable()
export class SignInProvider {
constructor(
private readonly userRepo: UserRepository,
private readonly passwordEncoder: PasswordEncoder,
private readonly googleValidator: GoogleValidator,
private readonly entitySaver: EntitySaver,
) {}

async signInByLogin(login: string, password: string): Promise<User> {
const user = await this.userRepo.getOne(login);

const passwordValid = await user.isPasswordValid(
password,
this.passwordEncoder,
);

if (!passwordValid) {
throw new InvalidCredentialsException(login, password);
}

return user;
}

async signInByGoogle(profile: GoogleProfile): Promise<User> {
const [valid, optionalUser] = await Promise.all([
this.googleValidator.isValid(profile),
this.userRepo.findOneByGoogle(profile.id),
]);

if (!valid) {
throw new InvalidSocialRequestException(profile.email, 'Google', profile);
}

// okay, user already exist, just sign-in
if (optionalUser.nonEmpty()) {
return optionalUser.get();
}

const user = new User(`google-${profile.id}`);
user.attachGoogle(profile.id);

await this.entitySaver.save(user);

return user;
}
}
47 changes: 47 additions & 0 deletions back/src/user/application/SocialBinder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Injectable } from '@nestjs/common';

import { EntitySaver } from '&back/db/EntitySaver';
import { GoogleProfile } from '&shared/models/user/external/GoogleProfile';

import { UserRepository } from '../domain/UserRepository';
import { InvalidSocialRequestException } from './exception/InvalidSocialRequestException';
import { LoginAlreadyTakenException } from './exception/LoginAlreadyTakenException';
import { GoogleValidator } from './social/GoogleValidator';

@Injectable()
export class SocialBinder {
constructor(
private readonly googleValidator: GoogleValidator,
private readonly userRepo: UserRepository,
private readonly entitySaver: EntitySaver,
) {}

async bindGoogle(login: string, profile: GoogleProfile) {
const [valid, user] = await Promise.all([
this.googleValidator.isValid(profile),
this.userRepo.getOne(login),
]);

if (!valid) {
throw new InvalidSocialRequestException(login, 'Google', profile);
}

user.attachGoogle(profile.id);

await this.entitySaver.save(user);
}

async bindTelegram(login: string, telegramId: number) {
const attachedUser = await this.userRepo.findOneByTelegram(telegramId);

if (attachedUser.nonEmpty()) {
throw new LoginAlreadyTakenException(login);
}

const user = await this.userRepo.getOne(login);

user.attachTelegram(telegramId);

await this.entitySaver.save(user);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export class InvalidSocialRequestException extends Error {
public constructor(
public readonly login: string,
public readonly social: string,
public readonly payload: any,
) {
super(`Invalid credentials for ${login} from ${social}`);
}
}
45 changes: 45 additions & 0 deletions back/src/user/application/social/GoogleValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Injectable } from '@nestjs/common';
import * as deepEqual from 'fast-deep-equal';
import { OAuth2Client } from 'google-auth-library';

import { Configuration } from '&back/config/Configuration';
import { GoogleProfile } from '&shared/models/user/external/GoogleProfile';

@Injectable()
export class GoogleValidator {
private readonly client: OAuth2Client;
private readonly googleClientId: string;

constructor(config: Configuration) {
this.googleClientId = config.getStringOrThrow('GOOGLE_CLIENT_ID');

const googleClientSecret = config.getStringOrThrow('GOOGLE_CLIENT_SECRET');

this.client = new OAuth2Client(this.googleClientId, googleClientSecret);
}

async isValid(profile: GoogleProfile): Promise<boolean> {
const { token } = profile;

try {
const ticket = await this.client.verifyIdToken({
idToken: profile.token,
audience: this.googleClientId,
});

const payload = ticket.getPayload();

const payloadProfile: GoogleProfile = {
token,
name: payload.name,
id: payload.sub,
photo: payload.picture,
email: payload.email,
};

return deepEqual(profile, payloadProfile);
} catch (error) {
return false;
}
}
}
10 changes: 10 additions & 0 deletions back/src/user/domain/User.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,18 @@ export class User {
@Column()
public readonly isManager: boolean = false;

@Column()
public email: string | undefined;

@Column()
private password: string | undefined;

@Column()
private telegramId: string | undefined;

@Column()
private googleId: string | undefined;

public constructor(login: string) {
this.login = login;

Expand All @@ -43,4 +49,8 @@ export class User {
public attachTelegram(telegramId: number): void {
this.telegramId = telegramId.toString();
}

public attachGoogle(googleId: string): void {
this.googleId = googleId;
}
}
11 changes: 11 additions & 0 deletions back/src/user/domain/UserRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ class UserRepo {
return Option.of(user);
}

public async findOneByGoogle(googleId: string): Promise<Option<User>> {
const user = await this.userRepo
.createQueryBuilder()
.where({
googleId,
})
.getOne();

return Option.of(user);
}

public async getDefaultCurrency(login: string): Promise<Currency> {
const user = await this.getOne(login);

Expand Down
29 changes: 23 additions & 6 deletions back/src/user/presentation/http/controller/AuthController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ import {

import { Authenticator } from '&back/user/application/Authenticator';
import { Registrator } from '&back/user/application/Registrator';
import { SignInProvider } from '&back/user/application/SignInProvider';
import { User } from '&back/user/domain/User.entity';
import { PostNoCreate } from '&back/utils/presentation/http/PostNoCreate';

import { AuthRequest } from '../request/AuthRequest';
import { GoogleBindRequest } from '../request/GoogleBindRequest';
import { TokenResponse } from '../response/TokenResponse';

@Controller('user/auth')
Expand All @@ -20,6 +23,7 @@ export class AuthController {
public constructor(
private readonly registrator: Registrator,
private readonly authenticator: Authenticator,
private readonly signInProvider: SignInProvider,
) {}

@PostNoCreate('sign-in')
Expand All @@ -29,7 +33,9 @@ export class AuthController {
public async signIn(@Body() request: AuthRequest): Promise<TokenResponse> {
const { email, password } = request;

return this.createResponseByCredentials(email, password);
const user = await this.signInProvider.signInByLogin(email, password);

return this.createResponse(user);
}

@Post('sign-up')
Expand All @@ -41,14 +47,25 @@ export class AuthController {

await this.registrator.signUp(email, password);

return this.createResponseByCredentials(email, password);
const user = await this.signInProvider.signInByLogin(email, password);

return this.createResponse(user);
}

private async createResponseByCredentials(
login: string,
password: string,
@Post('google')
@ApiOperation({ title: 'Sign-up (or sign-in) by google account' })
@ApiCreatedResponse({ description: 'Success', type: TokenResponse })
@ApiBadRequestResponse({ description: 'Invalid Google payload' })
async authByGoogle(
@Body() request: GoogleBindRequest,
): Promise<TokenResponse> {
const token = await this.authenticator.signIn(login, password);
const user = await this.signInProvider.signInByGoogle(request);

return this.createResponse(user);
}

private async createResponse(user: User): Promise<TokenResponse> {
const token = await this.authenticator.encode(user);

return {
token,
Expand Down
Loading

0 comments on commit 638b5b1

Please sign in to comment.