24
24
25
25
package org .hatdex .hat .api .controllers
26
26
27
- import java .net .URLDecoder
28
- import javax .inject .Inject
27
+ import java .net .{ URLDecoder , URLEncoder }
29
28
29
+ import akka .Done
30
+ import javax .inject .Inject
30
31
import com .mohiva .play .silhouette .api .repositories .AuthInfoRepository
31
32
import com .mohiva .play .silhouette .api .util .{ Credentials , PasswordHasherRegistry }
32
33
import com .mohiva .play .silhouette .api .{ LoginEvent , Silhouette }
33
34
import com .mohiva .play .silhouette .impl .exceptions .{ IdentityNotFoundException , InvalidPasswordException }
34
35
import com .mohiva .play .silhouette .impl .providers .CredentialsProvider
35
36
import org .hatdex .hat .api .json .HatJsonFormats
36
37
import org .hatdex .hat .api .models ._
37
- import org .hatdex .hat .api .service .{ HatServicesService , MailTokenService , UsersService }
38
+ import org .hatdex .hat .api .service .applications .ApplicationsService
39
+ import org .hatdex .hat .api .service .{ HatServicesService , LogService , MailTokenService , UsersService }
38
40
import org .hatdex .hat .authentication ._
39
- import org .hatdex .hat .phata .models .{ ApiPasswordChange , ApiPasswordResetRequest , MailTokenUser }
41
+ import org .hatdex .hat .phata .models ._
40
42
import org .hatdex .hat .resourceManagement .{ HatServerProvider , _ }
41
43
import org .hatdex .hat .utils .{ HatBodyParsers , HatMailer }
42
- import play .api .Logger
44
+ import play .api .{ Configuration , Logger }
43
45
import play .api .cache .{ Cached , CachedBuilder }
44
46
import play .api .libs .json .Json
45
- import play .api .mvc ._
47
+ import play .api .libs .ws .WSClient
48
+ import play .api .mvc .{ Action , _ }
46
49
47
50
import scala .concurrent .ExecutionContext .Implicits .global
48
51
import scala .concurrent .Future
49
52
50
53
class Authentication @ Inject () (
51
54
components : ControllerComponents ,
52
55
cached : Cached ,
56
+ configuration : Configuration ,
53
57
parsers : HatBodyParsers ,
54
58
hatServerProvider : HatServerProvider ,
55
59
silhouette : Silhouette [HatApiAuthEnvironment ],
@@ -58,8 +62,11 @@ class Authentication @Inject() (
58
62
passwordHasherRegistry : PasswordHasherRegistry ,
59
63
authInfoRepository : AuthInfoRepository [HatServer ],
60
64
usersService : UsersService ,
65
+ applicationsService : ApplicationsService ,
66
+ logService : LogService ,
61
67
mailer : HatMailer ,
62
68
tokenService : MailTokenService [MailTokenUser ],
69
+ wsClient : WSClient ,
63
70
limiter : UserLimiter ) extends HatApiController (components, silhouette) with HatJsonFormats {
64
71
65
72
private val logger = Logger (this .getClass)
@@ -223,4 +230,136 @@ class Authentication @Inject() (
223
230
Future .successful(Unauthorized (Json .toJson(ErrorMessage (" Invalid Token" , " Token does not exist" ))))
224
231
}
225
232
}
233
+
234
+ /**
235
+ * Sends an email to the owner with a link to claim the hat
236
+ */
237
+ def handleClaimStart (): Action [ApiClaimHatRequest ] = UserAwareAction .async(parsers.json[ApiClaimHatRequest ]) { implicit request =>
238
+
239
+ val claimHatRequest = request.body
240
+ val email = request.dynamicEnvironment.ownerEmail
241
+ val response = Ok (Json .toJson(SuccessResponse (" You will shortly receive an email with claim instructions" )))
242
+
243
+ if (claimHatRequest.email == email) {
244
+ usersService.listUsers.map(_.find(_.roles.contains(Owner ()))).flatMap {
245
+ case Some (user) =>
246
+ applicationsService.applicationStatus()(request.dynamicEnvironment, user, request).flatMap { applications =>
247
+ val maybeApplication = applications.find(_.application.id.equals(claimHatRequest.applicationId))
248
+ val maybeAppDetails = maybeApplication.map { app =>
249
+ ((app.application.developer.name, app.application.developer.logo.map(_.normal).getOrElse(" #" )),
250
+ (app.application.id, app.application.info.version.toString))
251
+ }
252
+
253
+ val scheme = if (request.secure) {
254
+ " https://"
255
+ }
256
+ else {
257
+ " http://"
258
+ }
259
+
260
+ tokenService.retrieve(email, isSignup = true ).flatMap {
261
+ case Some (existingTokenUser) if ! existingTokenUser.isExpired =>
262
+ val claimLink = s " $scheme${request.host}/#/hat/claim/ ${existingTokenUser.id}?email= ${URLEncoder .encode(email, " UTF-8" )}"
263
+ mailer.claimHat(email, claimLink, maybeAppDetails.map(_._1))
264
+
265
+ Future .successful(response)
266
+ case Some (_) => Future .successful(Ok (Json .toJson(SuccessResponse (" The HAT is already claimed" ))))
267
+
268
+ case None =>
269
+ val token = MailClaimTokenUser (email)
270
+
271
+ val eventualResult = for {
272
+ _ <- tokenService.create(token)
273
+ _ <- logService
274
+ .logAction(request.dynamicEnvironment.domain, LogRequest (" unclaimed" , None , None ), maybeAppDetails.map(_._2))
275
+ .recover {
276
+ case e =>
277
+ logger.error(s " LogActionError::unclaimed. Reason: ${e.getMessage}" )
278
+ Done
279
+ }
280
+ } yield {
281
+
282
+ val claimLink = s " $scheme${request.host}/#/hat/claim/ ${token.id}?email= ${URLEncoder .encode(email, " UTF-8" )}"
283
+ mailer.claimHat(email, claimLink, maybeAppDetails.map(_._1))
284
+
285
+ response
286
+ }
287
+
288
+ eventualResult.recover {
289
+ case e =>
290
+ logger.error(s " Could not create new HAT claim token. Reason: ${e.getMessage}" )
291
+ InternalServerError (Json .toJson(ErrorMessage (" Internal Server Error" , " Failed to initialize HAT claim process" )))
292
+ }
293
+ }
294
+ }
295
+ case None => Future .successful(response)
296
+ }
297
+ }
298
+ else {
299
+ Future .successful(response)
300
+ }
301
+ }
302
+
303
+ def handleClaimComplete (claimToken : String ): Action [HatClaimCompleteRequest ] = UserAwareAction .async(parsers.json[HatClaimCompleteRequest ]) { implicit request =>
304
+ implicit val hatClaimComplete : HatClaimCompleteRequest = request.body
305
+
306
+ tokenService.retrieve(claimToken).flatMap {
307
+ case Some (token) if token.isSignUp && ! token.isExpired && token.email == request.dynamicEnvironment.ownerEmail =>
308
+ usersService.listUsers.map(_.find(_.roles.contains(Owner ()))).flatMap {
309
+ case Some (user) =>
310
+ val eventualResult = for {
311
+ _ <- updateHatMembership(hatClaimComplete)
312
+ _ <- authInfoRepository.update(user.loginInfo, passwordHasherRegistry.current.hash(request.body.password))
313
+ _ <- tokenService.expire(token.id)
314
+ authenticator <- env.authenticatorService.create(user.loginInfo)
315
+ result <- env.authenticatorService.renew(authenticator, Ok (Json .toJson(SuccessResponse (" HAT claimed" ))))
316
+ _ <- logService.logAction(request.dynamicEnvironment.domain, LogRequest (" claimed" , None , None ), None ).recover {
317
+ case e =>
318
+ logger.error(s " LogActionError::unclaimed. Reason: ${e.getMessage}" )
319
+ Done
320
+ }
321
+ } yield {
322
+ // env.eventBus.publish(LoginEvent(user, request))
323
+ // mailer.passwordChanged(token.email, user)
324
+
325
+ result
326
+ }
327
+
328
+ eventualResult.recover {
329
+ case e =>
330
+ logger.error(s " HAT claim process failed with error ${e.getMessage}" )
331
+ BadRequest (Json .toJson(ErrorMessage (" Bad Request" , " HAT claim process failed" )))
332
+ }
333
+
334
+ case None => Future .successful(Unauthorized (Json .toJson(ErrorMessage (" HAT claim unauthorized" , " No user matching token" ))))
335
+ }
336
+
337
+ case Some (_) =>
338
+ Future .successful(Unauthorized (Json .toJson(ErrorMessage (" Invalid Token" , " Token expired or invalid" ))))
339
+
340
+ case None =>
341
+ Future .successful(Unauthorized (Json .toJson(ErrorMessage (" Invalid Token" , " Token does not exist" ))))
342
+ }
343
+ }
344
+
345
+ private def updateHatMembership (claim : HatClaimCompleteRequest ): Future [Done ] = {
346
+ val path = " api/products/hat/claim"
347
+ val hattersUrl = s " ${configuration.underlying.getString(" hatters.scheme" )}${configuration.underlying.getString(" hatters.address" )}"
348
+
349
+ logger.info(s " Proxy POST request to $hattersUrl/ $path with parameters: $claim" )
350
+
351
+ val futureResponse = wsClient.url(s " $hattersUrl/ $path" )
352
+ // .withHttpHeaders("x-auth-token" → token.accessToken)
353
+ .post(Json .toJson(claim.copy(password = " " )))
354
+
355
+ futureResponse.flatMap { response =>
356
+ response.status match {
357
+ case OK =>
358
+ Future .successful(Done )
359
+ case _ =>
360
+ logger.error(s " Failed to claim HAT with Hatters. Claim details: \n $claim\n Hatters response: ${response.body}" )
361
+ Future .failed(new UnknownError (" HAT claim failed" ))
362
+ }
363
+ }
364
+ }
226
365
}
0 commit comments