Passed
Push — master ( 00c0d4...3c958b )
by Leandro
01:29
created

AuthController.refresh   A

Complexity

Conditions 4

Size

Total Lines 27
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 23
dl 0
loc 27
rs 9.328
c 0
b 0
f 0
cc 4
1
import { Authorized, Body, Get, JsonController, NotAcceptableError, NotFoundError, Post, QueryParam } from "routing-controllers";
2
import { getCustomRepository, getRepository, Repository } from "typeorm";
3
import { validate } from "class-validator";
4
import { ValidationError } from "../errors/ValidationError";
5
import { comparePassword, generateHash, randomInt, randomString } from "../utils/hash";
6
import { User } from "../entities/User";
7
import { DateTime } from "luxon";
8
import { UserRepository } from "../repositories/UserRepository";
9
import { createJwt } from "../utils/jwt";
10
import { createEmail } from "../utils/email";
11
import { Credentials } from "../types/Credentials";
12
import { SignUp } from "../types/SignUp";
13
import { ResetPassword } from "../types/ResetPassword";
14
import { AuthToken } from "../types/AuthToken";
15
import { Token } from "../entities/Token";
16
import { RefreshToken } from "../types/RefreshToken";
17
import buildUrl from "build-url";
18
19
@JsonController("/auth")
20
export class AuthController {
21
22
  private userRepository: UserRepository;
23
  private tokenRepository: Repository<Token>;
24
25
  constructor () {
26
    this.userRepository = getCustomRepository(UserRepository);
27
    this.tokenRepository = getRepository(Token);
28
  }
29
30
  @Post("/")
31
  async signin(@Body({required: true}) data: Credentials): Promise<AuthToken> {
32
    const errors = await validate(data);
33
    if (errors.length ) {
34
      throw new ValidationError(errors);
35
    }
36
37
    const user = await this.findUser(data.username, true);
38
    if (!user.confirmedAt) {
39
      throw new NotAcceptableError("UserNotConfirmed");
40
    }
41
42
    const isValid = await comparePassword(data.password, user.password);
43
    if (!isValid) {
44
      throw new NotAcceptableError("InvalidUsernameAndPassword");
45
    }
46
47
    return this.createToken(user);
48
  }
49
50
  @Authorized()
51
  @Get("/exists")
52
  async exists(
53
    @QueryParam("username", {required: true}) username: string
54
  ): Promise<boolean> {
55
    const user = await this.findUser(username);
56
57
    if (user && !user.confirmedAt) {
58
      throw new NotAcceptableError("UserNotConfirmed");
59
    }
60
61
    return !!user;
62
  }
63
64
  @Authorized()
65
  @Post("/signup")
66
  async signup(@Body({required: true}) data: SignUp): Promise<null> {
67
    let user = await this.findUser(data.username);
68
69
    if (user) {
70
      throw new NotAcceptableError("UserAlreadyExists");
71
    }
72
73
    const errors = await validate(data);
74
    if (errors.length ) {
75
      throw new ValidationError(errors);
76
    }
77
78
    user = new User();
79
    user.name = data.name;
80
    user.username = data.username;
81
    user.password = await generateHash(data.password);
82
    user.apiKey = randomString();
83
    this.userRepository.save(user);
84
85
    await this.sendConfirmation(user);
86
    return null;
87
  }
88
89
  @Authorized()
90
  @Post("/confirmation")
91
  async confirmation(
92
    @QueryParam("username", {required: true}) username: string,
93
    @QueryParam("token", {required: true}) token: string
94
  ): Promise<null> {
95
    const user = await this.findUser(username, true);
96
97
    if (user.confirmedAt) {
98
      throw new NotAcceptableError("UserAlreadyConfirmed");
99
    }
100
101
    if (user.confirmationToken !== token) {
102
      throw new NotAcceptableError("InvalidToken");
103
    }
104
105
    const confirmationSentAt = DateTime.fromJSDate(user.confirmationSentAt);
106
    const diff = DateTime.now().diff(confirmationSentAt).shiftTo("hours");
107
    if (diff.hours > 3) {
108
      throw new NotAcceptableError("expiredConfirmationToken");
109
    }
110
111
    const repository = getRepository(User);
112
    user.confirmedAt = new Date();
113
    await repository.save(user);
114
    return null;
115
  }
116
117
  @Authorized()
118
  @Post("/resend")
119
  async resend(
120
    @QueryParam("username", {required: true}) username: string
121
  ): Promise<null> {
122
    const user = await this.findUser(username, true);
123
124
    if (user.confirmedAt) {
125
      throw new NotAcceptableError("UserAlreadyConfirmed");
126
    }
127
128
    await this.sendConfirmation(user);
129
    return null;
130
  }
131
132
  @Authorized()
133
  @Post("/recovery")
134
  async recovery(
135
    @QueryParam("username", {required: true}) username: string
136
  ): Promise<null> {
137
    const user = await this.findUser(username, true);
138
139
    user.resetToken = randomString(60);
140
    user.resetSentAt = new Date();
141
    await this.userRepository.save(user);
142
143
    const email = createEmail();
144
    email.send({
145
      message: {to: username},
146
      template: "account-recovery",
147
      locals: {
148
        name: user.name,
149
        link: buildUrl(process.env.APP_URL, {
150
          path: `/reset/${encodeURI(user.resetToken)}`,
151
        }),
152
      },
153
    });
154
155
    return null;
156
  }
157
158
  @Authorized()
159
  @Post("/reset")
160
  async reset(
161
    @Body({required: true}) data: ResetPassword,
162
  ): Promise<string> {
163
164
    const errors = await validate(data);
165
    if (errors.length ) {
166
      throw new ValidationError(errors);
167
    }
168
169
    const user = await this.userRepository.findOne({resetToken: data.token});
170
    if (!user) {
171
      throw new NotFoundError("TokenNotFound");
172
    }
173
174
    const resetSentAt = DateTime.fromJSDate(user.resetSentAt);
175
    const diff = DateTime.now().diff(resetSentAt).shiftTo("hours");
176
    if (diff.hours > 24) {
177
      throw new NotAcceptableError("TokenExpired");
178
    }
179
180
    user.password = await generateHash(data.password);
181
    this.userRepository.save(user);
182
183
    return user.username;
184
  }
185
186
  @Authorized()
187
  @Post("/refresh")
188
  async refresh(
189
    @Body({required: true}) data: RefreshToken,
190
  ): Promise<AuthToken> {
191
192
    const errors = await validate(data);
193
    if (errors.length) {
194
      throw new ValidationError(errors);
195
    }
196
197
    const token = await this.tokenRepository.findOne({token: data.token}, {
198
      relations: ["user"],
199
    });
200
201
    if (!token) {
202
      throw new NotFoundError("RefreshTokenNotFound");
203
    }
204
205
    const createdAt = DateTime.fromJSDate(token.createdAt);
206
    const diff = DateTime.now().diff(createdAt).shiftTo("minutes");
207
    if (diff.minutes > parseInt(process.env.JWT_EXPIRATION)) {
208
      throw new NotAcceptableError("RefreshTokenExpired");
209
    }
210
211
    return this.createToken(token.user);
212
  }
213
214
  private async createToken(user: User): Promise<AuthToken> {
215
    const refreshToken = randomString(64);
216
    const accessToken = createJwt({
217
      id: user.id,
218
      name: user.name,
219
      username: user.username,
220
    });
221
222
    this.tokenRepository.insert({
223
      token: refreshToken,
224
      user: user,
225
    });
226
227
    return {
228
      accessToken: accessToken,
229
      refreshToken: refreshToken,
230
    };
231
  }
232
233
  private async findUser(username: string, trhowException?: boolean): Promise<User> {
234
    const user = await this.userRepository.findOneByUsername(username);
235
236
    if (!user && trhowException) {
237
      throw new NotFoundError("UserNotFound");
238
    }
239
240
    return user;
241
  }
242
243
  private async sendConfirmation(user: User) {
244
    user.confirmationToken = randomInt(6);
245
    user.confirmationSentAt = new Date();
246
    await this.userRepository.save(user);
247
248
    const email = createEmail();
249
    email.send({
250
      message: {to: user.username},
251
      template: "account-confirmation",
252
      locals: {
253
        name: user.name,
254
        link: buildUrl(process.env.APP_URL, {
255
          path: "/confirm-signup",
256
          queryParams: {
257
            username: user.username,
258
            token: user.confirmationToken,
259
          },
260
        }),
261
      },
262
    });
263
  }
264
}
265