Auth #28

Merged
Sieciech merged 3 commits from 21-auth into dev 2021-08-04 08:23:05 +00:00
57 changed files with 11555 additions and 10182 deletions

View File

@ -19,9 +19,7 @@ services:
- ./logs:/var/log - ./logs:/var/log
- vendor:/web/backend/vendor - vendor:/web/backend/vendor
- var:/web/backend/var - var:/web/backend/var
- nodemodules:/web/frontend/node_modules
volumes: volumes:
nodemodules: nodemodules:
vendor: vendor:
var: var:
nodemodules:

View File

@ -1,8 +1,7 @@
#!/bin/bash #!/bin/bash
echo "> Configure cache" echo "> Configure cache"
mkdir -p /tmp/app/{vendor,node_modules} mkdir -p /tmp/app/vendor
ln -s /tmp/app/node_modules /web/frontend/node_modules
ln -s /tmp/app/vendor /web/backend/vendor ln -s /tmp/app/vendor /web/backend/vendor
echo "> Configure user" echo "> Configure user"
@ -46,10 +45,14 @@ echo "> Configure symfony"
cd /web/backend cd /web/backend
composer install composer install
php bin/console doctrine:schema:update --force php bin/console doctrine:schema:update --force
php php bin/console lexik:jwt:generate-keypair --skip-if-exists
echo "> Configure angular" echo "> Configure angular"
cd /web/frontend cd /web/frontend
su developer -c 'time npm ci' time su developer -c 'npm ci'
echo "> Starting angular" echo "> Starting angular"
su developer -c 'ng serve --disable-host-check --host 0.0.0.0 --poll' su developer -c 'ng serve --disable-host-check --host 0.0.0.0 --poll'
echo "> Waiting"
sleep Infinity

View File

@ -30,3 +30,9 @@ APP_SECRET=0fc8d6b67b9f1100b3eb3e3c80d36fda
# DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7" # DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7"
DATABASE_URL="postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=13&charset=utf8" DATABASE_URL="postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=13&charset=utf8"
###< doctrine/doctrine-bundle ### ###< doctrine/doctrine-bundle ###
###> lexik/jwt-authentication-bundle ###
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
JWT_PASSPHRASE=8270442e040a48fd42967bf1690d5dba
###< lexik/jwt-authentication-bundle ###

View File

