<?php
/**
* Created by PhpStorm.
* User: adria
* Date: 1/3/2019
* Time: 12:11 AM
*/
namespace App\Controller\Api;
use App\Controller\Response;
use App\Entity\ApiToken;
use App\Entity\UserFcmTokens;
use App\Entity\Users;
use App\Entity\UserVerificationTokens;
use App\Message\SendResetPasswordMessage;
use App\Message\SendVerifyAccountMessage;
use App\Helper\ConstantValues;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Psr\Log\LoggerInterface;
use OpenApi\Attributes as OA;
use Nelmio\ApiDocBundle\Annotation\Model;
class Auth extends AbstractController
{
use Response;
use \App\Controller\Request;
protected $translator;
protected $logger;
public function __construct(TranslatorInterface $translator, LoggerInterface $logger)
{
$this->translator = $translator;
$this->logger = $logger;
}
#[Route("/api/user/register", name: "register_user", methods: ["POST"]),
OA\Tag(name: "Access"),
OA\RequestBody(
content: new OA\JsonContent(
properties: [
new OA\Property(property: "email", type: "string"),
new OA\Property(property: "source", type: "string"),
new OA\Property(property: "lang", type: "string"),
new OA\Property(property: "tos_consent", type: "integer"),
new OA\Property(property: "gdpr_consent", type: "integer"),
new OA\Property(property: "firstname", type: "string"),
new OA\Property(property: "lastname", type: "string"),
new OA\Property(property: "mobile_phone", type: "string"),
new OA\Property(property: "password", type: "string")
]
)
),
OA\Response(
response: 200,
description: "successful register",
content: new OA\JsonContent(
properties: [
new OA\Property(property: "user",
ref: new Model(type: Users::class))
]
)
)
]
public function registerAction(
Request $request,
EntityManagerInterface $em,
UserPasswordHasherInterface $passwordHasher,
MessageBusInterface $bus
)
{
# get Request json
$content = $request->getContent();
# parse JSON to Array
$data = \json_decode($content, true);
$lang = $this->getLangFromData($data);
if (!isset($data['email']) || !$this->isValidEmail($data['email'])) {
return $this->errorJsonResponse(['response' => $this->translator->trans('Invalid email address', [], 'messages', $lang)]);
}
# get doctrine repository : AppUsers
$repository = $em->getRepository(Users::class);
# find if an user with this email exists
$user = $repository->findOneBy(['emailAddress' => $data['email']]);
# add new user if the email is not used
if (!$user) {
if (empty($data['tos_consent']) || empty($data['gdpr_consent'])) {
return $this->errorJsonResponse(
[
'response' => $this->translator->trans('Compliance not accepted', [], 'messages', $lang),
'errors' => ['tos_consent', 'gdpr_consent']
],
SymfonyResponse::HTTP_OK);
}
$user = new Users();
$user->setEmailAddress($data['email']);
$user->setUsername($data['email']);
$user->setFirstname(isset($data['firstname']) ? $data['firstname'] : "");
$user->setLastname(isset($data['lastname']) ? $data['lastname'] : "");
$user->setMobilePhone(isset($data['mobile_phone']) ? $data['mobile_phone'] : null);
$user->setFullname($user->getFirstname() . ' ' . $user->getLastname());
$user->setRegisterSource(isset($data['source']) ? $data['source'] : 'app');
$user->setCreatedAt(new \DateTime());
$user->setUpdatedAt(new \DateTime());
$user->setLang($lang);
$user->setActive(true);
$user->setUnitMeasure(1);
$user->setRemoved(false);
$user->setUserPoints(0);
$user->setHealthyMinutes(0);
$user->setEmailVerified(false);
$user->setTosConsentAt(new \DateTime());
$user->setGdprConsentAt(new \DateTime());
$encoded = $passwordHasher->hashPassword($user, $data['password']);
$user->setPassword($encoded);
$em->persist($user);
$em->flush();
$em->refresh($user);
# send verification email
$verificationCode = $em
->getRepository(UserVerificationTokens::class)
->generate($user->getId());
# preparing URLs
$verificationUrl = rtrim($this->getParameter('application_web_url'), '/')
.'/email_verification/'
.$verificationCode->getToken();
$wasNotMeUrl = $verificationUrl.'/was_not_me'.'?lang='.$lang;
$verificationUrl = $verificationUrl.'?lang='.$lang;
# sending email
$bus->dispatch(
(new SendVerifyAccountMessage)
->setBaseApiUrl($this->getParameter("application_api_url"))
->setFrom($this->getParameter('mailer_from_address'))
->setUser($user)
->setSubject($this->translator->trans('Email verification subject', [], 'messages', $lang))
->setView('emails/accountVerification' . ucfirst($lang) . '.html.twig')
->setVerificationUrl($verificationUrl)
->setWasNotMeUrl($wasNotMeUrl)
);
} else {
return $this->errorJsonResponse(['response' => $this->translator->trans('Email used', [], 'messages', $lang)]);
}
return $this->jsonResponse(['user' => $repository->getUser($user->getId())]);
}
#[Route("/api/user/sign_in", name: "sign_in", methods: ["POST"]),
OA\Tag(name: "Access"),
OA\RequestBody(
content: new OA\JsonContent(
properties: [
new OA\Property(property: "email", type: "string"),
new OA\Property(property: "source", type: "string"),
new OA\Property(property: "lang", type: "string"),
new OA\Property(property: "tos_consent", type: "integer"),
new OA\Property(property: "gdpr_consent", type: "integer"),
new OA\Property(property: "firstname", type: "string"),
new OA\Property(property: "lastname", type: "string"),
new OA\Property(property: "mobile_phone", type: "string"),
new OA\Property(property: "password", type: "string")
]
)
),
OA\Response(
response: 200,
description: "successful login",
content: new OA\JsonContent(
properties: [
new OA\Property(
property: "api_token",
type: "object",
properties: [
new OA\Property(property: "token", type: "string"),
new OA\Property(
property: "expiresAt", type: "datetime")
]
),
new OA\Property(property: "user",
ref: new Model(type: Users::class))
]
)
)
]
public function signInAction(
Request $request, EntityManagerInterface $em, UserPasswordHasherInterface $passwordHasher
)
{
# get Request json
$content = $request->getContent();
# parse JSON to Array
$data = \json_decode($content, true);
$lang = $this->getLangFromData($data);
if (!isset($data['email']) || !$this->isValidEmail($data['email'])) {
return $this->errorJsonResponse(['response' => $this->translator->trans('Invalid email address', [], 'messages', $lang)]);
}
# get doctrine repository : AppUsers
$repository = $em->getRepository(Users::class);
# find if an user with this email exists
$user = $repository->findOneBy(['emailAddress' => $data['email']]);
# sign in with FACEBOOK OR GMAIL
if (isset($data['source']) && in_array($data['source'], ['facebook', 'gmail', 'apple'])) {
if (!$user) {
if (empty($data['tos_consent']) || empty($data['gdpr_consent'])) {
return $this->errorJsonResponse(
[
'response' => $this->translator->trans('Compliance not accepted', [], 'messages', $lang),
'errors' => ['tos_consent', 'gdpr_consent']
],
SymfonyResponse::HTTP_NOT_FOUND);
}
$user = new Users();
$user->setEmailAddress($data['email']);
$user->setUsername($data['email']);
$user->setFirstname(isset($data['firstname']) ? $data['firstname'] : null);
$user->setLastname(isset($data['lastname']) ? $data['lastname'] : null);
$user->setMobilePhone(isset($data['mobile_phone']) ? $data['mobile_phone'] : null);
$user->setFullname($user->getFirstname() . ' ' . $user->getLastname());
$user->setRegisterSource(isset($data['source']) ? $data['source'] : 'app');
$user->setCreatedAt(new \DateTime());
$user->setUpdatedAt(new \DateTime());
$user->setLang($lang);
$user->setActive(true);
$user->setRemoved(false);
$user->setRegisterSource($data['source']);
$user->setUnitMeasure(1);
$user->setUserPoints(0);
$user->setHealthyMinutes(0);
$user->setEmailVerified(true);
$user->setTosConsentAt(new \DateTime());
$user->setGdprConsentAt(new \DateTime());
$encoded = $passwordHasher->hashPassword($user, (isset($data['password']) ? $data['password'] : 1234));
$user->setPassword($encoded);
$em->persist($user);
$em->flush();
}
} else {
if (!$user || !$passwordHasher->isPasswordValid($user, $data['password'])) {
return $this->errorJsonResponse(['response' => $this->translator->trans('Invalid credentials', [], 'messages', $lang)]);
}
}
$user->setLang($lang);
// not allow log-in of unverified addresses
if (!$user->getEmailVerified()) {
return $this->explicitErrorJsonResponse(
['response' => $this->translator->trans('Email verification error', [], 'messages', $lang)],
SymfonyResponse::HTTP_OK,
errors: [ConstantValues::ISSUE_EMAIL_NOT_VERIFIED],
);
}
// generate apiToken
$apiToken = new ApiToken(
$user,
new \DateTime(sprintf('+%d seconds', $this->getParameter('auth_token_expiration_seconds')))
);
$em->persist($apiToken);
$em->persist($user);
$em->flush();
# send status ok
return $this->jsonResponse([
'user' => $repository->getUser($user->getId()),
'api_token' => [
'token' => $apiToken->getToken(),
'expiresAt' => $apiToken->getExpiresAt(),
],
]);
}
/**
* @Route("/api/user/{id}/token", name="save_user_token", methods={"POST"})
*/
#[Route("/api/user/{id}/token",
name: "save_user_token", methods: ["POST"]),
OA\Tag(name: "Access"),
OA\RequestBody(
content: new OA\JsonContent(
properties: [
new OA\Property(property: "token", type: "string",
description: "user token"),
]
)
)
]
public function saveUserTokenAction($id, Request $request, EntityManagerInterface $em)
{
try {
$user = $this->getUser(); // TOKEN
if (!$user or ! $user instanceof Users) {
$type = is_null($user) ? 'null' : get_class($user);
return $this->errorJsonResponse(['message' => "User not found or invalid ($type)"]);
}
# get Request json
$content = $request->getContent();
# parse JSON to Array
$data = \json_decode($content, true);
$lang = $this->getLangFromData($data);
if (!isset($data['token']) || empty($data['token'])) {
return $this->errorJsonResponse(['response' => $this->translator->trans('Token not found', [], 'messages', $lang)]);
}
# get doctrine repository : AppUsers
$repository = $em->getRepository(UserFcmTokens::class);
# find if an user with this email exists
$token = $repository->findOneBy([
'token' => $data['token'],
]);
$token_exists = false;
if (!$token) {
$token = new UserFcmTokens();
$token->setUserId($user->getId());
$token->setRemoved(false);
$token->setToken($data['token']);
} else {
$token->setUserId($user->getId());
$token_exists = true;
}
$em->persist($token);
$em->flush();
} catch (\Exception $exception) {
$this->logger->error(json_encode([
'timestamp' => (new \DateTime())->format(\DateTime::ATOM),
'endpoint' => sprintf('api/user/%d/token', $id),
'error' => $exception->getMessage(),
'data' => $data,
'token_exists' => $token_exists
]));
}
# send status ok regardless
return $this->jsonResponse([]);
}
#[Route("/api/user/{id}/update", name: "update_user", methods: ["PUT"]),
OA\Tag(name: "Access"),
OA\RequestBody(
content: new OA\JsonContent(
properties: [
new OA\Property(property: "email", type: "string"),
new OA\Property(property: "lang", type: "string"),
new OA\Property(property: "tos_consent", type: "integer"),
new OA\Property(property: "gdpr_consent", type: "integer"),
new OA\Property(property: "firstname", type: "string"),
new OA\Property(property: "lastname", type: "string"),
new OA\Property(property: "mobile_phone", type: "string"),
new OA\Property(property: "password", type: "string"),
new OA\Property(property: "unit_measure", type: "integer")
]
)
),
OA\Response(
response: 200,
description: "successful update",
content: new OA\JsonContent(
properties: [
new OA\Property(property: "user",
ref: new Model(type: Users::class))
]
)
)
]
public function updateUserAction(
$id, Request $request, EntityManagerInterface $em, UserPasswordHasherInterface $passwordHasher
)
{
try {
$user = $this->getUser(); // TOKEN
if (!$user or ! $user instanceof Users) {
$type = is_null($user) ? 'null' : get_class($user);
return $this->errorJsonResponse(['message' => "User not found or invalid ($type)"]);
}
# get Request json
$content = $request->getContent();
# parse JSON to Array
$data = \json_decode($content, true);
$lang = $this->getLangFromData($data);
if (!isset($data['email']) || !$this->isValidEmail($data['email'])) {
return $this->errorJsonResponse(['response' => $this->translator->trans('Invalid email address', [], 'messages', $lang)]);
}
# get doctrine repository : AppUsers
$repository = $em->getRepository(Users::class);
if ($user) {
if ($user->getEmailAddress() !== $data['email']) {
# get doctrine repository : AppUsers
$checkUser = $em->createQueryBuilder()
->select('u')
->from(Users::class, 'u')
->where('u.id != :id AND u.emailAddress = :email')
->setParameter('id', $id)
->setParameter('email', $data['email'])
->getQuery()
->getResult();
if ($checkUser) {
return $this->errorJsonResponse(['response' => $this->translator->trans('Email used', [], 'messages', $lang)]);
}
}
$user->setEmailAddress($data['email']);
$user->setUsername($data['email']);
$user->setFirstname(isset($data['firstname']) ? $data['firstname'] : $user->getFirstname());
$user->setLastname(isset($data['lastname']) ? $data['lastname'] : $user->getLastname());
$user->setMobilePhone(isset($data['mobile_phone']) ? $data['mobile_phone'] : $user->getMobilePhone());
$user->setFullname($user->getFirstname() . ' ' . $user->getLastname());
$user->setLang($lang);
$user->setUnitMeasure($data['unit_measure'] ?? 1);
if (!empty($data['tos_consent'])) {
$user->setTosConsentAt(new \DateTime());
}
if (!empty($data['gdpr_consent'])) {
$user->setGdprConsentAt(new \DateTime());
}
if (!empty($data['password'])) {
$encoded = $passwordHasher->hashPassword($user, $data['password']);
$user->setPassword($encoded);
}
$em->persist($user);
$em->flush();
# send status ok
return $this->jsonResponse([
'status' => 'OK',
'user' => $repository->getUser($user->getId()),
]);
} else {
return $this->errorJsonResponse(['response' => $this->translator->trans('User not found', [], 'messages', $lang)]);
}
} catch (\Exception $exception) {
return new JsonResponse([
'status' => 'error',
'message' => $exception->getMessage(),
], 200);
}
}
#[Route("/api/user/reset_password",
name: "reset_user_password", methods: ["POST"]),
OA\Tag(name: "Access"),
OA\RequestBody(
content: new OA\JsonContent(
properties: [
new OA\Property(property: "email_address", type: "string",
description: "user email address"),
new OA\Property(property: "lang", type: "string",
description: "language code")
]
)
)
]
public function resetPasswordAction(
Request $request,
UserPasswordHasherInterface $passwordHasher,
EntityManagerInterface $em,
MessageBusInterface $bus
)
{
try {
# get Request json
$content = $request->getContent();
# parse JSON to Array
$data = \json_decode($content, true);
$lang = $this->getLangFromData($data);
if (!isset($data['email']) || !$this->isValidEmail($data['email'])) {
return $this->errorJsonResponse(['response' => $this->translator->trans('Invalid email address', [], 'messages', $lang)]);
}
# get doctrine repository : AppUsers
$repository = $em->getRepository(Users::class);
# find if an user with this email exists
$user = $repository->findOneBy([
'emailAddress' => $data['email'],
]);
if ($user) {
$newPassword = \bin2hex(\random_bytes(5));
$encoded = $passwordHasher->hashPassword($user, $newPassword);
$user->setPassword($encoded);
$em->persist($user);
$em->flush();
$bus->dispatch(
(new SendResetPasswordMessage)
->setBaseApiUrl($this->getParameter("application_api_url"))
->setFrom($this->getParameter('mailer_from_address'))
->setUser($user)
->setSubject($this->translator->trans('Reset password', [], 'messages', $lang))
->setView('emails/resetPassword' . ucfirst($lang) . '.html.twig')
->setPassword($newPassword)
);
} else {
return $this->errorJsonResponse(['response' => $this->translator->trans('User not found', [], 'messages', $lang)]);
}
# send status ok
return $this->jsonResponse([]);
} catch (\Exception $exception) {
return new JsonResponse([
'status' => 'error',
'message' => $exception->getMessage(),
], 200);
}
}
#[Route("/api/user/{id}", name: "get_user_details", methods: ["GET"]),
OA\Tag(name: "Access"),
OA\Response(
response: 200,
description: "successful fetch",
content: new OA\JsonContent(
properties: [
new OA\Property(property: "user",
ref: new Model(type: Users::class))
]
)
)
]
public function getUserAction($id, EntityManagerInterface $em)
{
$user = $this->getUser(); // TOKEN
if (!$user or ! $user instanceof Users) {
$type = is_null($user) ? 'null' : get_class($user);
return $this->errorJsonResponse(['message' => "User not found or invalid ($type)"]);
}
# get doctrine repository : AppUsers
$repository = $em->getRepository(Users::class);
# send status ok
return $this->jsonResponse(['user' => $repository->getUser($user->getId())]);
}
}