chore: manually committing auth module to remoter

This commit is contained in:
Balu Babu
2023-01-09 19:02:14 +05:30
parent 90bc0483ae
commit 0c154be04e
16 changed files with 468 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
import { Body, Controller, Get, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
import { signInMagicDto } from './dto/signin-magic.dto';
import { verifyMagicDto } from './dto/verify-magic.dto';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('signin')
async signIn(@Body() authData: signInMagicDto) {
return this.authService.signIn(authData.email);
}
}

View File

@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UserModule } from 'src/user/user.module';
import { MailerModule } from 'src/mailer/mailer.module';
import { PrismaModule } from 'src/prisma/prisma.module';
@Module({
imports: [PrismaModule, UserModule, MailerModule],
providers: [AuthService],
controllers: [AuthController],
})
export class AuthModule {}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';
describe('AuthService', () => {
let service: AuthService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthService],
}).compile();
service = module.get<AuthService>(AuthService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,68 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { MailerService } from 'src/mailer/mailer.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { User } from 'src/user/user.model';
import { UserService } from 'src/user/user.service';
import { verifyMagicDto } from './dto/verify-magic.dto';
import { DateTime } from 'luxon';
import * as argon2 from 'argon2';
import bcrypt from 'bcrypt';
import * as O from 'fp-ts/Option';
import * as E from 'fp-ts/Either';
import * as TE from 'fp-ts/TaskEither';
import { PasswordlessToken } from 'src/types/Passwordless';
import { EmailCodec } from 'src/types/Email';
import { INVALID_EMAIL } from 'src/errors';
import { pipe } from 'fp-ts/lib/function';
import { validateEmail } from 'src/utils';
@Injectable()
export class AuthService {
constructor(
private usersService: UserService,
private prismaService: PrismaService,
private readonly mailerService: MailerService,
) {}
// generate Id and token for email magiclink
private async generatePasswordlessTokens(user: User) {
const salt = await bcrypt.genSalt(10);
const expiresOn = DateTime.now().plus({ hours: 3 }).toISO().toString();
const idToken: PasswordlessToken =
await this.prismaService.passwordlessVerification.create({
data: {
expiresOn: expiresOn,
deviceIdentifier: salt,
userUid: user.id,
},
});
return idToken;
}
async signIn(email: string) {
if (!validateEmail(email)) return E.left(INVALID_EMAIL);
let user: User;
const queriedUser = await this.usersService.findUserByEmail(email);
if (O.isNone(queriedUser)) {
user = await this.usersService.createUser(email);
} else {
user = queriedUser.value;
}
const generatedTokens = await this.generatePasswordlessTokens(user);
this.mailerService.sendMail(email, {
template: 'code-your-own',
variables: {
inviteeEmail: email,
magicLink: `${process.env.APP_DOMAIN}/magic-link?token=${generatedTokens.token}`,
},
});
return { deviceIdentifier: generatedTokens.deviceIdentifier };
}
}

View File

@@ -0,0 +1,3 @@
export class refreshTokensDto {
refresh_token: string;
}

View File

@@ -0,0 +1,3 @@
export class signInMagicDto {
email: string;
}

View File

@@ -0,0 +1,4 @@
export class verifyMagicDto {
identifier: string;
token: string;
}

View File

@@ -0,0 +1,16 @@
export type MailDescription = {
template: 'team-invitation';
variables: {
invitee: string;
invite_team_name: string;
action_url: string;
};
};
export type UserMagicLinkMailDescription = {
template: 'code-your-own'; //Alias of template in Postmark, change this to env variable
variables: {
inviteeEmail: string;
magicLink: string;
};
};

View File

@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { MailerService } from './mailer.service';
@Module({
providers: [MailerService],
exports: [MailerService],
})
export class MailerModule {}

View File

@@ -0,0 +1,40 @@
import { Injectable, OnModuleInit } from '@nestjs/common';
import { Email } from 'src/types/Email';
import {
MailDescription,
UserMagicLinkMailDescription,
} from './MailDescriptions';
import * as postmark from 'postmark';
import { throwErr } from 'src/utils';
import * as TE from 'fp-ts/TaskEither';
import { EMAIL_FAILED } from 'src/errors';
@Injectable()
export class MailerService implements OnModuleInit {
client: postmark.ServerClient;
onModuleInit() {
this.client = new postmark.ServerClient(
process.env.POSTMARK_SERVER_TOKEN ||
throwErr('No Postmark Server Token defined'),
);
}
sendMail(
to: string,
mailDesc: MailDescription | UserMagicLinkMailDescription,
) {
return TE.tryCatch(
() =>
this.client.sendEmailWithTemplate({
To: to,
From:
process.env.POSTMARK_SENDER_EMAIL ||
throwErr('No Postmark Sender Email defined'),
TemplateAlias: mailDesc.template,
TemplateModel: mailDesc.variables,
}),
() => EMAIL_FAILED,
);
}
}

View File

@@ -0,0 +1,17 @@
import * as t from 'io-ts';
interface EmailBrand {
readonly Email: unique symbol;
}
// The validation branded type for an email
export const EmailCodec = t.brand(
t.string,
(x): x is t.Branded<string, EmailBrand> =>
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
x,
),
'Email',
);
export type Email = t.TypeOf<typeof EmailCodec>;

View File

@@ -0,0 +1,9 @@
import { User } from 'src/user/user.model';
export interface PasswordlessToken {
deviceIdentifier: string;
token: string;
userUid: string;
user?: User;
expiresOn: Date;
}

View File

@@ -0,0 +1,72 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
import * as O from 'fp-ts/Option';
import { User } from './user.model';
@Injectable()
export class UserService {
constructor(private prisma: PrismaService) {}
async findUserByEmail(email: string) {
try {
const user: User = await this.prisma.user.findUniqueOrThrow({
where: {
email: email,
},
});
return O.some(user);
} catch (error) {
return O.none;
}
}
async createUser(email: string) {
const createdUser = await this.prisma.user.create({
data: {
email: email,
accounts: {
create: {
provider: 'magic',
providerAccountId: email,
},
},
},
});
return createdUser;
}
async createUserSSO(accessToken, refreshToken, profile) {
const createdUser = await this.prisma.user.create({
data: {
name: profile.displayName,
email: profile.emails[0].value,
image: profile.photos[0].value,
accounts: {
create: {
provider: profile.provider,
providerAccountId: profile.id,
providerRefreshToken: refreshToken,
providerAccessToken: accessToken,
},
},
},
});
return createdUser;
}
async createProviderAccount(user, accessToken, refreshToken, profile) {
const createdProvider = await this.prisma.account.create({
data: {
userId: user.id,
provider: profile.provider,
providerAccountId: profile.id,
providerRefreshToken: refreshToken ? refreshToken : null,
providerAccessToken: accessToken ? accessToken : null,
},
});
return createdProvider;
}
}