@ -18,3 +18,7 @@
/phpunit.xml /phpunit.xml
.phpunit.result.cache .phpunit.result.cache
###< phpunit/phpunit ### ###< phpunit/phpunit ###
###> lexik/jwt-authentication-bundle ###
/config/jwt/*.pem
###< lexik/jwt-authentication-bundle ###

View File

@ -12,8 +12,10 @@
"doctrine/doctrine-bundle": "^2.4", "doctrine/doctrine-bundle": "^2.4",
"doctrine/doctrine-migrations-bundle": "^3.1", "doctrine/doctrine-migrations-bundle": "^3.1",
"doctrine/orm": "^2.9", "doctrine/orm": "^2.9",
"lexik/jwt-authentication-bundle": "^2.12",
"phpdocumentor/reflection-docblock": "^5.2", "phpdocumentor/reflection-docblock": "^5.2",
"sensio/framework-extra-bundle": "^6.1", "sensio/framework-extra-bundle": "^6.1",
"symfony-bundles/json-request-bundle": "^4.0",
"symfony/asset": "5.3.*", "symfony/asset": "5.3.*",
"symfony/console": "5.3.*", "symfony/console": "5.3.*",
"symfony/dotenv": "5.3.*", "symfony/dotenv": "5.3.*",

View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "e3fa1ece831d58dd0515e0413472408f", "content-hash": "7f30e61383bffabaf547a9160e26d9c1",
"packages": [ "packages": [
{ {
"name": "composer/package-versions-deprecated", "name": "composer/package-versions-deprecated",
@ -1653,6 +1653,252 @@
], ],
"time": "2021-07-09T11:58:40+00:00" "time": "2021-07-09T11:58:40+00:00"
}, },
{
"name": "lcobucci/clock",
"version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/lcobucci/clock.git",
"reference": "353d83fe2e6ae95745b16b3d911813df6a05bfb3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/lcobucci/clock/zipball/353d83fe2e6ae95745b16b3d911813df6a05bfb3",
"reference": "353d83fe2e6ae95745b16b3d911813df6a05bfb3",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"require-dev": {
"infection/infection": "^0.17",
"lcobucci/coding-standard": "^6.0",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^0.12",
"phpstan/phpstan-deprecation-rules": "^0.12",
"phpstan/phpstan-phpunit": "^0.12",
"phpstan/phpstan-strict-rules": "^0.12",
"phpunit/php-code-coverage": "9.1.4",
"phpunit/phpunit": "9.3.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Lcobucci\\Clock\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Luís Cobucci",
"email": "lcobucci@gmail.com"
}
],
"description": "Yet another clock abstraction",
"support": {
"issues": "https://github.com/lcobucci/clock/issues",
"source": "https://github.com/lcobucci/clock/tree/2.0.x"
},
"funding": [
{
"url": "https://github.com/lcobucci",
"type": "github"
},
{
"url": "https://www.patreon.com/lcobucci",
"type": "patreon"
}
],
"time": "2020-08-27T18:56:02+00:00"
},
{
"name": "lcobucci/jwt",
"version": "4.1.4",
"source": {
"type": "git",
"url": "https://github.com/lcobucci/jwt.git",
"reference": "71cf170102c8371ccd933fa4df6252086d144de6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/lcobucci/jwt/zipball/71cf170102c8371ccd933fa4df6252086d144de6",
"reference": "71cf170102c8371ccd933fa4df6252086d144de6",
"shasum": ""
},
"require": {
"ext-hash": "*",
"ext-json": "*",
"ext-mbstring": "*",
"ext-openssl": "*",
"ext-sodium": "*",
"lcobucci/clock": "^2.0",
"php": "^7.4 || ^8.0"
},
"require-dev": {
"infection/infection": "^0.21",
"lcobucci/coding-standard": "^6.0",
"mikey179/vfsstream": "^1.6.7",
"phpbench/phpbench": "^1.0@alpha",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^0.12",
"phpstan/phpstan-deprecation-rules": "^0.12",
"phpstan/phpstan-phpunit": "^0.12",
"phpstan/phpstan-strict-rules": "^0.12",
"phpunit/php-invoker": "^3.1",
"phpunit/phpunit": "^9.5"
},
"type": "library",
"autoload": {
"psr-4": {
"Lcobucci\\JWT\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Luís Cobucci",
"email": "lcobucci@gmail.com",
"role": "Developer"
}
],
"description": "A simple library to work with JSON Web Token and JSON Web Signature",
"keywords": [
"JWS",
"jwt"
],
"support": {
"issues": "https://github.com/lcobucci/jwt/issues",
"source": "https://github.com/lcobucci/jwt/tree/4.1.4"
},
"funding": [
{
"url": "https://github.com/lcobucci",
"type": "github"
},
{
"url": "https://www.patreon.com/lcobucci",
"type": "patreon"
}
],
"time": "2021-03-23T23:53:08+00:00"
},
{
"name": "lexik/jwt-authentication-bundle",
"version": "v2.12.3",
"source": {
"type": "git",
"url": "https://github.com/lexik/LexikJWTAuthenticationBundle.git",
"reference": "7fb85afb1a63bb7e518a369baa355599f822ff43"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/lexik/LexikJWTAuthenticationBundle/zipball/7fb85afb1a63bb7e518a369baa355599f822ff43",
"reference": "7fb85afb1a63bb7e518a369baa355599f822ff43",
"shasum": ""
},
"require": {
"ext-openssl": "*",
"lcobucci/jwt": "^3.4|^4.0",
"namshi/jose": "^7.2",
"php": ">=7.1",
"symfony/deprecation-contracts": "^2.4",
"symfony/framework-bundle": "^4.4|^5.1",
"symfony/security-bundle": "^4.4|^5.1"
},
"require-dev": {
"symfony/browser-kit": "^4.4|^5.1",
"symfony/console": "^4.4|^5.1",
"symfony/dom-crawler": "^4.4|^5.1",
"symfony/phpunit-bridge": "^4.4|^5.1",
"symfony/var-dumper": "^4.4|^5.1",
"symfony/yaml": "^4.4|^5.1"
},
"suggest": {
"gesdinet/jwt-refresh-token-bundle": "Implements a refresh token system over Json Web Tokens in Symfony",
"spomky-labs/lexik-jose-bridge": "Provides a JWT Token encoder with encryption support"
},
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-master": "2.x-dev"
}
},
"autoload": {
"psr-4": {
"Lexik\\Bundle\\JWTAuthenticationBundle\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jeremy Barthe",
"email": "j.barthe@lexik.fr",
"homepage": "https://github.com/jeremyb"
},
{
"name": "Nicolas Cabot",
"email": "n.cabot@lexik.fr",
"homepage": "https://github.com/slashfan"
},
{
"name": "Cedric Girard",
"email": "c.girard@lexik.fr",
"homepage": "https://github.com/cedric-g"
},
{
"name": "Dev Lexik",
"email": "dev@lexik.fr",
"homepage": "https://github.com/lexik"
},
{
"name": "Robin Chalas",
"email": "robin.chalas@gmail.com",
"homepage": "https://github.com/chalasr"
},
{
"name": "Lexik Community",
"homepage": "https://github.com/lexik/LexikJWTAuthenticationBundle/graphs/contributors"
}
],
"description": "This bundle provides JWT authentication for your Symfony REST API",
"homepage": "https://github.com/lexik/LexikJWTAuthenticationBundle",
"keywords": [
"Authentication",
"JWS",
"api",
"bundle",
"jwt",
"rest",
"symfony"
],
"support": {
"issues": "https://github.com/lexik/LexikJWTAuthenticationBundle/issues",
"source": "https://github.com/lexik/LexikJWTAuthenticationBundle/tree/v2.12.3"
},
"funding": [
{
"url": "https://github.com/chalasr",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/lexik/jwt-authentication-bundle",
"type": "tidelift"
}
],
"time": "2021-07-07T09:06:05+00:00"
},
{ {
"name": "monolog/monolog", "name": "monolog/monolog",
"version": "2.3.1", "version": "2.3.1",
@ -1749,6 +1995,73 @@
], ],
"time": "2021-07-14T11:56:39+00:00" "time": "2021-07-14T11:56:39+00:00"
}, },
{
"name": "namshi/jose",
"version": "7.2.3",
"source": {
"type": "git",
"url": "https://github.com/namshi/jose.git",
"reference": "89a24d7eb3040e285dd5925fcad992378b82bcff"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/namshi/jose/zipball/89a24d7eb3040e285dd5925fcad992378b82bcff",
"reference": "89a24d7eb3040e285dd5925fcad992378b82bcff",
"shasum": ""
},
"require": {
"ext-date": "*",
"ext-hash": "*",
"ext-json": "*",
"ext-pcre": "*",
"ext-spl": "*",
"php": ">=5.5",
"symfony/polyfill-php56": "^1.0"
},
"require-dev": {
"phpseclib/phpseclib": "^2.0",
"phpunit/phpunit": "^4.5|^5.0",
"satooshi/php-coveralls": "^1.0"
},
"suggest": {
"ext-openssl": "Allows to use OpenSSL as crypto engine.",
"phpseclib/phpseclib": "Allows to use Phpseclib as crypto engine, use version ^2.0."
},
"type": "library",
"autoload": {
"psr-4": {
"Namshi\\JOSE\\": "src/Namshi/JOSE/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Alessandro Nadalin",
"email": "alessandro.nadalin@gmail.com"
},
{
"name": "Alessandro Cinelli (cirpo)",
"email": "alessandro.cinelli@gmail.com"
}
],
"description": "JSON Object Signing and Encryption library for PHP.",
"keywords": [
"JSON Web Signature",
"JSON Web Token",
"JWS",
"json",
"jwt",
"token"
],
"support": {
"issues": "https://github.com/namshi/jose/issues",
"source": "https://github.com/namshi/jose/tree/master"
},
"time": "2016-12-05T07:27:31+00:00"
},
{ {
"name": "phpdocumentor/reflection-common", "name": "phpdocumentor/reflection-common",
"version": "2.2.0", "version": "2.2.0",
@ -2235,6 +2548,68 @@
}, },
"time": "2021-05-31T10:40:46+00:00" "time": "2021-05-31T10:40:46+00:00"
}, },
{
"name": "symfony-bundles/json-request-bundle",
"version": "4.0.5",
"source": {
"type": "git",
"url": "https://github.com/symfony-bundles/json-request-bundle.git",
"reference": "53af1ff1df5457ea16d18cebab1309e667ea86c4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony-bundles/json-request-bundle/zipball/53af1ff1df5457ea16d18cebab1309e667ea86c4",
"reference": "53af1ff1df5457ea16d18cebab1309e667ea86c4",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": ">=7.4 || ^8.0",
"symfony/framework-bundle": "^5.0"
},
"require-dev": {
"doctrine/annotations": "^1.0",
"phpstan/phpstan": "^0.12",
"phpunit/php-code-coverage": "^9.2",
"phpunit/phpunit": "^9.5",
"symfony/browser-kit": "^5.2",
"symfony/yaml": "^5.2"
},
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-master": "4.x-dev"
}
},
"autoload": {
"psr-4": {
"SymfonyBundles\\JsonRequestBundle\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Dmitry Khaperets",
"email": "khaperets@gmail.com"
}
],
"description": "Symfony JsonRequest Bundle",
"homepage": "https://github.com/symfony-bundles/json-request-bundle",
"keywords": [
"angular",
"bundle",
"json",
"symfony"
],
"support": {
"issues": "https://github.com/symfony-bundles/json-request-bundle/issues",
"source": "https://github.com/symfony-bundles/json-request-bundle/tree/4.0.5"
},
"time": "2021-02-16T11:16:20+00:00"
},
{ {
"name": "symfony/asset", "name": "symfony/asset",
"version": "v5.3.2", "version": "v5.3.2",
@ -5186,6 +5561,74 @@
], ],
"time": "2021-05-27T09:27:20+00:00" "time": "2021-05-27T09:27:20+00:00"
}, },
{
"name": "symfony/polyfill-php56",
"version": "v1.20.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php56.git",
"reference": "54b8cd7e6c1643d78d011f3be89f3ef1f9f4c675"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/54b8cd7e6c1643d78d011f3be89f3ef1f9f4c675",
"reference": "54b8cd7e6c1643d78d011f3be89f3ef1f9f4c675",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"type": "metapackage",
"extra": {
"branch-alias": {
"dev-main": "1.20-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 5.6+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php56/tree/v1.20.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-10-23T14:02:19+00:00"
},
{ {
"name": "symfony/polyfill-php73", "name": "symfony/polyfill-php73",
"version": "v1.23.0", "version": "v1.23.0",
@ -9942,5 +10385,5 @@
"ext-iconv": "*" "ext-iconv": "*"
}, },
"platform-dev": [], "platform-dev": [],
"plugin-api-version": "2.0.0" "plugin-api-version": "2.1.0"
} }

View File

@ -12,4 +12,6 @@ return [
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true],
SymfonyBundles\JsonRequestBundle\JsonRequestBundle::class => ['all' => true],
]; ];

View File

@ -0,0 +1,4 @@
lexik_jwt_authentication:
secret_key: '%env(resolve:JWT_SECRET_KEY)%'
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
pass_phrase: '%env(JWT_PASSPHRASE)%'

View File

@ -13,17 +13,29 @@ security:
app_user_provider: app_user_provider:
entity: entity:
class: App\Entity\User class: App\Entity\User
property: id property: email
# used to reload user from session & other features (e.g. switch_user) # used to reload user from session & other features (e.g. switch_user)
# used to reload user from session & other features (e.g. switch_user) # used to reload user from session & other features (e.g. switch_user)
firewalls: firewalls:
login:
pattern: ^/api/login
stateless: true
json_login:
check_path: /api/login
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure
api:
pattern: ^/api
stateless: true
guard:
authenticators:
- lexik_jwt_authentication.jwt_token_authenticator
dev: dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/ pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false security: false
main: main:
lazy: true lazy: true
provider: app_user_provider provider: app_user_provider
# activate different ways to authenticate # activate different ways to authenticate
# https://symfony.com/doc/current/security.html#firewalls-authentication # https://symfony.com/doc/current/security.html#firewalls-authentication
@ -35,3 +47,6 @@ security:
access_control: access_control:
# - { path: ^/admin, roles: ROLE_ADMIN } # - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER } # - { path: ^/profile, roles: ROLE_USER }
# - { path: ^/register, roles: IS_AUTHENTICATED_ANONYMOUSLY }
# - { path: ^/api/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
# - { path: ^/api, roles: IS_AUTHENTICATED_FULLY }

View File

@ -1 +1,3 @@
cd ../../docker
docker.exe compose -p curenet exec app bash -c 'cd /web/backend && bash' docker.exe compose -p curenet exec app bash -c 'cd /web/backend && bash'
cd src/backend

View File

@ -0,0 +1,66 @@
<?php
namespace App\Controller;
use App\Entity\User;
use App\Traits\JsonResponseTrait;
use App\Repository\UserRepository;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
class AuthController extends AbstractController
{
use JsonResponseTrait;
public function __construct(private UserRepository $userRepository) {
}
#[Route('/api/register', methods: ["POST"], name: 'register')]
public function register(Request $request, UserPasswordHasherInterface $encoder)
{
$em = $this->getDoctrine()->getManager();
$name = $request->get('name');
$surname = $request->get('surname');
$password = $request->get('password');
$email = $request->get('email');
if (empty($name) || empty($password) || empty($email)){
return $this->notAcceptable("Invalid Username or Password or Email");
}
$existing = $this->userRepository->findByEmail($email);
if ($existing) {
return $this->notAcceptable("User email exists");
}
$user = new User();
$user->setPassword($encoder->hashPassword($user, $password));
$user->setEmail($email);
$user->setName($name);
$user->setSurname($surname);
$em->persist($user);
$em->flush();
return $this->created($user);
}
#[Route('/api/login', methods: ['POST'], name: 'login')]
public function login(JWTTokenManagerInterface $JWTManager)
{
$user = new User();
return $this->ok(['token' => $JWTManager->create($user)]);
}
#[Route('/api/user-check', methods: ['POST'])]
public function checkUser() {
$user = $this->getUser();
if (!$user) {
return $this->unauthorized([]);
}
$user->generateAvatar();
return $this->ok($user);
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Entity\Abstraction;
use ReflectionClass;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
class BaseEntity {
protected array $hidden = [];
protected array $map = [];
private array $systemParams = ['hidden', 'map', 'systemParams'];
public function toArray() {
$output = [];
$normalizers = [new ObjectNormalizer()];
$serializer = new Serializer($normalizers);
$array = $serializer->normalize($this, null);
$hidden = array_merge($this->hidden, $this->systemParams);
foreach ($array as $key => $value) {
if (in_array($key, $hidden)) {
continue;
}
if (isset($this->map[$key])) {
$key = $this->map[$key];
}
$output[$key] = $value;
}
return $output;
}
}

View File

@ -2,17 +2,21 @@
namespace App\Entity; namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use App\Repository\UserRepository;
use App\Entity\Abstraction\BaseEntity;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Security\User\JWTUserInterface;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
/** /**
* @ORM\Entity(repositoryClass=UserRepository::class) * @ORM\Entity(repositoryClass=UserRepository::class)
* @ORM\Table(name="`user`") * @ORM\Table(name="`user`")
*/ */
class User implements UserInterface, PasswordAuthenticatedUserInterface class User extends BaseEntity implements UserInterface, PasswordAuthenticatedUserInterface, JWTUserInterface
{ {
protected array $hidden = ['password', 'salt', 'userIdentifier', 'username'];
/** /**
* @ORM\Id * @ORM\Id
* @ORM\GeneratedValue * @ORM\GeneratedValue
@ -61,6 +65,12 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
*/ */
private $state; private $state;
public static function createFromPayload($username, array $payload)
{
$user = new User();
return $user;
}
public function getId(): ?string public function getId(): ?string
{ {
return $this->id; return $this->id;
@ -80,7 +90,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
*/ */
public function getUserIdentifier(): string public function getUserIdentifier(): string
{ {
return (string) $this->id; return (string) $this->email;
} }
/** /**
@ -88,7 +98,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
*/ */
public function getUsername(): string public function getUsername(): string
{ {
return (string) $this->id; return (string) $this->email;
} }
/** /**
@ -216,4 +226,10 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this; return $this;
} }
public function generateAvatar() {
if (!$this->avatar) {
$this->avatar = 'https://www.gravatar.com/avatar/' . md5($this->email) . '?s=514&d=robohash';
}
}
} }

View File

@ -36,6 +36,20 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader
$this->_em->flush(); $this->_em->flush();
} }
/**
* @return User[] Returns an array of User objects
*/
public function findByEmail($value)
{
return $this->createQueryBuilder('u')
->andWhere('u.email = :val')
->setParameter('val', $value)
->setMaxResults(1)
->getQuery()
->getOneOrNullResult()
;
}
// /** // /**
// * @return User[] Returns an array of User objects // * @return User[] Returns an array of User objects
// */ // */

View File

@ -0,0 +1,35 @@
<?php
namespace App\Traits;
use App\Entity\Abstraction\BaseEntity;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
trait JsonResponseTrait {
protected function ok($data, $code = Response::HTTP_OK, $headers = []) {
return $this->response($data, $code, $headers);
}
protected function created($data, $code = Response::HTTP_CREATED, $headers = []) {
return $this->response($data, $code, $headers);
}
protected function notAcceptable($data, $code = Response::HTTP_NOT_ACCEPTABLE, $headers = []) {
return $this->response([ 'error' => 'Not acceptable', 'data' => $data], $code, $headers);
}
protected function unauthorized($data, $code = Response::HTTP_UNAUTHORIZED, $headers = []) {
return $this->response([ 'error' => 'Unauthorized', 'data' => $data], $code, $headers);
}
protected function response($data, $code = Response::HTTP_OK, $headers = []) {
if ($data instanceof BaseEntity) {
$data = $data->toArray();
}
return new JsonResponse($data, $code, $headers);
}
}

View File

@ -91,12 +91,33 @@
"laminas/laminas-code": { "laminas/laminas-code": {
"version": "4.4.2" "version": "4.4.2"
}, },
"lcobucci/clock": {
"version": "2.0.0"
},
"lcobucci/jwt": {
"version": "4.1.4"
},
"lexik/jwt-authentication-bundle": {
"version": "2.5",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "master",
"version": "2.5",
"ref": "5b2157bcd5778166a5696e42f552ad36529a07a6"
},
"files": [
"config/packages/lexik_jwt_authentication.yaml"
]
},
"monolog/monolog": { "monolog/monolog": {
"version": "2.3.1" "version": "2.3.1"
}, },
"myclabs/deep-copy": { "myclabs/deep-copy": {
"version": "1.10.2" "version": "1.10.2"
}, },
"namshi/jose": {
"version": "7.2.3"
},
"nikic/php-parser": { "nikic/php-parser": {
"version": "v4.12.0" "version": "v4.12.0"
}, },
@ -222,6 +243,9 @@
"config/packages/sensio_framework_extra.yaml" "config/packages/sensio_framework_extra.yaml"
] ]
}, },
"symfony-bundles/json-request-bundle": {
"version": "4.0.5"
},
"symfony/asset": { "symfony/asset": {
"version": "v5.3.2" "version": "v5.3.2"
}, },
@ -442,6 +466,9 @@
"symfony/polyfill-mbstring": { "symfony/polyfill-mbstring": {
"version": "v1.23.0" "version": "v1.23.0"
}, },
"symfony/polyfill-php56": {
"version": "v1.20.0"
},
"symfony/polyfill-php73": { "symfony/polyfill-php73": {
"version": "v1.23.0" "version": "v1.23.0"
}, },

View File

@ -1715,6 +1715,14 @@
"schema-utils": "^2.7.0" "schema-utils": "^2.7.0"
} }
}, },
"@ng-stack/forms": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@ng-stack/forms/-/forms-2.4.0.tgz",
"integrity": "sha512-Czo4ZbzEcTSAQ8yS4d0PzYMeB8GidOz1ABZhUjwnZiLaNOY2gnIdpQEIqE+GjIVOyusGEjQ/psLneBj8gE2EjQ==",
"requires": {
"tslib": "^2.0.0"
}
},
"@ngtools/webpack": { "@ngtools/webpack": {
"version": "12.1.3", "version": "12.1.3",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-12.1.3.tgz", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-12.1.3.tgz",

View File

@ -21,6 +21,7 @@
"@angular/platform-browser": "~12.1.3", "@angular/platform-browser": "~12.1.3",
"@angular/platform-browser-dynamic": "~12.1.3", "@angular/platform-browser-dynamic": "~12.1.3",
"@angular/router": "~12.1.3", "@angular/router": "~12.1.3",
"@ng-stack/forms": "^2.4.0",
"@ngx-translate/core": "^13.0.0", "@ngx-translate/core": "^13.0.0",
"@ngx-translate/http-loader": "^6.0.0", "@ngx-translate/http-loader": "^6.0.0",
"bootstrap": "^5.0.2", "bootstrap": "^5.0.2",

View File

@ -1,5 +1,6 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { Route, RouterModule } from '@angular/router'; import { Route, RouterModule } from '@angular/router';
import { AuthGuard } from './modules/auth/services/auth/auth.guard';
import { NotFoundComponent } from './modules/shared/components/not-found/not-found.component'; import { NotFoundComponent } from './modules/shared/components/not-found/not-found.component';
interface AppMenuEntry { interface AppMenuEntry {
@ -15,6 +16,10 @@ interface AppRoute extends Route {
} }
export const appRoutes: AppRoute[] = [ export const appRoutes: AppRoute[] = [
{
path: 'auth',
loadChildren: () => import('./modules/auth/auth.module').then(m => m.AuthModule),
},
{ {
path: '', path: '',
component: NotFoundComponent, component: NotFoundComponent,
@ -25,6 +30,8 @@ export const appRoutes: AppRoute[] = [
matchExact: true, matchExact: true,
}, },
], ],
canActivate: [ AuthGuard, ],
canActivateChild: [ AuthGuard, ],
}, },
{ {
path: 'profile', path: 'profile',
@ -35,6 +42,8 @@ export const appRoutes: AppRoute[] = [
icon: 'fa fa-user', icon: 'fa fa-user',
}, },
], ],
canActivate: [ AuthGuard, ],
canActivateChild: [ AuthGuard, ],
}, },
{ {
path: 'community', path: 'community',
@ -45,6 +54,8 @@ export const appRoutes: AppRoute[] = [
icon: 'fa fa-users', icon: 'fa fa-users',
}, },
], ],
canActivate: [ AuthGuard, ],
canActivateChild: [ AuthGuard, ],
}, },
{ {
path: 'questions', path: 'questions',
@ -56,6 +67,8 @@ export const appRoutes: AppRoute[] = [
level: 1, level: 1,
} }
], ],
canActivate: [ AuthGuard, ],
canActivateChild: [ AuthGuard, ],
}, },
{ {
path: 'messages', path: 'messages',
@ -66,6 +79,8 @@ export const appRoutes: AppRoute[] = [
icon: 'fa fa-comments', icon: 'fa fa-comments',
}, },
], ],
canActivate: [ AuthGuard, ],
canActivateChild: [ AuthGuard, ],
}, },
{ {
path: 'tests', path: 'tests',
@ -76,6 +91,8 @@ export const appRoutes: AppRoute[] = [
icon: 'fa fa-heartbeat', icon: 'fa fa-heartbeat',
}, },
], ],
canActivate: [ AuthGuard, ],
canActivateChild: [ AuthGuard, ],
}, },
{ {
path: 'research', path: 'research',
@ -86,6 +103,8 @@ export const appRoutes: AppRoute[] = [
icon: 'fa fa-flask', icon: 'fa fa-flask',
}, },
], ],
canActivate: [ AuthGuard, ],
canActivateChild: [ AuthGuard, ],
}, },
{ {
path: 'admin', path: 'admin',
@ -103,6 +122,8 @@ export const appRoutes: AppRoute[] = [
level: 1, level: 1,
} }
], ],
canActivate: [ AuthGuard, ],
canActivateChild: [ AuthGuard, ],
}, },
]; ];

View File

@ -1,18 +1,23 @@
<ng-template #toolbar> <ng-template #toolbar>
<mat-toolbar [color]="isDark ? null : 'primary'"> <mat-toolbar class="toolbar-background">
<button mat-icon-button (click)="toggleSidebar()"> <button mat-icon-button (click)="toggleSidebar()" *ngIf="!isSidebarHidden">
<i class="fa fa-bars"></i> <i class="fa fa-bars"></i>
</button> </button>
<span>{{ 'APP.NAME' | translate }}</span> <span>{{ 'APP.NAME' | translate }}</span>
<span class="flex-grow-1"></span> <span class="flex-grow-1"></span>
<div>
<div *ngFor="let component of dynamicToolbarComponents">
<ng-container [ngComponentOutlet]="component"></ng-container>
</div>
</div>
</mat-toolbar> </mat-toolbar>
</ng-template> </ng-template>
<ng-container *ngIf="!isMobile"> <ng-container *ngIf="!isMobile">
<ng-container *ngTemplateOutlet="toolbar"></ng-container> <ng-container *ngTemplateOutlet="toolbar"></ng-container>
</ng-container> </ng-container>
<mat-drawer-container class="flex-grow-1"> <mat-drawer-container class="flex-grow-1">
<mat-drawer [mode]="sidebarMode" [opened]="isSidebarOpen" (openedChange)="onSidebarOpenedChange($event)"> <mat-drawer [mode]="sidebarMode" [opened]="isSidebarOpen" (openedChange)="onSidebarOpenedChange($event)" *ngIf="!isSidebarHidden">
<div class="menu"> <div class="menu">
<div> <div>
<ng-container *ngFor="let route of appRoutes"> <ng-container *ngFor="let route of appRoutes">
@ -29,10 +34,9 @@
</div> </div>
</ng-container> </ng-container>
</ng-container> </ng-container>
</div> </div>
<div> <div>
<div class="d-flex user-box"> <div class="d-flex user-box" *ngIf="user">
<div class="avatar" [style.background-image]="url(user.avatar)"></div> <div class="avatar" [style.background-image]="url(user.avatar)"></div>
<div class="d-flex align-items-center flex-grow-1 p-2">{{ user.name }} {{ user.surname }}</div> <div class="d-flex align-items-center flex-grow-1 p-2">{{ user.name }} {{ user.surname }}</div>
<div class="user-menu-container"> <div class="user-menu-container">
@ -55,10 +59,12 @@
<mat-option *ngFor="let theme of themes" [value]="theme">{{ 'THEME.'+theme.toUpperCase() | translate }}</mat-option> <mat-option *ngFor="let theme of themes" [value]="theme">{{ 'THEME.'+theme.toUpperCase() | translate }}</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<button mat-menu-item (click)="logout()">
Wyloguj
</button>
</mat-menu> </mat-menu>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</mat-drawer> </mat-drawer>

View File

@ -5,12 +5,16 @@ import { AppService, WindowSize } from './modules/shared/services/app/app.servic
import { BrowserStorageService } from './modules/shared/services/browser-storage/browser-storage.service'; import { BrowserStorageService } from './modules/shared/services/browser-storage/browser-storage.service';
import { ThemeService } from './modules/shared/services/theme/theme.service'; import { ThemeService } from './modules/shared/services/theme/theme.service';
import { DomSanitizer, SafeStyle } from '@angular/platform-browser'; import { DomSanitizer, SafeStyle } from '@angular/platform-browser';
import { AuthService } from './modules/auth/services/auth/auth.service';
import { Router } from '@angular/router';
import { UserModel } from './modules/auth/models/user.model';
enum SidebarOpenEnum { enum SidebarOpenEnum {
Default, Default,
Open, Open,
Closed, Closed,
} }
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
templateUrl: './app.component.html', templateUrl: './app.component.html',
@ -28,76 +32,102 @@ export class AppComponent {
themes: string[] = []; themes: string[] = [];
themeControl = new FormControl(); themeControl = new FormControl();
langControl = new FormControl(); langControl = new FormControl();
isDark = true;
appRoutes = appRoutes; appRoutes = appRoutes;
user = { user: UserModel;
name: 'Name', isSidebarHidden = false;
surname: 'Surname', dynamicToolbarComponents = [];
avatar: 'https://www.gravatar.com/avatar/81b206a89f89d5b1123b87606075c6a8?s=514&d=robohash',
};
get isSidebarOpen(): boolean { get isSidebarOpen(): boolean {
switch (this.sidebarOpen) { switch (this.sidebarOpen) {
case SidebarOpenEnum.Open: case SidebarOpenEnum.Open:
return true; return true;
break;
case SidebarOpenEnum.Closed: case SidebarOpenEnum.Closed:
return false; return false;
break;
default: default:
return this.defaultSidebarOpen; return this.defaultSidebarOpen;
break;
} }
} }
constructor( constructor(
appService: AppService, private appService: AppService,
themeService: ThemeService, private themeService: ThemeService,
private browserStorageService: BrowserStorageService, private browserStorageService: BrowserStorageService,
private sanitizer: DomSanitizer, private sanitizer: DomSanitizer,
private authService: AuthService,
private router: Router,
) { ) {
this.themes = themeService.getThemes(); this.configureSidebarEvents();
this.langs = appService.getLangs(); this.configureThemeEvents();
this.lang = appService.getLang(); this.configureLanguageEvents();
this.configureResizeEvents();
this.configureUserEvents();
}
configureUserEvents(): void {
this.user = this.authService.getUser();
this.authService.userChange.subscribe(user => {
this.user = user;
});
}
configureSidebarEvents(): void {
this.isSidebarHidden = this.appService.getSidebarHidden();
this.appService.sidebarHiddenChange.subscribe(hidden => {
this.isSidebarHidden = hidden;
});
const sidebarUserPrefference = this.browserStorageService.getItem('sidebar.open');
if (sidebarUserPrefference !== null) {
this.sidebarOpen = sidebarUserPrefference;
}
this.appService.dynamicToolbarComponentsChange.subscribe(components => {
this.dynamicToolbarComponents = components;
});
}
configureThemeEvents(): void {
this.themes = this.themeService.getThemes();
this.themeControl.valueChanges.subscribe(theme => { this.themeControl.valueChanges.subscribe(theme => {
themeService.setTheme(theme); this.themeService.setTheme(theme);
}); });
appService.changeLang(this.lang); this.onThemeChanged(this.themeService.getTheme());
this.themeService.themeChange.subscribe(theme => {
this.onThemeChanged(theme);
});
}
configureLanguageEvents(): void {
this.langs = this.appService.getLangs();
this.lang = this.appService.getLang();
this.appService.changeLang(this.lang);
this.langControl.valueChanges.subscribe(lang => { this.langControl.valueChanges.subscribe(lang => {
appService.changeLang(lang); this.appService.changeLang(lang);
});
this.onThemeChanged(themeService.getTheme());
themeService.themeChange.subscribe(theme => {
this.onThemeChanged(theme);
});
this.onResize(appService.size);
appService.sizeChange.subscribe(size => {
this.onResize(size);
});
this.onLangChanged(appService.getLang());
appService.langChange.subscribe(lang => {
this.onLangChanged(lang);
}); });
const sidebarUserPrefference = this.browserStorageService.getItem('sidebar.open'); this.onLangChanged(this.appService.getLang());
if (sidebarUserPrefference !== null) { this.appService.langChange.subscribe(lang => {
this.sidebarOpen = sidebarUserPrefference; this.onLangChanged(lang);
} });
}
configureResizeEvents(): void {
this.onResize(this.appService.size);
this.appService.sizeChange.subscribe(size => {
this.onResize(size);
});
} }
onThemeChanged(theme: string): void { onThemeChanged(theme: string): void {
if (theme === undefined) { if (theme === undefined) {
return; return;
} }
this.theme = theme; this.theme = theme;
this.themeControl.setValue(theme); this.themeControl.setValue(theme);
this.isDark = theme.indexOf('dark') !== -1;
} }
onResize(size: WindowSize): void { onResize(size: WindowSize): void {
if (size === undefined) { if (size === undefined) {
return; return;
} }
this.isMobile = size.isMobile; this.isMobile = size.isMobile;
this.defaultSidebarOpen = !size.isMobile; this.defaultSidebarOpen = !size.isMobile;
@ -124,4 +154,9 @@ export class AppComponent {
url(url: string): SafeStyle { url(url: string): SafeStyle {
return this.sanitizer.bypassSecurityTrustStyle(`url('${url}')`); return this.sanitizer.bypassSecurityTrustStyle(`url('${url}')`);
} }
logout(): void {
this.authService.logout();
this.router.navigateByUrl('/');
}
} }

View File

@ -1,6 +1,6 @@
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { HttpClientModule, HttpClient } from '@angular/common/http'; import { HttpClientModule, HttpClient, HTTP_INTERCEPTORS } from '@angular/common/http';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@ -11,6 +11,9 @@ import { BrowserStorageService } from './modules/shared/services/browser-storage
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { TokenInterceptor } from './modules/shared/interceptors/token/token.interceptor';
import { AuthTokenService } from './modules/shared/services/auth/auth-token.service';
import { AuthService } from './modules/auth/services/auth/auth.service';
export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader { export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
return new TranslateHttpLoader(http, '/assets/lang/', '.json'); return new TranslateHttpLoader(http, '/assets/lang/', '.json');
@ -41,6 +44,9 @@ export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
AppService, AppService,
ThemeService, ThemeService,
BrowserStorageService, BrowserStorageService,
AuthTokenService,
AuthService,
{ provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, multi: true }
], ],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })

View File

@ -1,6 +1,7 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Route } from '@angular/router'; import { RouterModule, Route } from '@angular/router';
import { NotFoundComponent } from '../shared/components/not-found/not-found.component'; import { NotFoundComponent } from '../shared/components/not-found/not-found.component';
import { UsersComponent } from './components/users/users.component';
const routes: Route[] = [ const routes: Route[] = [
{ {
@ -9,7 +10,7 @@ const routes: Route[] = [
}, },
{ {
path: 'users', path: 'users',
component: NotFoundComponent, component: UsersComponent,
}, },
]; ];

View File

@ -3,14 +3,21 @@ import { CommonModule } from '@angular/common';
import { AdminRoutingModule } from './admin-routing.module'; import { AdminRoutingModule } from './admin-routing.module';
import { SharedModule } from '../shared/shared.module'; import { SharedModule } from '../shared/shared.module';
import { UsersComponent } from './components/users/users.component';
import { AdminUserService } from './services/admin-user/admin-user.service';
@NgModule({ @NgModule({
declarations: [], declarations: [
UsersComponent
],
imports: [ imports: [
CommonModule, CommonModule,
AdminRoutingModule, AdminRoutingModule,
SharedModule, SharedModule,
] ],
providers: [
AdminUserService,
],
}) })
export class AdminModule { } export class AdminModule { }

View File

@ -0,0 +1 @@
<p>users works!</p>

View File

@ -0,0 +1,18 @@
import { Component, OnInit } from '@angular/core';
import { AdminUserService } from '../../services/admin-user/admin-user.service';
@Component({
selector: 'app-users',
templateUrl: './users.component.html',
styleUrls: ['./users.component.scss']
})
export class UsersComponent implements OnInit {
constructor(private adminUserService: AdminUserService) { }
ngOnInit(): void {
this.adminUserService.getUsers().subscribe(() => {
});
}
}

View File

@ -0,0 +1,14 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { UserModel } from 'src/app/modules/auth/models/user.model';
@Injectable()
export class AdminUserService {
constructor(private http: HttpClient) { }
getUsers(): Observable<UserModel[]> {
return this.http.get<UserModel[]>('/api/admin/users');
}
}

View File

@ -0,0 +1,31 @@
import { NgModule } from '@angular/core';
import { RouterModule, Route } from '@angular/router';
import { AuthComponent } from './components/auth/auth.component';
import { AuthTabEnum } from './enums/auth-tab.enum';
const routes: Route[] = [
{
path: AuthTabEnum.Login,
component: AuthComponent,
},
{
path: AuthTabEnum.Register,
component: AuthComponent,
},
{
path: AuthTabEnum.RestorePassword,
component: AuthComponent,
},
{
path: '**',
redirectTo: 'login',
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AuthRoutingModule { }

View File

@ -0,0 +1,29 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AuthRoutingModule } from './auth-routing.module';
import { AuthComponent } from './components/auth/auth.component';
import { ThemeSwitcherComponent } from './components/theme-switcher/theme-switcher.component';
import { MaterialModule } from '../material/material.module';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AuthService } from './services/auth/auth.service';
import { NgStackFormsModule } from '@ng-stack/forms';
@NgModule({
declarations: [
AuthComponent,
ThemeSwitcherComponent,
],
imports: [
CommonModule,
AuthRoutingModule,
MaterialModule,
FormsModule,
ReactiveFormsModule,
NgStackFormsModule,
],
providers: [
AuthService,
],
})
export class AuthModule { }

View File

@ -0,0 +1,66 @@
<div class="primary-background toolbar-background"></div>
<div class="col-11 col-lg-7 col-xl-5 p-3 content">
<mat-card class="px-0">
<mat-tab-group mat-align-tabs="center" [selectedIndex]="selectedIndex" (selectedIndexChange)="selectedIndexChange($event)">
<mat-tab label="Logowanie">
<div [formGroup]="loginForm" class="row align-items-center p-4 m-0">
<div class="col-12 col-md-4" required>Email</div>
<mat-form-field class="col-12 col-md-8">
<mat-label>Adres email</mat-label>
<input formControlName="email" matInput>
</mat-form-field>
<div class="col-12 col-md-4" required>Hasło</div>
<mat-form-field class="col-12 col-md-8">
<mat-label>Hasło</mat-label>
<input formControlName="password" type="password" matInput>
</mat-form-field>
<div class="text-end">
<button mat-button (click)="selectedIndexChange(getSelectedIndex(AuthTabEnum.RestorePassword))">Odzyskaj hasło</button>
<button mat-flat-button color="primary" (click)="login()">Zaloguj</button>
</div>
</div>
</mat-tab>
<mat-tab label="Rejestracja">
<div [formGroup]="registerForm" class="row align-items-center p-4 m-0">
<div class="col-12 col-md-4" required>Imię</div>
<mat-form-field class="col-12 col-md-8">
<mat-label>Imię</mat-label>
<input formControlName="name" matInput>
</mat-form-field>
<div class="col-12 col-md-4">Nazwisko</div>
<mat-form-field class="col-12 col-md-8">
<mat-label>Nazwisko</mat-label>
<input formControlName="surname" matInput>
</mat-form-field>
<div class="col-12 col-md-4" required>Email</div>
<mat-form-field class="col-12 col-md-8">
<mat-label>Adres email</mat-label>
<input formControlName="email" matInput>
</mat-form-field>
<div class="col-12 col-md-4" required>Hasło</div>
<mat-form-field class="col-12 col-md-8">
<mat-label>Hasło</mat-label>
<input formControlName="password" type="password" matInput>
</mat-form-field>
<div class="text-end">
<button mat-flat-button color="primary" (click)="register()">Zarejestruj</button>
</div>
</div>
</mat-tab>
<mat-tab label="Odzyskiwanie hasła" *ngIf="selectedIndex === 2">
<div [formGroup]="restoreForm" class="row align-items-center p-4 m-0">
<div class="col-12 col-md-4" required>Email</div>
<mat-form-field class="col-12 col-md-8">
<mat-label>Adres email</mat-label>
<input formControlName="email" matInput>
</mat-form-field>
<div class="text-end">
<button mat-button (click)="selectedIndexChange(getSelectedIndex(AuthTabEnum.Login))">Anuluj</button>
<button mat-flat-button color="primary">Odzyskaj hasło</button>
</div>
</div>
</mat-tab>
</mat-tab-group>
</mat-card>
</div>

View File

@ -0,0 +1,9 @@
.primary-background{
background-color: var(--toolbar-background);
height: 200px;
}
.content{
margin: 0px auto;
margin-top: -150px;
height: 200px;
}

View File

@ -0,0 +1,118 @@
import { Location } from '@angular/common';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router, UrlSegment } from '@angular/router';
import { FormBuilder, FormGroup } from '@ng-stack/forms';
import { AppService } from 'src/app/modules/shared/services/app/app.service';
import { AuthTabEnum } from '../../enums/auth-tab.enum';
import { LoginModel } from '../../models/login.model';
import { RegisterModel } from '../../models/register.model';
import { RestoreModel } from '../../models/restore.model';
import { AuthService } from '../../services/auth/auth.service';
import { ThemeSwitcherComponent } from '../theme-switcher/theme-switcher.component';
@Component({
selector: 'app-auth',
templateUrl: './auth.component.html',
styleUrls: ['./auth.component.scss']
})
export class AuthComponent implements OnInit, OnDestroy {
registerForm: FormGroup<RegisterModel>;
loginForm: FormGroup<LoginModel>;
restoreForm: FormGroup<RestoreModel>;
tab: AuthTabEnum;
selectedIndex: number;
defaultTab = AuthTabEnum.Login;
tabIndexes = [
AuthTabEnum.Login,
AuthTabEnum.Register,
AuthTabEnum.RestorePassword,
];
showRestore = false;
AuthTabEnum = AuthTabEnum;
constructor(
private appService: AppService,
private authService: AuthService,
private activatedRoute: ActivatedRoute,
private router: Router,
private location: Location,
formBuilder: FormBuilder,
) {
this.registerForm = formBuilder.group<RegisterModel>({
name: 'Name',
surname: 'Surname',
email: 'email@test.com',
password: 'password',
});
this.loginForm = formBuilder.group<LoginModel>({
email: 'email@test.com',
password: 'password',
});
this.restoreForm = formBuilder.group<RestoreModel>({
email: 'email@test.com',
});
this.handleUrl(activatedRoute.snapshot.url);
activatedRoute.url.subscribe(url => {
this.handleUrl(url);
});
}
handleUrl(url: UrlSegment[]): void {
const path = url[0].path;
this.tab = path as AuthTabEnum;
this.selectedIndex = this.getSelectedIndex();
}
getSelectedIndex(tab: AuthTabEnum = null): number {
if (tab === null) {
tab = this.tab;
}
let index = this.tabIndexes.findIndex(i => i === tab);
if (index === -1) {
index = this.tabIndexes.findIndex(i => i === this.defaultTab);
}
return index;
}
getIndexTab(): AuthTabEnum {
return this.tabIndexes[this.selectedIndex];
}
selectedIndexChange(index: number): void {
this.selectedIndex = index;
const tab = this.getIndexTab();
const url = this.router.createUrlTree(['..', tab], {
relativeTo: this.activatedRoute,
}).toString();
this.location.go(url);
}
ngOnInit(): void {
this.appService.setSidebarHidden(true);
this.appService.addDynamicToolbarComponent(ThemeSwitcherComponent);
}
ngOnDestroy(): void {
this.appService.setSidebarHidden(false);
this.appService.removeDynamicToolbarComponent(ThemeSwitcherComponent);
}
register(): void {
this.authService.createAccount(this.registerForm.getRawValue()).subscribe(data => {
this.loginForm.patchValue({
email: this.registerForm.controls.email.value,
password: this.registerForm.controls.password.value,
});
this.selectedIndexChange(this.getSelectedIndex(AuthTabEnum.Login));
});
}
login(): void {
this.authService.login(this.loginForm.getRawValue()).subscribe(data => {
const afterUrl = '/';
this.router.navigateByUrl(afterUrl);
});
}
}

View File

@ -0,0 +1,3 @@
<button mat-icon-button (click)="toggleTheme()">
<i class="fa" [class.fa-sun-o]="isDark" [class.fa-moon-o]="!isDark"></i>
</button>

View File

@ -0,0 +1,32 @@
import { Component, OnInit } from '@angular/core';
import { ThemeService } from 'src/app/modules/shared/services/theme/theme.service';
@Component({
selector: 'app-theme-switcher',
templateUrl: './theme-switcher.component.html',
styleUrls: ['./theme-switcher.component.scss']
})
export class ThemeSwitcherComponent implements OnInit {
isDark = false;
darkTheme = 'dark';
lightTheme = 'light';
theme = 'light';
constructor(
private themeService: ThemeService,
) {
this.theme = themeService.getTheme();
themeService.themeChange.subscribe(theme => {
this.theme = theme;
});
}
ngOnInit(): void {
}
toggleTheme(): void {
this.themeService.setTheme(this.theme === this.darkTheme ? this.lightTheme : this.darkTheme);
this.isDark = this.theme === this.darkTheme;
}
}

View File

@ -0,0 +1,5 @@
export enum AuthTabEnum {
Login = 'login',
Register = 'register',
RestorePassword = 'restore-password',
}

View File

@ -0,0 +1,4 @@
export interface LoginModel {
email: string;
password: string;
}

View File

@ -0,0 +1,6 @@
export interface RegisterModel {
name: string;
surname: string;
email: string;
password: string;
}

View File

@ -0,0 +1,3 @@
export interface RestoreModel {
email: string;
}

View File

@ -0,0 +1,3 @@
export interface TokenResponse {
token: string;
}

View File

@ -0,0 +1,8 @@
export interface UserModel {
id: number;
name: string;
surname: string;
avatar: string | null;
country: string | null;
state: string | null;
}

View File

@ -0,0 +1,34 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthTokenService } from 'src/app/modules/shared/services/auth/auth-token.service';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate, CanActivateChild {
constructor(private authTokenService: AuthTokenService, private router: Router) {
}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
const can = this.authTokenService.userValidate();
if (!can){
this.router.navigate(['/auth']);
}
return can;
}
canActivateChild(
childRoute: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
const can = this.authTokenService.userValidate();
if (!can){
this.router.navigate(['/auth']);
}
return can;
}
}

View File

@ -0,0 +1,68 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { LoginModel } from '../../models/login.model';
import { RegisterModel } from '../../models/register.model';
import { TokenResponse } from '../../models/token.response';
import { UserModel } from '../../models/user.model';
import { AuthTokenService } from '../../../shared/services/auth/auth-token.service';
import { Router } from '@angular/router';
@Injectable()
export class AuthService {
private static updateAgent: any;
private user: UserModel;
public userChange = new Subject<UserModel>();
constructor(
private http: HttpClient,
private authTokenService: AuthTokenService,
private router: Router,
) {
this.checkUser();
this.userChange.subscribe(user => {
this.user = user;
});
}
getUser(): UserModel {
return this.user;
}
checkUser(): void {
this.http.post<UserModel>('/api/user-check', {}).subscribe(user => {
this.userChange.next(user);
if (AuthService.updateAgent) {
clearTimeout(AuthService.updateAgent);
}
AuthService.updateAgent = setTimeout(() => {
this.checkUser();
}, 2000) as any;
}, () => {
this.logout();
});
}
createAccount(data: RegisterModel): Observable<UserModel> {
return this.http.post<UserModel>('/api/register', data);
}
login(data: LoginModel): Observable<TokenResponse> {
const out = new Subject<TokenResponse>();
this.http.post<TokenResponse>('/api/login', {
username: data.email,
password: data.password,
}).subscribe(response => {
this.authTokenService.setToken(response.token);
this.checkUser();
out.next(response);
});
return out;
}
logout(): void {
this.authTokenService.removeToken();
this.router.navigate(['/auth']);
}
}

View File

@ -5,23 +5,29 @@ import {MatButtonModule} from '@angular/material/button';
import {MatSidenavModule} from '@angular/material/sidenav'; import {MatSidenavModule} from '@angular/material/sidenav';
import {MatSelectModule} from '@angular/material/select'; import {MatSelectModule} from '@angular/material/select';
import {MatMenuModule} from '@angular/material/menu'; import {MatMenuModule} from '@angular/material/menu';
import {MatCardModule} from '@angular/material/card';
import {MatTabsModule} from '@angular/material/tabs';
import {MatInputModule} from '@angular/material/input';
const itemsToExport = [ const itemsToExport = [
MatToolbarModule, MatToolbarModule,
MatButtonModule, MatButtonModule,
MatSidenavModule, MatSidenavModule,
MatSelectModule, MatSelectModule,
MatMenuModule, MatMenuModule,
MatCardModule,
MatTabsModule,
MatInputModule,
]; ];
@NgModule({ @NgModule({
declarations: [], declarations: [],
imports: [ imports: [
CommonModule, CommonModule,
...itemsToExport, ...itemsToExport,
], ],
exports: [ exports: [
...itemsToExport, ...itemsToExport,
] ]
}) })
export class MaterialModule { } export class MaterialModule { }

View File

@ -1,3 +1,10 @@
<div class="m-3 p-3"> <div class="primary-background toolbar-background"></div>
{{ 'ERROR.PAGE_NOT_FOUND' | translate }} <div class="content align-items-center justify-content-center">
<div class="col-11 col-lg-7 col-xl-5">
<mat-card>
<div class="m-3 p-3">
{{ 'ERROR.PAGE_NOT_FOUND' | translate }}
</div>
</mat-card>
</div>
</div> </div>

View File

@ -0,0 +1,10 @@
.primary-background{
background-color: var(--toolbar-background);
height: 100px;
}
.content{
margin: 0px auto;
margin-top: -100px;
height: 200px;
display: flex;
}

View File

@ -0,0 +1,20 @@
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { AuthTokenService } from 'src/app/modules/shared/services/auth/auth-token.service';
@Injectable()
export class TokenInterceptor implements HttpInterceptor {
constructor(private authTokenService: AuthTokenService) {
}
intercept(httpRequest: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (this.authTokenService.hasToken()) {
const Authorization = `Bearer ${this.authTokenService.getToken()}`;
return next.handle(httpRequest.clone({ setHeaders: { Authorization } }));
}
return next.handle(httpRequest);
}
}

View File

@ -0,0 +1,9 @@
export class User {
id: number;
email: string;
name: string;
surname: string;
avatar: string;
country: string;
state: string;
}

View File

@ -4,70 +4,107 @@ import { fromEvent, Subject } from 'rxjs';
import { BrowserStorageService } from '../browser-storage/browser-storage.service'; import { BrowserStorageService } from '../browser-storage/browser-storage.service';
export class WindowSize { export class WindowSize {
isMobile: boolean; isMobile: boolean;
constructor( constructor(
public width: number, public width: number,
public height: number, public height: number,
) { ) {
this.isMobile = this.width <= 700; this.isMobile = this.width <= 700;
} }
static generate(): WindowSize { static generate(): WindowSize {
return new WindowSize(window.innerWidth, window.innerHeight); return new WindowSize(window.innerWidth, window.innerHeight);
} }
update(): WindowSize { update(): WindowSize {
this.width = window.innerWidth; this.width = window.innerWidth;
this.height = window.innerHeight; this.height = window.innerHeight;
return this; return this;
} }
} }
@Injectable() @Injectable()
export class AppService { export class AppService {
private lang = 'en'; private sidebarHidden = false;
private defaultLang = 'en'; private lang = 'en';
langChange = new Subject<string>(); private defaultLang = 'en';
size: WindowSize; private dynamicToolbarComponents = [];
sizeChange = new Subject<WindowSize>(); langChange = new Subject<string>();
size: WindowSize;
sizeChange = new Subject<WindowSize>();
sidebarHiddenChange = new Subject<boolean>();
dynamicToolbarComponentsChange = new Subject<any[]>();
constructor( constructor(
browserStorageService: BrowserStorageService, private browserStorageService: BrowserStorageService,
private translate: TranslateService, private translate: TranslateService,
) { ) {
this.langChange.subscribe(lang => { this.configureResizeEvents();
this.lang = lang; this.configureLanguageEvents();
browserStorageService.setItem('language', lang); this.configureSidebarEvents();
});
this.sizeChange.subscribe(size => this.size = size);
this.sizeChange.next(WindowSize.generate());
let language = browserStorageService.getItem('language');
if (!language) {
language = this.defaultLang;
} }
this.translate.setDefaultLang(this.defaultLang);
this.changeLang(language);
fromEvent(window, 'resize').subscribe(e => {
this.sizeChange.next(WindowSize.generate());
});
}
changeLang(lang: string): void { configureSidebarEvents(): void {
if (lang !== this.lang) { this.sidebarHiddenChange.subscribe(hidden => {
this.translate.use(lang); this.sidebarHidden = hidden;
this.langChange.next(lang); });
} }
}
getLang(): string { configureResizeEvents(): void {
return this.lang; this.sizeChange.subscribe(size => this.size = size);
} this.sizeChange.next(WindowSize.generate());
fromEvent(window, 'resize').subscribe(e => {
this.sizeChange.next(WindowSize.generate());
});
}
getLangs(): string[] { configureLanguageEvents(): void {
return [ this.langChange.subscribe(lang => {
'pl', this.lang = lang;
'en', this.browserStorageService.setItem('language', lang);
]; });
} let language = this.browserStorageService.getItem('language');
if (!language) {
language = this.defaultLang;
}
this.translate.setDefaultLang(this.defaultLang);
this.changeLang(language);
}
addDynamicToolbarComponent(component): void {
this.dynamicToolbarComponents.push(component);
this.dynamicToolbarComponentsChange.next(this.dynamicToolbarComponents);
}
removeDynamicToolbarComponent(component): void {
this.dynamicToolbarComponents = this.dynamicToolbarComponents.filter(c => c !== component);
this.dynamicToolbarComponentsChange.next(this.dynamicToolbarComponents);
}
setSidebarHidden(hidden: boolean): void {
this.sidebarHiddenChange.next(hidden);
}
getSidebarHidden(): boolean {
return this.sidebarHidden;
}
changeLang(lang: string): void {
if (lang !== this.lang) {
this.translate.use(lang);
this.langChange.next(lang);
}
}
getLang(): string {
return this.lang;
}
getLangs(): string[] {
return [
'pl',
'en',
];
}
} }

View File

@ -0,0 +1,34 @@
import { Injectable } from '@angular/core';
import { BrowserStorageService } from 'src/app/modules/shared/services/browser-storage/browser-storage.service';
@Injectable()
export class AuthTokenService {
private TokenKey = 'jwt.token';
constructor( private browserStorageService: BrowserStorageService) {
}
setToken(token: string): void {
this.browserStorageService.setItem(this.TokenKey, token);
}
hasToken(): boolean {
const token = this.browserStorageService.getItemOrDefault(this.TokenKey, null);
return token !== null;
}
getToken(): string {
return this.browserStorageService.getItem(this.TokenKey);
}
removeToken(): void {
this.browserStorageService.setItem(this.TokenKey, null);
}
userValidate(): boolean {
if (!this.hasToken()) {
return false;
}
return true;
}
}

View File

@ -4,6 +4,7 @@ import { NotFoundComponent } from './components/not-found/not-found.component';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { HttpLoaderFactory } from 'src/app/app.module'; import { HttpLoaderFactory } from 'src/app/app.module';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { MaterialModule } from '../material/material.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -11,6 +12,7 @@ import { HttpClient } from '@angular/common/http';
], ],
imports: [ imports: [
CommonModule, CommonModule,
MaterialModule,
TranslateModule.forRoot({ TranslateModule.forRoot({
defaultLanguage: 'en', defaultLanguage: 'en',
loader: { loader: {

View File

@ -1,6 +1,38 @@
@use 'sass:map'; @use 'sass:map';
$primary-color: map.get($theme, color, primary, 500); $primary-color: map.get($theme, color, primary, 500);
$toolbar-background: map.get($theme, color, background, app-bar);
$toolbar-color: map.get($theme, color, primary, contrast, 500) !default;
$main-background: map.get($theme, color, background, background) !default;
$warn-color: map.get($theme, warn, 500) !default;
body { body {
--primary-color: #{$primary-color}; --primary-color: #{$primary-color};
--toolbar-background: #{$toolbar-background};
--toolbar-color: #{$toolbar-color};
--main-background: #{$main-background};
--warn-color: #{$warn-color};
.mat-toolbar{
background-color: var(--toolbar-background);
color: var(--toolbar-color);
}
.toolbar-background{
position: relative;
& > *{
position: relative;
z-index: 2;
}
&:before{
position: absolute;
z-index: 0;
top: 0px;
left: 0px;
width:100%;
height: 100%;
content: "";
opacity: 0.125;
background-image: url('/assets/background/items.png');
background-position: top;
background-attachment: fixed;
}
}
} }

View File

@ -11,7 +11,7 @@
// Define the palettes for your theme using the Material Design palettes available in palette.scss // Define the palettes for your theme using the Material Design palettes available in palette.scss
// (imported above). For each palette, you can optionally specify a default, lighter, and darker // (imported above). For each palette, you can optionally specify a default, lighter, and darker
// hue. Available color palettes: https://material.io/design/color/ // hue. Available color palettes: https://material.io/design/color/
$theme-primary: mat.define-palette(mat.$blue-palette); $theme-primary: mat.define-palette(mat.$indigo-palette);
$theme-accent: mat.define-palette(mat.$green-palette, A200, A100, A400); $theme-accent: mat.define-palette(mat.$green-palette, A200, A100, A400);
// The warn palette is optional (defaults to red). // The warn palette is optional (defaults to red).

View File

@ -1,3 +1,4 @@
@use "sass:map";
// Custom Theming for Angular Material // Custom Theming for Angular Material
// For more information: https://material.angular.io/guide/theming // For more information: https://material.angular.io/guide/theming
@use '~@angular/material' as mat; @use '~@angular/material' as mat;
@ -11,7 +12,7 @@
// Define the palettes for your theme using the Material Design palettes available in palette.scss // Define the palettes for your theme using the Material Design palettes available in palette.scss
// (imported above). For each palette, you can optionally specify a default, lighter, and darker // (imported above). For each palette, you can optionally specify a default, lighter, and darker
// hue. Available color palettes: https://material.io/design/color/ // hue. Available color palettes: https://material.io/design/color/
$theme-primary: mat.define-palette(mat.$indigo-palette); $theme-primary: mat.define-palette(mat.$blue-palette);
$theme-accent: mat.define-palette(mat.$light-blue-palette, A200, A100, A400); $theme-accent: mat.define-palette(mat.$light-blue-palette, A200, A100, A400);
// The warn palette is optional (defaults to red). // The warn palette is optional (defaults to red).
@ -27,6 +28,10 @@ $theme: mat.define-light-theme((
) )
)); ));
$toolbar-background: map.get($theme, color, primary, 500);
$toolbar-text: map.get($theme, color, primary, contrast, 500);
$theme: map.set($theme, color, background, app-bar, $toolbar-background);
$theme: map.set($theme, color, foreground, app-bar, $toolbar-text);
// Include theme styles for core and each component used in your app. // Include theme styles for core and each component used in your app.
// Alternatively, you can import and @include the theme mixins for each component // Alternatively, you can import and @include the theme mixins for each component
// that you are using. // that you are using.

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

View File

@ -43,3 +43,8 @@ body .mat-drawer.mat-drawer-side {
.w-220{ .w-220{
width: 220px; width: 220px;
} }
[required]:after{
content: "*";
font-weight: bold;
color: var(--warn-color);
}