Compare commits
12 Commits
6-update-d
...
dev
Author | SHA1 | Date |
---|---|---|
Michał Sieciechowicz | 1422d96c80 | |
Michał Sieciechowicz | f52e611c67 | |
Michał Sieciechowicz | cbca7cb582 | |
Michał Sieciechowicz | dc5cb30510 | |
Michał Sieciechowicz | f57d6d81f0 | |
Michał Sieciechowicz | f0015a1961 | |
Michał Sieciechowicz | 6fcad220de | |
Michał Sieciechowicz | 0a7896ac94 | |
Michał Sieciechowicz | 7fabb5170a | |
Michał Sieciechowicz | c7f4e68a06 | |
Michał Sieciechowicz | 3fbd6d5b50 | |
Michał Sieciechowicz | d0b3a1be35 |
|
@ -30,6 +30,36 @@
|
|||
"xdebugSettings": {
|
||||
"max_children": 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "PHP 8.0 CLI",
|
||||
"type": "php",
|
||||
"request": "launch",
|
||||
"pathMappings": {
|
||||
"/web/backend": "${workspaceFolder}\\src\\backend"
|
||||
},
|
||||
"skipFiles": [
|
||||
"${workspaceFolder}/src/backend/vendor/",
|
||||
"/web/backend/vendor/"
|
||||
],
|
||||
"port": 7781,
|
||||
"hostname": "127.0.0.1",
|
||||
"xdebugSettings": {
|
||||
"max_children": 100,
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "PHP 8.0 CLI with vendor",
|
||||
"type": "php",
|
||||
"request": "launch",
|
||||
"pathMappings": {
|
||||
"/web/backend": "${workspaceFolder}\\src\\backend"
|
||||
},
|
||||
"port": 7781,
|
||||
"hostname": "127.0.0.1",
|
||||
"xdebugSettings": {
|
||||
"max_children": 100,
|
||||
}
|
||||
}
|
||||
|
||||
]
|
||||
|
|
|
@ -0,0 +1,198 @@
|
|||
pipeline {
|
||||
agent any
|
||||
|
||||
environment {
|
||||
DatabaseUrl = sh(returnStdout: true, script: 'bash /var/lib/jenkins/variables/CureNet/var.sh ${BRANCH_NAME} DatabaseUrl').trim()
|
||||
ProjectPath = sh(returnStdout: true, script: 'bash /var/lib/jenkins/variables/CureNet/var.sh ${BRANCH_NAME} ProjectPath').trim()
|
||||
DatabaseUrlTesting = sh(returnStdout: true, script: 'bash /var/lib/jenkins/variables/CureNet/var.sh master DatabaseUrl').trim()
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Pull') {
|
||||
steps {
|
||||
git(url: 'http://git.fufle.net/Community/CureNet.git', branch: '${BRANCH_NAME}', credentialsId: '799188a2-c281-4e4c-b78f-c82132ea792b', poll: true)
|
||||
}
|
||||
}
|
||||
|
||||
stage('Approve') {
|
||||
steps {
|
||||
echo "http://git.fufle.net/Community/CureNet/commit/${env.GIT_COMMIT}"
|
||||
script {
|
||||
if (env.BRANCH_NAME == "prod") {
|
||||
echo "http://git.fufle.net/Community/CureNet/compare/prod...staging"
|
||||
input(message: 'Deploy to production?', id: 'A', ok: 'Yes', submitter: 'deploy', submitterParameter: 'a')
|
||||
}
|
||||
if (env.BRANCH_NAME == "staging") {
|
||||
echo "http://git.fufle.net/Community/CureNet/compare/staging...beta"
|
||||
input(message: 'Deploy to staging instance?', id: 'A', ok: 'Yes', submitter: 'deploy', submitterParameter: 'a')
|
||||
}
|
||||
if (env.BRANCH_NAME == "beta") {
|
||||
echo "http://git.fufle.net/Community/CureNet/compare/beta...dev"
|
||||
input(message: 'Deploy to beta instance?', id: 'A', ok: 'Yes', submitter: 'deploy', submitterParameter: 'a')
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
stage('Install') {
|
||||
parallel {
|
||||
stage('Frontend') {
|
||||
steps {
|
||||
dir(path: 'src/frontend') {
|
||||
sh 'sed -i "s/-1001/${BUILD_ID}/g" src/environments/*.ts'
|
||||
sh 'Timestamp=`php8.0 -r "echo time();"` && sed -i "s/-1002/${Timestamp}/g" src/environments/*.ts'
|
||||
sh 'npm ci'
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
stage('Backend') {
|
||||
steps {
|
||||
dir(path: 'src/backend') {
|
||||
sh 'php8.0 /usr/local/bin/composer install'
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
stage('Configure for tests') {
|
||||
parallel {
|
||||
stage('Frontend') {
|
||||
steps {
|
||||
dir(path: 'src/frontend') {
|
||||
sh 'echo skip'
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
stage('Backend') {
|
||||
steps {
|
||||
dir(path: 'src/backend') {
|
||||
sh 'APP_SECRET=`php8.0 -r "echo md5(\\"branch=${BRANCH_NAME};\\");"` && sed -i "s/APP_SECRET=.*/APP_SECRET=${APP_SECRET}/g" .env'
|
||||
sh '''sed -i 's/DATABASE_URL=.*/DATABASE_URL=${DatabaseUrlTesting}/g' .env.test'''
|
||||
sh 'php8.0 /usr/local/bin/composer dump-env test'
|
||||
sh 'php8.0 bin/console doctrine:schema:update --force';
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
stage('Test') {
|
||||
parallel {
|
||||
stage('Frontend') {
|
||||
steps {
|
||||
dir(path: 'src/frontend') {
|
||||
sh 'ng lint'
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
stage('Backend') {
|
||||
steps {
|
||||
dir(path: 'src/backend') {
|
||||
sh 'php8.0 vendor/bin/phpunit'
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
stage('Configure') {
|
||||
parallel {
|
||||
stage('Frontend') {
|
||||
steps {
|
||||
dir(path: 'src/frontend') {
|
||||
sh 'echo skip'
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
stage('Backend') {
|
||||
steps {
|
||||
dir(path: 'src/backend') {
|
||||
sh '''APP_SECRET=`php8.0 -r "echo md5(\\"branch=${BRANCH_NAME};\\");"` && sed -i 's/APP_SECRET=.*/APP_SECRET=${APP_SECRET}/g' .env'''
|
||||
sh '''sed -i 's/DATABASE_URL=.*/DATABASE_URL=${DatabaseUrl}/g' .env.dev'''
|
||||
sh 'php8.0 /usr/local/bin/composer dump-env dev'
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build') {
|
||||
parallel {
|
||||
stage('Frontend') {
|
||||
steps {
|
||||
dir(path: 'src/frontend') {
|
||||
sh 'ng b -c production'
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
stage('Backend') {
|
||||
steps {
|
||||
dir(path: 'src/backend') {
|
||||
sh 'echo skip'
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
stage('Deploy') {
|
||||
when {
|
||||
anyOf {
|
||||
branch 'master'
|
||||
branch 'staging'
|
||||
branch 'beta'
|
||||
branch 'dev'
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
parallel {
|
||||
stage('Frontend') {
|
||||
steps {
|
||||
dir(path: 'src/frontend') {
|
||||
sh 'ssh web@fufle.net touch ${ProjectPath}/frontend/REMOVEME'
|
||||
sh 'ssh web@fufle.net rm -rf ${ProjectPath}/frontend/*'
|
||||
sh 'scp -r dist/* web@fufle.net:${ProjectPath}/frontend/'
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
stage('Backend') {
|
||||
steps {
|
||||
dir(path: 'src/backend') {
|
||||
sh 'ssh web@fufle.net touch ${ProjectPath}/backend/REMOVEME'
|
||||
sh 'ssh web@fufle.net rm -rf ${ProjectPath}/backend/*'
|
||||
sh 'scp -r ./* web@fufle.net:${ProjectPath}/backend/'
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@ We also encourage doctors and specialists to join the community, they will have
|
|||
### Disease analysis
|
||||
We want users to have access to the log of their disease where they can enter how it is progressing. On this basis, together with specialists, we will be able to find some common features for new diseases and predict as many negative symptoms as possible in order to be able to prevent them as much as possible.
|
||||
|
||||
[Technical documentation](./src/)
|
||||
[Technical documentation](./src/#technical-documentation)
|
||||
|
||||
# Polski
|
||||
## Czym jest CureNet?
|
||||
|
@ -26,6 +26,7 @@ Każdy może założyć profil oraz dodać jedną lub więcej chorób. Dzięki t
|
|||
### Społeczność specjalistów
|
||||
Zachęcamy także lekarzy i specjalistów, aby dołączyli do społeczności, będą oni mieli wgląd w listę swoich pacjentów ale także do listy osób, które mają podobne schorzenia jak ich podopieczni, dzięki czemu będą mieli dostęp do szerszego grona osób z daną chorobą. Będą mogli także przedyskutować swoje obserwacje z innymi lekarzami, którzy także mają doświadczenie z daną chorobą. Jest to naprawdę ważne przy bardzo unikalnych i nie dokońca poznanych jeszcze schorzeniach.
|
||||
### Analiza chorób
|
||||
Chcemy, aby użytkownicy mieli dostęp do dzinnika swojej choroby, gdzie będą mogli wprowadzać jak ona przebiega. Na tej podstawie wraz ze specjalistami będziemy w stanie znaleźć pewne wsólne cechy dla nowych chorób i przewidzieć jak najwięcej negatywnych symptomów, aby móc im jak najlepiej zapobiec.
|
||||
Chcemy, aby użytkownicy mieli dostęp do dziennika swojej choroby, gdzie będą mogli wprowadzać jak ona przebiega. Na tej podstawie wraz ze specjalistami będziemy w stanie znaleźć pewne wsólne cechy dla nowych chorób i przewidzieć jak najwięcej negatywnych symptomów, aby móc im jak najlepiej zapobiec.
|
||||
|
||||
[Dokumentacja techniczna](./src/#technical-documentation)
|
||||
|
||||
[Dokumentacja techniczna](./src/)
|
||||
|
|
|
@ -43,7 +43,7 @@ VOLUME /web/backend/vendor
|
|||
VOLUME /web/backend/var
|
||||
VOLUME /web/frontend/node_modules
|
||||
|
||||
EXPOSE 7700 7780 7781
|
||||
EXPOSE 7700 7780 7781 49153
|
||||
|
||||
STOPSIGNAL SIGQUIT
|
||||
|
||||
|
|
|
@ -4,19 +4,22 @@ services:
|
|||
build: .
|
||||
ports:
|
||||
- 7700:7700
|
||||
- 49153:49153
|
||||
expose:
|
||||
- 7780:7780
|
||||
- 7781:7781
|
||||
volumes:
|
||||
- .:/web/config
|
||||
- ../src/backend:/web/backend:cached
|
||||
- ../src/frontend:/web/frontend:cached
|
||||
- type: bind
|
||||
target: /web/frontend
|
||||
source: ../src/frontend
|
||||
volume:
|
||||
nocopy: true
|
||||
- ./logs:/var/log
|
||||
- vendor:/web/backend/vendor
|
||||
- var:/web/backend/var
|
||||
- nodemodules:/web/frontend/node_modules
|
||||
volumes:
|
||||
nodemodules:
|
||||
vendor:
|
||||
var:
|
||||
nodemodules:
|
|
@ -0,0 +1 @@
|
|||
docker.exe compose -p curenet exec app bash -c 'cd /web && bash'
|
|
@ -1,5 +1,14 @@
|
|||
#!/bin/bash
|
||||
|
||||
echo "> Configure cache"
|
||||
mkdir -p /tmp/app/vendor
|
||||
ln -s /tmp/app/vendor /web/backend/vendor
|
||||
|
||||
echo "> Configure user"
|
||||
useradd developer
|
||||
chown developer:developer /web/frontend -R
|
||||
chown developer:developer /web/backend -R
|
||||
|
||||
echo "> Configure logs"
|
||||
mkdir -p /var/log/{apache2,nginx,pgadmin,postgresql}
|
||||
chown postgres:adm /var/log/postgresql -R
|
||||
|
@ -36,10 +45,14 @@ echo "> Configure symfony"
|
|||
cd /web/backend
|
||||
composer install
|
||||
php bin/console doctrine:schema:update --force
|
||||
php php bin/console lexik:jwt:generate-keypair --skip-if-exists
|
||||
|
||||
echo "> Configure angular"
|
||||
cd /web/frontend
|
||||
npm ci
|
||||
time su developer -c 'npm ci'
|
||||
|
||||
echo "> Starting angular"
|
||||
ng serve
|
||||
su developer -c 'ng serve --disable-host-check --host 0.0.0.0 --poll'
|
||||
|
||||
echo "> Waiting"
|
||||
sleep Infinity
|
|
@ -26,7 +26,7 @@
|
|||
- [SQLTools](https://marketplace.visualstudio.com/items?itemName=mtxr.sqltools)
|
||||
- [SQLTools PostgreSQL/Redshift Driver](https://marketplace.visualstudio.com/items?itemName=mtxr.sqltools-driver-pg)
|
||||
- Plugins for git:
|
||||
1. [GitLens](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens)
|
||||
- [GitLens](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens)
|
||||
|
||||
## How to run app?
|
||||
1. Download project from repository using standalone git or built in any IDE
|
||||
|
@ -56,7 +56,7 @@
|
|||
6. Enter username: `postgres`
|
||||
7. Enter password: `WeryStronkPaz2`
|
||||
8. Save it
|
||||
9. Go to Servers > [you name] > Databases > curenet > Schemas > public > Tables
|
||||
9. Go to Servers > [your name] > Databases > curenet > Schemas > public > Tables
|
||||
10. You can see all tables
|
||||
### Database
|
||||
- Server: `localhost`
|
||||
|
@ -68,3 +68,7 @@
|
|||
- For application: `curenet`
|
||||
- For admin: `postgres`
|
||||
- Password: `WeryStronkPaz2`
|
||||
|
||||
|
||||
### Known issues
|
||||
1. If the docker build the app but can't run it, check the docker/startup.sh file, it should be saved with LF as end of line sequence. If it's CRLF try to change it to LF, save the file and try to run app again
|
|
@ -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="postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=13&charset=utf8"
|
||||
###< 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 ###
|
||||
|
|
|
@ -4,3 +4,4 @@ APP_SECRET='$ecretf0rt3st'
|
|||
SYMFONY_DEPRECATIONS_HELPER=999999
|
||||
PANTHER_APP_ENV=panther
|
||||
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots
|
||||
DATABASE_URL="postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=13&charset=utf8"
|
||||
|
|
|
@ -18,3 +18,7 @@
|
|||
/phpunit.xml
|
||||
.phpunit.result.cache
|
||||
###< phpunit/phpunit ###
|
||||
|
||||
###> lexik/jwt-authentication-bundle ###
|
||||
/config/jwt/*.pem
|
||||
###< lexik/jwt-authentication-bundle ###
|
||||
|
|
|
@ -12,8 +12,10 @@
|
|||
"doctrine/doctrine-bundle": "^2.4",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.1",
|
||||
"doctrine/orm": "^2.9",
|
||||
"lexik/jwt-authentication-bundle": "^2.12",
|
||||
"phpdocumentor/reflection-docblock": "^5.2",
|
||||
"sensio/framework-extra-bundle": "^6.1",
|
||||
"symfony-bundles/json-request-bundle": "^4.0",
|
||||
"symfony/asset": "5.3.*",
|
||||
"symfony/console": "5.3.*",
|
||||
"symfony/dotenv": "5.3.*",
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "e3fa1ece831d58dd0515e0413472408f",
|
||||
"content-hash": "7f30e61383bffabaf547a9160e26d9c1",
|
||||
"packages": [
|
||||
{
|
||||
"name": "composer/package-versions-deprecated",
|
||||
|
@ -1653,6 +1653,252 @@
|
|||
],
|
||||
"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",
|
||||
"version": "2.3.1",
|
||||
|
@ -1749,6 +1995,73 @@
|
|||
],
|
||||
"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",
|
||||
"version": "2.2.0",
|
||||
|
@ -2235,6 +2548,68 @@
|
|||
},
|
||||
"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",
|
||||
"version": "v5.3.2",
|
||||
|
@ -5186,6 +5561,74 @@
|
|||
],
|
||||
"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",
|
||||
"version": "v1.23.0",
|
||||
|
@ -9942,5 +10385,5 @@
|
|||
"ext-iconv": "*"
|
||||
},
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.0.0"
|
||||
"plugin-api-version": "2.1.0"
|
||||
}
|
||||
|
|
|
@ -12,4 +12,6 @@ return [
|
|||
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
|
||||
Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true],
|
||||
SymfonyBundles\JsonRequestBundle\JsonRequestBundle::class => ['all' => true],
|
||||
];
|
||||
|
|
|
@ -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)%'
|
|
@ -13,17 +13,29 @@ security:
|
|||
app_user_provider:
|
||||
entity:
|
||||
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)
|
||||
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:
|
||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||
security: false
|
||||
main:
|
||||
lazy: true
|
||||
provider: app_user_provider
|
||||
|
||||
# activate different ways to authenticate
|
||||
# https://symfony.com/doc/current/security.html#firewalls-authentication
|
||||
|
||||
|
@ -35,3 +47,6 @@ security:
|
|||
access_control:
|
||||
# - { path: ^/admin, roles: ROLE_ADMIN }
|
||||
# - { path: ^/profile, roles: ROLE_USER }
|
||||
# - { path: ^/register, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||
# - { path: ^/api/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||
# - { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
|
|
@ -0,0 +1,3 @@
|
|||
cd ../../docker
|
||||
docker.exe compose -p curenet exec app bash -c 'cd /web/backend && bash'
|
||||
cd src/backend
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -2,17 +2,21 @@
|
|||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\UserRepository;
|
||||
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 Lexik\Bundle\JWTAuthenticationBundle\Security\User\JWTUserInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
|
||||
/**
|
||||
* @ORM\Entity(repositoryClass=UserRepository::class)
|
||||
* @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\GeneratedValue
|
||||
|
@ -61,6 +65,12 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||
*/
|
||||
private $state;
|
||||
|
||||
public static function createFromPayload($username, array $payload)
|
||||
{
|
||||
$user = new User();
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
|
@ -80,7 +90,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||
*/
|
||||
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
|
||||
{
|
||||
return (string) $this->id;
|
||||
return (string) $this->email;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -216,4 +226,10 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function generateAvatar() {
|
||||
if (!$this->avatar) {
|
||||
$this->avatar = 'https://www.gravatar.com/avatar/' . md5($this->email) . '?s=514&d=robohash';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,20 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader
|
|||
$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
|
||||
// */
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -91,12 +91,33 @@
|
|||
"laminas/laminas-code": {
|
||||
"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": {
|
||||
"version": "2.3.1"
|
||||
},
|
||||
"myclabs/deep-copy": {
|
||||
"version": "1.10.2"
|
||||
},
|
||||
"namshi/jose": {
|
||||
"version": "7.2.3"
|
||||
},
|
||||
"nikic/php-parser": {
|
||||
"version": "v4.12.0"
|
||||
},
|
||||
|
@ -222,6 +243,9 @@
|
|||
"config/packages/sensio_framework_extra.yaml"
|
||||
]
|
||||
},
|
||||
"symfony-bundles/json-request-bundle": {
|
||||
"version": "4.0.5"
|
||||
},
|
||||
"symfony/asset": {
|
||||
"version": "v5.3.2"
|
||||
},
|
||||
|
@ -442,6 +466,9 @@
|
|||
"symfony/polyfill-mbstring": {
|
||||
"version": "v1.23.0"
|
||||
},
|
||||
"symfony/polyfill-php56": {
|
||||
"version": "v1.20.0"
|
||||
},
|
||||
"symfony/polyfill-php73": {
|
||||
"version": "v1.23.0"
|
||||
},
|
||||
|
|
|
@ -48,7 +48,17 @@
|
|||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
"src/styles.scss",
|
||||
{
|
||||
"input": "src/app/styles/light.scss",
|
||||
"bundleName": "light",
|
||||
"inject": false
|
||||
},
|
||||
{
|
||||
"input": "src/app/styles/dark.scss",
|
||||
"bundleName": "dark",
|
||||
"inject": false
|
||||
}
|
||||
],
|
||||
"scripts": [],
|
||||
"vendorChunk": true,
|
||||
|
|
|
@ -291,6 +291,23 @@
|
|||
"tslib": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"@angular/cdk": {
|
||||
"version": "12.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-12.1.3.tgz",
|
||||
"integrity": "sha512-uCWHk/PjddNJsdrmexasphWGbf4kYtYyhUCSd4HEBrIDjYz166MTVSr3FHgn/s8/tlVou7uTnaEZM+ILWoe2iQ==",
|
||||
"requires": {
|
||||
"parse5": "^5.0.0",
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"parse5": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
|
||||
"integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@angular/cli": {
|
||||
"version": "12.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-12.1.3.tgz",
|
||||
|
@ -491,6 +508,14 @@
|
|||
"tslib": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"@angular/material": {
|
||||
"version": "12.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@angular/material/-/material-12.1.3.tgz",
|
||||
"integrity": "sha512-ekkg3MDEN133NhcE0du7a69lFAIFyvK6Va+YRTxYQE0BCNVMTWXtGbOXZLovDMjgo1xdXwrEJrMRBeYo+FIttA==",
|
||||
"requires": {
|
||||
"tslib": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"@angular/platform-browser": {
|
||||
"version": "12.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-12.1.3.tgz",
|
||||
|
@ -1690,6 +1715,14 @@
|
|||
"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": {
|
||||
"version": "12.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-12.1.3.tgz",
|
||||
|
@ -1699,6 +1732,22 @@
|
|||
"enhanced-resolve": "5.8.2"
|
||||
}
|
||||
},
|
||||
"@ngx-translate/core": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-13.0.0.tgz",
|
||||
"integrity": "sha512-+tzEp8wlqEnw0Gc7jtVRAJ6RteUjXw6JJR4O65KlnxOmJrCGPI0xjV/lKRnQeU0w4i96PQs/jtpL921Wrb7PWg==",
|
||||
"requires": {
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@ngx-translate/http-loader": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-6.0.0.tgz",
|
||||
"integrity": "sha512-LCekn6qCbeXWlhESCxU1rAbZz33WzDG0lI7Ig0pYC1o5YxJWrkU9y3Y4tNi+jakQ7R6YhTR2D3ox6APxDtA0wA==",
|
||||
"requires": {
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
|
@ -2725,6 +2774,16 @@
|
|||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
||||
"dev": true
|
||||
},
|
||||
"bindings": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
},
|
||||
"bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
|
@ -2819,6 +2878,11 @@
|
|||
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
|
||||
"dev": true
|
||||
},
|
||||
"bootstrap": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.0.2.tgz",
|
||||
"integrity": "sha512-1Ge963tyEQWJJ+8qtXFU6wgmAVj9gweEjibUdbmcCEYsn38tVwRk8107rk2vzt6cfQcRr3SlZ8aQBqaD8aqf+Q=="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
|
@ -4978,6 +5042,13 @@
|
|||
"escape-string-regexp": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
|
@ -5075,6 +5146,11 @@
|
|||
"integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==",
|
||||
"dev": true
|
||||
},
|
||||
"font-awesome": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz",
|
||||
"integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM="
|
||||
},
|
||||
"for-in": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
|
||||
|
@ -7430,6 +7506,13 @@
|
|||
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
|
||||
"dev": true
|
||||
},
|
||||
"nan": {
|
||||
"version": "2.14.2",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
|
||||
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"nanoid": {
|
||||
"version": "3.1.23",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz",
|
||||
|
@ -12599,7 +12682,11 @@
|
|||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
|
||||
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"bindings": "^1.5.0",
|
||||
"nan": "^2.12.1"
|
||||
}
|
||||
},
|
||||
"glob-parent": {
|
||||
"version": "3.1.0",
|
||||
|
|
|
@ -12,13 +12,20 @@
|
|||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "~12.1.3",
|
||||
"@angular/cdk": "^12.1.3",
|
||||
"@angular/common": "~12.1.3",
|
||||
"@angular/compiler": "~12.1.3",
|
||||
"@angular/core": "~12.1.3",
|
||||
"@angular/forms": "~12.1.3",
|
||||
"@angular/material": "^12.1.3",
|
||||
"@angular/platform-browser": "~12.1.3",
|
||||
"@angular/platform-browser-dynamic": "~12.1.3",
|
||||
"@angular/router": "~12.1.3",
|
||||
"@ng-stack/forms": "^2.4.0",
|
||||
"@ngx-translate/core": "^13.0.0",
|
||||
"@ngx-translate/http-loader": "^6.0.0",
|
||||
"bootstrap": "^5.0.2",
|
||||
"font-awesome": "^4.7.0",
|
||||
"rxjs": "~6.5.5",
|
||||
"tslib": "^2.0.0",
|
||||
"zone.js": "~0.11.4"
|
||||
|
@ -27,9 +34,9 @@
|
|||
"@angular-devkit/build-angular": "~12.1.3",
|
||||
"@angular/cli": "~12.1.3",
|
||||
"@angular/compiler-cli": "~12.1.3",
|
||||
"@types/node": "^12.11.1",
|
||||
"@types/jasmine": "~3.6.0",
|
||||
"@types/jasminewd2": "~2.0.3",
|
||||
"@types/node": "^12.11.1",
|
||||
"codelyzer": "^6.0.0",
|
||||
"jasmine-core": "~3.6.0",
|
||||
"jasmine-spec-reporter": "~5.0.0",
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
cd ../../docker
|
||||
docker.exe compose -p curenet exec app bash -c 'cd /web/frontend && bash'
|
||||
cd ../src/frontend
|
|
@ -1,10 +1,134 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { Routes, 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';
|
||||
|
||||
const routes: Routes = [];
|
||||
interface AppMenuEntry {
|
||||
label: string;
|
||||
icon: string;
|
||||
path?: string;
|
||||
matchExact?: boolean;
|
||||
level?: number;
|
||||
}
|
||||
|
||||
interface AppRoute extends Route {
|
||||
menuEntries?: AppMenuEntry[];
|
||||
}
|
||||
|
||||
export const appRoutes: AppRoute[] = [
|
||||
{
|
||||
path: 'auth',
|
||||
loadChildren: () => import('./modules/auth/auth.module').then(m => m.AuthModule),
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
component: NotFoundComponent,
|
||||
menuEntries: [
|
||||
{
|
||||
label: 'MENU.HOME',
|
||||
icon: 'fa fa-home',
|
||||
matchExact: true,
|
||||
},
|
||||
],
|
||||
canActivate: [ AuthGuard, ],
|
||||
canActivateChild: [ AuthGuard, ],
|
||||
},
|
||||
{
|
||||
path: 'profile',
|
||||
component: NotFoundComponent,
|
||||
menuEntries: [
|
||||
{
|
||||
label: 'MENU.PROFILE',
|
||||
icon: 'fa fa-user',
|
||||
},
|
||||
],
|
||||
canActivate: [ AuthGuard, ],
|
||||
canActivateChild: [ AuthGuard, ],
|
||||
},
|
||||
{
|
||||
path: 'community',
|
||||
component: NotFoundComponent,
|
||||
menuEntries: [
|
||||
{
|
||||
label: 'MENU.COMMUNITY',
|
||||
icon: 'fa fa-users',
|
||||
},
|
||||
],
|
||||
canActivate: [ AuthGuard, ],
|
||||
canActivateChild: [ AuthGuard, ],
|
||||
},
|
||||
{
|
||||
path: 'questions',
|
||||
component: NotFoundComponent,
|
||||
menuEntries: [
|
||||
{
|
||||
label: 'MENU.QUESTIONS',
|
||||
icon: 'fa fa-question',
|
||||
level: 1,
|
||||
}
|
||||
],
|
||||
canActivate: [ AuthGuard, ],
|
||||
canActivateChild: [ AuthGuard, ],
|
||||
},
|
||||
{
|
||||
path: 'messages',
|
||||
component: NotFoundComponent,
|
||||
menuEntries: [
|
||||
{
|
||||
label: 'MENU.MESSAGES',
|
||||
icon: 'fa fa-comments',
|
||||
},
|
||||
],
|
||||
canActivate: [ AuthGuard, ],
|
||||
canActivateChild: [ AuthGuard, ],
|
||||
},
|
||||
{
|
||||
path: 'tests',
|
||||
component: NotFoundComponent,
|
||||
menuEntries: [
|
||||
{
|
||||
label: 'MENU.TESTS',
|
||||
icon: 'fa fa-heartbeat',
|
||||
},
|
||||
],
|
||||
canActivate: [ AuthGuard, ],
|
||||
canActivateChild: [ AuthGuard, ],
|
||||
},
|
||||
{
|
||||
path: 'research',
|
||||
component: NotFoundComponent,
|
||||
menuEntries: [
|
||||
{
|
||||
label: 'MENU.RESEARCH',
|
||||
icon: 'fa fa-flask',
|
||||
},
|
||||
],
|
||||
canActivate: [ AuthGuard, ],
|
||||
canActivateChild: [ AuthGuard, ],
|
||||
},
|
||||
{
|
||||
path: 'admin',
|
||||
loadChildren: () => import('./modules/admin/admin.module').then(m => m.AdminModule),
|
||||
menuEntries: [
|
||||
{
|
||||
label: 'MENU.ADMIN',
|
||||
icon: 'fa fa-id-card',
|
||||
matchExact: true,
|
||||
},
|
||||
{
|
||||
path: 'admin/users',
|
||||
label: 'MENU.USERS',
|
||||
icon: 'fa fa-users',
|
||||
level: 1,
|
||||
}
|
||||
],
|
||||
canActivate: [ AuthGuard, ],
|
||||
canActivateChild: [ AuthGuard, ],
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
exports: [RouterModule]
|
||||
imports: [RouterModule.forRoot(appRoutes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
|
|
|
@ -1 +1,79 @@
|
|||
<router-outlet></router-outlet>
|
||||
|
||||
<ng-template #toolbar>
|
||||
<mat-toolbar class="toolbar-background">
|
||||
<button mat-icon-button (click)="toggleSidebar()" *ngIf="!isSidebarHidden">
|
||||
<i class="fa fa-bars"></i>
|
||||
</button>
|
||||
<span>{{ 'APP.NAME' | translate }}</span>
|
||||
<span class="flex-grow-1"></span>
|
||||
<div>
|
||||
<div *ngFor="let component of dynamicToolbarComponents">
|
||||
<ng-container [ngComponentOutlet]="component"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</mat-toolbar>
|
||||
</ng-template>
|
||||
<ng-container *ngIf="!isMobile">
|
||||
<ng-container *ngTemplateOutlet="toolbar"></ng-container>
|
||||
</ng-container>
|
||||
<mat-drawer-container class="flex-grow-1">
|
||||
<mat-drawer [mode]="sidebarMode" [opened]="isSidebarOpen" (openedChange)="onSidebarOpenedChange($event)" *ngIf="!isSidebarHidden">
|
||||
<div class="menu">
|
||||
<div>
|
||||
<ng-container *ngFor="let route of appRoutes">
|
||||
<ng-container *ngIf="route.menuEntries">
|
||||
<div
|
||||
*ngFor="let menu of route.menuEntries"
|
||||
class="menu-item"
|
||||
[attr.style]="menu.level ? 'margin-left: '+(menu.level * 20)+'px' : ''"
|
||||
[routerLink]="menu.path ? menu.path : route.path"
|
||||
routerLinkActive="active"
|
||||
[routerLinkActiveOptions]="{exact: menu.matchExact}">
|
||||
<i [class]="menu.icon" [title]="menu.label | translate"></i>
|
||||
<span>{{ menu.label | translate }}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex user-box" *ngIf="user">
|
||||
<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="user-menu-container">
|
||||
<div class="user-menu">
|
||||
<button mat-icon-button [matMenuTriggerFor]="usermenu">
|
||||
<i class="fa fa-cog"></i>
|
||||
</button>
|
||||
</div>
|
||||
<mat-menu #usermenu="matMenu" yPosition="above" xPosition="before" class="w-220">
|
||||
<mat-form-field class="w-100 px-3">
|
||||
<mat-label>{{ 'APP.LANGUAGE' | translate }}</mat-label>
|
||||
<mat-select [formControl]="langControl">
|
||||
<mat-option *ngFor="let lang of langs" [value]="lang">{{ 'LANGUAGE.'+lang.toUpperCase() | translate }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="w-100 px-3">
|
||||
<mat-label>{{ 'APP.THEME' | translate }}</mat-label>
|
||||
<mat-select [formControl]="themeControl">
|
||||
<mat-option *ngFor="let theme of themes" [value]="theme">{{ 'THEME.'+theme.toUpperCase() | translate }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<button mat-menu-item (click)="logout()">
|
||||
Wyloguj
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-drawer>
|
||||
|
||||
<ng-container *ngIf="isMobile">
|
||||
<ng-container *ngTemplateOutlet="toolbar"></ng-container>
|
||||
</ng-container>
|
||||
|
||||
<div class="example-sidenav-content">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</mat-drawer-container>
|
|
@ -0,0 +1,91 @@
|
|||
$sidebar-width: 250px;
|
||||
:host{
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.menu{
|
||||
width: $sidebar-width;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
> :first-child{
|
||||
flex-grow: 1;
|
||||
}
|
||||
> * {
|
||||
width: $sidebar-width;
|
||||
}
|
||||
}
|
||||
.avatar{
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
background-color: rgba(0,0,0, 0.25);
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
.menu-item{
|
||||
display: flex;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
padding: 10px;
|
||||
transition-duration: 400ms !important;
|
||||
transition-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !important;
|
||||
transition-property: all !important;
|
||||
&:after{
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0px;
|
||||
width: 3px;
|
||||
height: 0%;
|
||||
background: var(--primary-color);
|
||||
transition-duration: 400ms !important;
|
||||
transition-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !important;
|
||||
transition-property: all !important;
|
||||
}
|
||||
&.active{
|
||||
&:after{
|
||||
top: 0%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
i {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 10px;
|
||||
transition: all 0.4s ease;
|
||||
}
|
||||
span{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
.user-box{
|
||||
padding: 6px;
|
||||
}
|
||||
.user-menu-container{
|
||||
width: 46px;
|
||||
.user-menu{
|
||||
position: absolute;
|
||||
bottom: 6px;
|
||||
right: 6px;
|
||||
height: 48px;
|
||||
width: 46px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,19 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { appRoutes } from './app-routing.module';
|
||||
import { AppService, WindowSize } from './modules/shared/services/app/app.service';
|
||||
import { BrowserStorageService } from './modules/shared/services/browser-storage/browser-storage.service';
|
||||
import { ThemeService } from './modules/shared/services/theme/theme.service';
|
||||
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 {
|
||||
Default,
|
||||
Open,
|
||||
Closed,
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
|
@ -6,5 +21,142 @@ import { Component } from '@angular/core';
|
|||
styleUrls: ['./app.component.scss']
|
||||
})
|
||||
export class AppComponent {
|
||||
isMobile = true;
|
||||
title = 'CureNet';
|
||||
sidebarOpen = SidebarOpenEnum.Default;
|
||||
sidebarMode = 'over';
|
||||
defaultSidebarOpen = false;
|
||||
lang: string;
|
||||
theme: string;
|
||||
langs: string[] = [];
|
||||
themes: string[] = [];
|
||||
themeControl = new FormControl();
|
||||
langControl = new FormControl();
|
||||
appRoutes = appRoutes;
|
||||
user: UserModel;
|
||||
isSidebarHidden = false;
|
||||
dynamicToolbarComponents = [];
|
||||
|
||||
get isSidebarOpen(): boolean {
|
||||
switch (this.sidebarOpen) {
|
||||
case SidebarOpenEnum.Open:
|
||||
return true;
|
||||
case SidebarOpenEnum.Closed:
|
||||
return false;
|
||||
default:
|
||||
return this.defaultSidebarOpen;
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
private appService: AppService,
|
||||
private themeService: ThemeService,
|
||||
private browserStorageService: BrowserStorageService,
|
||||
private sanitizer: DomSanitizer,
|
||||
private authService: AuthService,
|
||||
private router: Router,
|
||||
) {
|
||||
this.configureSidebarEvents();
|
||||
this.configureThemeEvents();
|
||||
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.themeService.setTheme(theme);
|
||||
});
|
||||
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.appService.changeLang(lang);
|
||||
});
|
||||
|
||||
this.onLangChanged(this.appService.getLang());
|
||||
this.appService.langChange.subscribe(lang => {
|
||||
this.onLangChanged(lang);
|
||||
});
|
||||
}
|
||||
|
||||
configureResizeEvents(): void {
|
||||
this.onResize(this.appService.size);
|
||||
this.appService.sizeChange.subscribe(size => {
|
||||
this.onResize(size);
|
||||
});
|
||||
}
|
||||
|
||||
onThemeChanged(theme: string): void {
|
||||
if (theme === undefined) {
|
||||
return;
|
||||
}
|
||||
this.theme = theme;
|
||||
this.themeControl.setValue(theme);
|
||||
}
|
||||
|
||||
onResize(size: WindowSize): void {
|
||||
if (size === undefined) {
|
||||
return;
|
||||
}
|
||||
this.isMobile = size.isMobile;
|
||||
this.defaultSidebarOpen = !size.isMobile;
|
||||
this.sidebarMode = this.isMobile ? 'over' : 'side';
|
||||
}
|
||||
|
||||
onLangChanged(lang: string): void {
|
||||
if (lang === undefined) {
|
||||
return;
|
||||
}
|
||||
this.lang = lang;
|
||||
this.langControl.setValue(lang);
|
||||
}
|
||||
|
||||
toggleSidebar(): void {
|
||||
this.sidebarOpen = this.isSidebarOpen ? SidebarOpenEnum.Closed : SidebarOpenEnum.Open;
|
||||
this.browserStorageService.setItem('sidebar.open', this.sidebarOpen);
|
||||
}
|
||||
|
||||
onSidebarOpenedChange(opened: boolean): void {
|
||||
this.sidebarOpen = opened ? SidebarOpenEnum.Open : SidebarOpenEnum.Closed;
|
||||
}
|
||||
|
||||
url(url: string): SafeStyle {
|
||||
return this.sanitizer.bypassSecurityTrustStyle(`url('${url}')`);
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
this.authService.logout();
|
||||
this.router.navigateByUrl('/');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,53 @@
|
|||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { HttpClientModule, HttpClient, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { MaterialModule } from './modules/material/material.module';
|
||||
import { AppService } from './modules/shared/services/app/app.service';
|
||||
import { ThemeService } from './modules/shared/services/theme/theme.service';
|
||||
import { BrowserStorageService } from './modules/shared/services/browser-storage/browser-storage.service';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
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 {
|
||||
return new TranslateHttpLoader(http, '/assets/lang/', '.json');
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
AppComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
AppRoutingModule
|
||||
AppRoutingModule,
|
||||
BrowserAnimationsModule,
|
||||
MaterialModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
HttpClientModule,
|
||||
TranslateModule.forRoot({
|
||||
defaultLanguage: 'en',
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useFactory: HttpLoaderFactory,
|
||||
deps: [HttpClient]
|
||||
}
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
AppService,
|
||||
ThemeService,
|
||||
BrowserStorageService,
|
||||
AuthTokenService,
|
||||
AuthService,
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, multi: true }
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Route } from '@angular/router';
|
||||
import { NotFoundComponent } from '../shared/components/not-found/not-found.component';
|
||||
import { UsersComponent } from './components/users/users.component';
|
||||
|
||||
const routes: Route[] = [
|
||||
{
|
||||
path: '',
|
||||
component: NotFoundComponent,
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
component: UsersComponent,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AdminRoutingModule { }
|
|
@ -0,0 +1,23 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { AdminRoutingModule } from './admin-routing.module';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { UsersComponent } from './components/users/users.component';
|
||||
import { AdminUserService } from './services/admin-user/admin-user.service';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
UsersComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
AdminRoutingModule,
|
||||
SharedModule,
|
||||
],
|
||||
providers: [
|
||||
AdminUserService,
|
||||
],
|
||||
})
|
||||
export class AdminModule { }
|
|
@ -0,0 +1 @@
|
|||
<p>users works!</p>
|
|
@ -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(() => {
|
||||
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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 { }
|
|
@ -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 { }
|
|
@ -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>
|
|
@ -0,0 +1,9 @@
|
|||
.primary-background{
|
||||
background-color: var(--toolbar-background);
|
||||
height: 200px;
|
||||
}
|
||||
.content{
|
||||
margin: 0px auto;
|
||||
margin-top: -150px;
|
||||
height: 200px;
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export enum AuthTabEnum {
|
||||
Login = 'login',
|
||||
Register = 'register',
|
||||
RestorePassword = 'restore-password',
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export interface LoginModel {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
export interface RegisterModel {
|
||||
name: string;
|
||||
surname: string;
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export interface RestoreModel {
|
||||
email: string;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export interface TokenResponse {
|
||||
token: string;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
export interface UserModel {
|
||||
id: number;
|
||||
name: string;
|
||||
surname: string;
|
||||
avatar: string | null;
|
||||
country: string | null;
|
||||
state: string | null;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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']);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {MatToolbarModule} from '@angular/material/toolbar';
|
||||
import {MatButtonModule} from '@angular/material/button';
|
||||
import {MatSidenavModule} from '@angular/material/sidenav';
|
||||
import {MatSelectModule} from '@angular/material/select';
|
||||
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 = [
|
||||
MatToolbarModule,
|
||||
MatButtonModule,
|
||||
MatSidenavModule,
|
||||
MatSelectModule,
|
||||
MatMenuModule,
|
||||
MatCardModule,
|
||||
MatTabsModule,
|
||||
MatInputModule,
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [
|
||||
CommonModule,
|
||||
...itemsToExport,
|
||||
],
|
||||
exports: [
|
||||
...itemsToExport,
|
||||
]
|
||||
})
|
||||
export class MaterialModule { }
|
|
@ -0,0 +1,10 @@
|
|||
<div class="primary-background toolbar-background"></div>
|
||||
<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>
|
|
@ -0,0 +1,10 @@
|
|||
.primary-background{
|
||||
background-color: var(--toolbar-background);
|
||||
height: 100px;
|
||||
}
|
||||
.content{
|
||||
margin: 0px auto;
|
||||
margin-top: -100px;
|
||||
height: 200px;
|
||||
display: flex;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-not-found',
|
||||
templateUrl: './not-found.component.html',
|
||||
styleUrls: ['./not-found.component.scss']
|
||||
})
|
||||
export class NotFoundComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
export class User {
|
||||
id: number;
|
||||
email: string;
|
||||
name: string;
|
||||
surname: string;
|
||||
avatar: string;
|
||||
country: string;
|
||||
state: string;
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { fromEvent, Subject } from 'rxjs';
|
||||
import { BrowserStorageService } from '../browser-storage/browser-storage.service';
|
||||
|
||||
export class WindowSize {
|
||||
isMobile: boolean;
|
||||
|
||||
constructor(
|
||||
public width: number,
|
||||
public height: number,
|
||||
) {
|
||||
this.isMobile = this.width <= 700;
|
||||
}
|
||||
|
||||
static generate(): WindowSize {
|
||||
return new WindowSize(window.innerWidth, window.innerHeight);
|
||||
}
|
||||
|
||||
update(): WindowSize {
|
||||
this.width = window.innerWidth;
|
||||
this.height = window.innerHeight;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
private sidebarHidden = false;
|
||||
private lang = 'en';
|
||||
private defaultLang = 'en';
|
||||
private dynamicToolbarComponents = [];
|
||||
langChange = new Subject<string>();
|
||||
size: WindowSize;
|
||||
sizeChange = new Subject<WindowSize>();
|
||||
sidebarHiddenChange = new Subject<boolean>();
|
||||
dynamicToolbarComponentsChange = new Subject<any[]>();
|
||||
|
||||
constructor(
|
||||
private browserStorageService: BrowserStorageService,
|
||||
private translate: TranslateService,
|
||||
) {
|
||||
this.configureResizeEvents();
|
||||
this.configureLanguageEvents();
|
||||
this.configureSidebarEvents();
|
||||
}
|
||||
|
||||
configureSidebarEvents(): void {
|
||||
this.sidebarHiddenChange.subscribe(hidden => {
|
||||
this.sidebarHidden = hidden;
|
||||
});
|
||||
}
|
||||
|
||||
configureResizeEvents(): void {
|
||||
this.sizeChange.subscribe(size => this.size = size);
|
||||
this.sizeChange.next(WindowSize.generate());
|
||||
fromEvent(window, 'resize').subscribe(e => {
|
||||
this.sizeChange.next(WindowSize.generate());
|
||||
});
|
||||
}
|
||||
|
||||
configureLanguageEvents(): void {
|
||||
this.langChange.subscribe(lang => {
|
||||
this.lang = lang;
|
||||
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',
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class BrowserStorageService {
|
||||
|
||||
private storage: Storage;
|
||||
private namespace: string;
|
||||
|
||||
constructor() {
|
||||
this.storage = localStorage;
|
||||
this.namespace = 'default';
|
||||
}
|
||||
|
||||
setNamespace(namespace: string): void {
|
||||
this.namespace = namespace;
|
||||
}
|
||||
|
||||
getNamespace(namespace: string | null = null): string {
|
||||
return namespace ?? this.namespace;
|
||||
}
|
||||
|
||||
setItem(key: string, value: any, namespace: string | null = null): void {
|
||||
namespace = this.getNamespace(namespace);
|
||||
const path = `${namespace}.${key}`;
|
||||
this.storage.setItem(path, JSON.stringify(value));
|
||||
}
|
||||
|
||||
getItem(key: string, namespace: string | null = null): any {
|
||||
namespace = this.getNamespace(namespace);
|
||||
const path = `${namespace}.${key}`;
|
||||
return JSON.parse(this.storage.getItem(path) ?? 'null');
|
||||
}
|
||||
|
||||
getItemOrDefault(key: string, defaultValue: any, namespace: string | null = null): any {
|
||||
return this.getItem(key, namespace) ?? defaultValue;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { BrowserStorageService } from '../browser-storage/browser-storage.service';
|
||||
|
||||
@Injectable()
|
||||
export class ThemeService {
|
||||
private theme: string;
|
||||
themeChange = new Subject<string>();
|
||||
|
||||
constructor(
|
||||
browserStorageService: BrowserStorageService
|
||||
) {
|
||||
this.themeChange.subscribe(theme => {
|
||||
this.theme = theme;
|
||||
browserStorageService.setItem('current-theme', theme);
|
||||
});
|
||||
let userTheme = browserStorageService.getItem('current-theme');
|
||||
if (!userTheme) {
|
||||
userTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
this.setTheme(userTheme);
|
||||
}
|
||||
|
||||
getTheme(): string {
|
||||
return this.theme;
|
||||
}
|
||||
|
||||
setTheme(theme: string): void {
|
||||
if (theme !== this.theme) {
|
||||
this.themeChange.next(theme);
|
||||
document.getElementById('current-theme').setAttribute('href', `/${theme}.css`);
|
||||
}
|
||||
}
|
||||
|
||||
getThemes(): string[] {
|
||||
return [
|
||||
'light',
|
||||
'dark',
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NotFoundComponent } from './components/not-found/not-found.component';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { HttpLoaderFactory } from 'src/app/app.module';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { MaterialModule } from '../material/material.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
NotFoundComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
MaterialModule,
|
||||
TranslateModule.forRoot({
|
||||
defaultLanguage: 'en',
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useFactory: HttpLoaderFactory,
|
||||
deps: [HttpClient]
|
||||
}
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class SharedModule { }
|
|
@ -0,0 +1,38 @@
|
|||
@use 'sass:map';
|
||||
|
||||
$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 {
|
||||
--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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,398 @@
|
|||
$theme: (
|
||||
color: (
|
||||
primary: (
|
||||
50: #e8eaf6,
|
||||
100: #c5cae9,
|
||||
200: #9fa8da,
|
||||
300: #7986cb,
|
||||
400: #5c6bc0,
|
||||
500: #3f51b5,
|
||||
600: #3949ab,
|
||||
700: #303f9f,
|
||||
800: #283593,
|
||||
900: #1a237e,
|
||||
A100: #8c9eff,
|
||||
A200: #536dfe,
|
||||
A400: #3d5afe,
|
||||
A700: #304ffe,
|
||||
contrast: (
|
||||
50: rgba(0, 0, 0, 0.87),
|
||||
100: rgba(0, 0, 0, 0.87),
|
||||
200: rgba(0, 0, 0, 0.87),
|
||||
300: white,
|
||||
400: white,
|
||||
500: white,
|
||||
600: white,
|
||||
700: white,
|
||||
800: white,
|
||||
900: white,
|
||||
A100: rgba(0, 0, 0, 0.87),
|
||||
A200: white,
|
||||
A400: white,
|
||||
A700: white,
|
||||
),
|
||||
default: #3f51b5,
|
||||
lighter: #c5cae9,
|
||||
darker: #303f9f,
|
||||
text: #3f51b5,
|
||||
default-contrast: white,
|
||||
lighter-contrast: rgba(0, 0, 0, 0.87),
|
||||
darker-contrast: white,
|
||||
"50-contrast": rgba(0, 0, 0, 0.87),
|
||||
"100-contrast": rgba(0, 0, 0, 0.87),
|
||||
"200-contrast": rgba(0, 0, 0, 0.87),
|
||||
"300-contrast": white,
|
||||
"400-contrast": white,
|
||||
"500-contrast": white,
|
||||
"600-contrast": white,
|
||||
"700-contrast": white,
|
||||
"800-contrast": white,
|
||||
"900-contrast": white,
|
||||
"A100-contrast": rgba(0, 0, 0, 0.87),
|
||||
"A200-contrast": white,
|
||||
"A400-contrast": white,
|
||||
"A700-contrast": white,
|
||||
"contrast-contrast": null,
|
||||
),
|
||||
accent: (
|
||||
50: #fce4ec,
|
||||
100: #f8bbd0,
|
||||
200: #f48fb1,
|
||||
300: #f06292,
|
||||
400: #ec407a,
|
||||
500: #e91e63,
|
||||
600: #d81b60,
|
||||
700: #c2185b,
|
||||
800: #ad1457,
|
||||
900: #880e4f,
|
||||
A100: #ff80ab,
|
||||
A200: #ff4081,
|
||||
A400: #f50057,
|
||||
A700: #c51162,
|
||||
contrast: (
|
||||
50: rgba(0, 0, 0, 0.87),
|
||||
100: rgba(0, 0, 0, 0.87),
|
||||
200: rgba(0, 0, 0, 0.87),
|
||||
300: rgba(0, 0, 0, 0.87),
|
||||
400: rgba(0, 0, 0, 0.87),
|
||||
500: white,
|
||||
600: white,
|
||||
700: white,
|
||||
800: white,
|
||||
900: white,
|
||||
A100: rgba(0, 0, 0, 0.87),
|
||||
A200: white,
|
||||
A400: white,
|
||||
A700: white,
|
||||
),
|
||||
default: #ff4081,
|
||||
lighter: #ff80ab,
|
||||
darker: #f50057,
|
||||
text: #ff4081,
|
||||
default-contrast: white,
|
||||
lighter-contrast: rgba(0, 0, 0, 0.87),
|
||||
darker-contrast: white,
|
||||
"50-contrast": rgba(0, 0, 0, 0.87),
|
||||
"100-contrast": rgba(0, 0, 0, 0.87),
|
||||
"200-contrast": rgba(0, 0, 0, 0.87),
|
||||
"300-contrast": rgba(0, 0, 0, 0.87),
|
||||
"400-contrast": rgba(0, 0, 0, 0.87),
|
||||
"500-contrast": white,
|
||||
"600-contrast": white,
|
||||
"700-contrast": white,
|
||||
"800-contrast": white,
|
||||
"900-contrast": white,
|
||||
"A100-contrast": rgba(0, 0, 0, 0.87),
|
||||
"A200-contrast": white,
|
||||
"A400-contrast": white,
|
||||
"A700-contrast": white,
|
||||
"contrast-contrast": null,
|
||||
),
|
||||
warn: (
|
||||
50: #ffebee,
|
||||
100: #ffcdd2,
|
||||
200: #ef9a9a,
|
||||
300: #e57373,
|
||||
400: #ef5350,
|
||||
500: #f44336,
|
||||
600: #e53935,
|
||||
700: #d32f2f,
|
||||
800: #c62828,
|
||||
900: #b71c1c,
|
||||
A100: #ff8a80,
|
||||
A200: #ff5252,
|
||||
A400: #ff1744,
|
||||
A700: #d50000,
|
||||
contrast: (
|
||||
50: rgba(0, 0, 0, 0.87),
|
||||
100: rgba(0, 0, 0, 0.87),
|
||||
200: rgba(0, 0, 0, 0.87),
|
||||
300: rgba(0, 0, 0, 0.87),
|
||||
400: rgba(0, 0, 0, 0.87),
|
||||
500: white,
|
||||
600: white,
|
||||
700: white,
|
||||
800: white,
|
||||
900: white,
|
||||
A100: rgba(0, 0, 0, 0.87),
|
||||
A200: white,
|
||||
A400: white,
|
||||
A700: white,
|
||||
),
|
||||
default: #f44336,
|
||||
lighter: #ffcdd2,
|
||||
darker: #d32f2f,
|
||||
text: #f44336,
|
||||
default-contrast: white,
|
||||
lighter-contrast: rgba(0, 0, 0, 0.87),
|
||||
darker-contrast: white,
|
||||
"50-contrast": rgba(0, 0, 0, 0.87),
|
||||
"100-contrast": rgba(0, 0, 0, 0.87),
|
||||
"200-contrast": rgba(0, 0, 0, 0.87),
|
||||
"300-contrast": rgba(0, 0, 0, 0.87),
|
||||
"400-contrast": rgba(0, 0, 0, 0.87),
|
||||
"500-contrast": white,
|
||||
"600-contrast": white,
|
||||
"700-contrast": white,
|
||||
"800-contrast": white,
|
||||
"900-contrast": white,
|
||||
"A100-contrast": rgba(0, 0, 0, 0.87),
|
||||
"A200-contrast": white,
|
||||
"A400-contrast": white,
|
||||
"A700-contrast": white,
|
||||
"contrast-contrast": null,
|
||||
),
|
||||
is-dark: true,
|
||||
foreground: (
|
||||
base: white,
|
||||
divider: rgba(255, 255, 255, 0.12),
|
||||
dividers: rgba(255, 255, 255, 0.12),
|
||||
disabled: rgba(255, 255, 255, 0.5),
|
||||
disabled-button: rgba(255, 255, 255, 0.3),
|
||||
disabled-text: rgba(255, 255, 255, 0.5),
|
||||
elevation: black,
|
||||
hint-text: rgba(255, 255, 255, 0.5),
|
||||
secondary-text: rgba(255, 255, 255, 0.7),
|
||||
icon: white,
|
||||
icons: white,
|
||||
text: white,
|
||||
slider-min: white,
|
||||
slider-off: rgba(255, 255, 255, 0.3),
|
||||
slider-off-active: rgba(255, 255, 255, 0.3),
|
||||
),
|
||||
background: (
|
||||
status-bar: black,
|
||||
app-bar: #212121,
|
||||
background: #303030,
|
||||
hover: rgba(255, 255, 255, 0.04),
|
||||
card: #424242,
|
||||
dialog: #424242,
|
||||
disabled-button: rgba(255, 255, 255, 0.12),
|
||||
raised-button: #424242,
|
||||
focused-button: rgba(255, 255, 255, 0.12),
|
||||
selected-button: #212121,
|
||||
selected-disabled-button: #424242,
|
||||
disabled-button-toggle: black,
|
||||
unselected-chip: #616161,
|
||||
disabled-list-option: black,
|
||||
tooltip: #616161,
|
||||
),
|
||||
),
|
||||
primary: (
|
||||
50: #e8eaf6,
|
||||
100: #c5cae9,
|
||||
200: #9fa8da,
|
||||
300: #7986cb,
|
||||
400: #5c6bc0,
|
||||
500: #3f51b5,
|
||||
600: #3949ab,
|
||||
700: #303f9f,
|
||||
800: #283593,
|
||||
900: #1a237e,
|
||||
A100: #8c9eff,
|
||||
A200: #536dfe,
|
||||
A400: #3d5afe,
|
||||
A700: #304ffe,
|
||||
contrast: (
|
||||
50: rgba(0, 0, 0, 0.87),
|
||||
100: rgba(0, 0, 0, 0.87),
|
||||
200: rgba(0, 0, 0, 0.87),
|
||||
300: white,
|
||||
400: white,
|
||||
500: white,
|
||||
600: white,
|
||||
700: white,
|
||||
800: white,
|
||||
900: white,
|
||||
A100: rgba(0, 0, 0, 0.87),
|
||||
A200: white,
|
||||
A400: white,
|
||||
A700: white,
|
||||
),
|
||||
default: #3f51b5,
|
||||
lighter: #c5cae9,
|
||||
darker: #303f9f,
|
||||
text: #3f51b5,
|
||||
default-contrast: white,
|
||||
lighter-contrast: rgba(0, 0, 0, 0.87),
|
||||
darker-contrast: white,
|
||||
"50-contrast": rgba(0, 0, 0, 0.87),
|
||||
"100-contrast": rgba(0, 0, 0, 0.87),
|
||||
"200-contrast": rgba(0, 0, 0, 0.87),
|
||||
"300-contrast": white,
|
||||
"400-contrast": white,
|
||||
"500-contrast": white,
|
||||
"600-contrast": white,
|
||||
"700-contrast": white,
|
||||
"800-contrast": white,
|
||||
"900-contrast": white,
|
||||
"A100-contrast": rgba(0, 0, 0, 0.87),
|
||||
"A200-contrast": white,
|
||||
"A400-contrast": white,
|
||||
"A700-contrast": white,
|
||||
"contrast-contrast": null,
|
||||
),
|
||||
accent: (
|
||||
50: #fce4ec,
|
||||
100: #f8bbd0,
|
||||
200: #f48fb1,
|
||||
300: #f06292,
|
||||
400: #ec407a,
|
||||
500: #e91e63,
|
||||
600: #d81b60,
|
||||
700: #c2185b,
|
||||
800: #ad1457,
|
||||
900: #880e4f,
|
||||
A100: #ff80ab,
|
||||
A200: #ff4081,
|
||||
A400: #f50057,
|
||||
A700: #c51162,
|
||||
contrast: (
|
||||
50: rgba(0, 0, 0, 0.87),
|
||||
100: rgba(0, 0, 0, 0.87),
|
||||
200: rgba(0, 0, 0, 0.87),
|
||||
300: rgba(0, 0, 0, 0.87),
|
||||
400: rgba(0, 0, 0, 0.87),
|
||||
500: white,
|
||||
600: white,
|
||||
700: white,
|
||||
800: white,
|
||||
900: white,
|
||||
A100: rgba(0, 0, 0, 0.87),
|
||||
A200: white,
|
||||
A400: white,
|
||||
A700: white,
|
||||
),
|
||||
default: #ff4081,
|
||||
lighter: #ff80ab,
|
||||
darker: #f50057,
|
||||
text: #ff4081,
|
||||
default-contrast: white,
|
||||
lighter-contrast: rgba(0, 0, 0, 0.87),
|
||||
darker-contrast: white,
|
||||
"50-contrast": rgba(0, 0, 0, 0.87),
|
||||
"100-contrast": rgba(0, 0, 0, 0.87),
|
||||
"200-contrast": rgba(0, 0, 0, 0.87),
|
||||
"300-contrast": rgba(0, 0, 0, 0.87),
|
||||
"400-contrast": rgba(0, 0, 0, 0.87),
|
||||
"500-contrast": white,
|
||||
"600-contrast": white,
|
||||
"700-contrast": white,
|
||||
"800-contrast": white,
|
||||
"900-contrast": white,
|
||||
"A100-contrast": rgba(0, 0, 0, 0.87),
|
||||
"A200-contrast": white,
|
||||
"A400-contrast": white,
|
||||
"A700-contrast": white,
|
||||
"contrast-contrast": null,
|
||||
),
|
||||
warn: (
|
||||
50: #ffebee,
|
||||
100: #ffcdd2,
|
||||
200: #ef9a9a,
|
||||
300: #e57373,
|
||||
400: #ef5350,
|
||||
500: #f44336,
|
||||
600: #e53935,
|
||||
700: #d32f2f,
|
||||
800: #c62828,
|
||||
900: #b71c1c,
|
||||
A100: #ff8a80,
|
||||
A200: #ff5252,
|
||||
A400: #ff1744,
|
||||
A700: #d50000,
|
||||
contrast: (
|
||||
50: rgba(0, 0, 0, 0.87),
|
||||
100: rgba(0, 0, 0, 0.87),
|
||||
200: rgba(0, 0, 0, 0.87),
|
||||
300: rgba(0, 0, 0, 0.87),
|
||||
400: rgba(0, 0, 0, 0.87),
|
||||
500: white,
|
||||
600: white,
|
||||
700: white,
|
||||
800: white,
|
||||
900: white,
|
||||
A100: rgba(0, 0, 0, 0.87),
|
||||
A200: white,
|
||||
A400: white,
|
||||
A700: white,
|
||||
),
|
||||
default: #f44336,
|
||||
lighter: #ffcdd2,
|
||||
darker: #d32f2f,
|
||||
text: #f44336,
|
||||
default-contrast: white,
|
||||
lighter-contrast: rgba(0, 0, 0, 0.87),
|
||||
darker-contrast: white,
|
||||
"50-contrast": rgba(0, 0, 0, 0.87),
|
||||
"100-contrast": rgba(0, 0, 0, 0.87),
|
||||
"200-contrast": rgba(0, 0, 0, 0.87),
|
||||
"300-contrast": rgba(0, 0, 0, 0.87),
|
||||
"400-contrast": rgba(0, 0, 0, 0.87),
|
||||
"500-contrast": white,
|
||||
"600-contrast": white,
|
||||
"700-contrast": white,
|
||||
"800-contrast": white,
|
||||
"900-contrast": white,
|
||||
"A100-contrast": rgba(0, 0, 0, 0.87),
|
||||
"A200-contrast": white,
|
||||
"A400-contrast": white,
|
||||
"A700-contrast": white,
|
||||
"contrast-contrast": null,
|
||||
),
|
||||
is-dark: true,
|
||||
foreground: (
|
||||
base: white,
|
||||
divider: rgba(255, 255, 255, 0.12),
|
||||
dividers: rgba(255, 255, 255, 0.12),
|
||||
disabled: rgba(255, 255, 255, 0.5),
|
||||
disabled-button: rgba(255, 255, 255, 0.3),
|
||||
disabled-text: rgba(255, 255, 255, 0.5),
|
||||
elevation: black,
|
||||
hint-text: rgba(255, 255, 255, 0.5),
|
||||
secondary-text: rgba(255, 255, 255, 0.7),
|
||||
icon: white,
|
||||
icons: white,
|
||||
text: white,
|
||||
slider-min: white,
|
||||
slider-off: rgba(255, 255, 255, 0.3),
|
||||
slider-off-active: rgba(255, 255, 255, 0.3),
|
||||
),
|
||||
background: (
|
||||
status-bar: black,
|
||||
app-bar: #212121,
|
||||
background: #303030,
|
||||
hover: rgba(255, 255, 255, 0.04),
|
||||
card: #424242,
|
||||
dialog: #424242,
|
||||
disabled-button: rgba(255, 255, 255, 0.12),
|
||||
raised-button: #424242,
|
||||
focused-button: rgba(255, 255, 255, 0.12),
|
||||
selected-button: #212121,
|
||||
selected-disabled-button: #424242,
|
||||
disabled-button-toggle: black,
|
||||
unselected-chip: #616161,
|
||||
disabled-list-option: black,
|
||||
tooltip: #616161,
|
||||
),
|
||||
);
|
|
@ -0,0 +1,35 @@
|
|||
// Custom Theming for Angular Material
|
||||
// For more information: https://material.angular.io/guide/theming
|
||||
@use '~@angular/material' as mat;
|
||||
// Plus imports for other components in your app.
|
||||
|
||||
// Include the common styles for Angular Material. We include this here so that you only
|
||||
// have to load a single css file for Angular Material in your app.
|
||||
// Be sure that you only ever include this mixin once!
|
||||
@include mat.core();
|
||||
|
||||
// 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
|
||||
// hue. Available color palettes: https://material.io/design/color/
|
||||
$theme-primary: mat.define-palette(mat.$indigo-palette);
|
||||
$theme-accent: mat.define-palette(mat.$green-palette, A200, A100, A400);
|
||||
|
||||
// The warn palette is optional (defaults to red).
|
||||
$theme-warn: mat.define-palette(mat.$red-palette);
|
||||
|
||||
// Create the theme object. A theme consists of configurations for individual
|
||||
// theming systems such as "color" or "typography".
|
||||
$theme: mat.define-dark-theme((
|
||||
color: (
|
||||
primary: $theme-primary,
|
||||
accent: $theme-accent,
|
||||
warn: $theme-warn,
|
||||
)
|
||||
));
|
||||
|
||||
// Include theme styles for core and each component used in your app.
|
||||
// Alternatively, you can import and @include the theme mixins for each component
|
||||
// that you are using.
|
||||
@include mat.all-component-themes($theme);
|
||||
|
||||
@import "./base";
|
|
@ -0,0 +1,40 @@
|
|||
@use "sass:map";
|
||||
// Custom Theming for Angular Material
|
||||
// For more information: https://material.angular.io/guide/theming
|
||||
@use '~@angular/material' as mat;
|
||||
// Plus imports for other components in your app.
|
||||
|
||||
// Include the common styles for Angular Material. We include this here so that you only
|
||||
// have to load a single css file for Angular Material in your app.
|
||||
// Be sure that you only ever include this mixin once!
|
||||
@include mat.core();
|
||||
|
||||
// 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
|
||||
// hue. Available color palettes: https://material.io/design/color/
|
||||
$theme-primary: mat.define-palette(mat.$blue-palette);
|
||||
$theme-accent: mat.define-palette(mat.$light-blue-palette, A200, A100, A400);
|
||||
|
||||
// The warn palette is optional (defaults to red).
|
||||
$theme-warn: mat.define-palette(mat.$red-palette);
|
||||
|
||||
// Create the theme object. A theme consists of configurations for individual
|
||||
// theming systems such as "color" or "typography".
|
||||
$theme: mat.define-light-theme((
|
||||
color: (
|
||||
primary: $theme-primary,
|
||||
accent: $theme-accent,
|
||||
warn: $theme-warn,
|
||||
)
|
||||
));
|
||||
|
||||
$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.
|
||||
// Alternatively, you can import and @include the theme mixins for each component
|
||||
// that you are using.
|
||||
@include mat.all-component-themes($theme);
|
||||
|
||||
@import "./base";
|
Binary file not shown.
After Width: | Height: | Size: 104 KiB |
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"APP": {
|
||||
"NAME": "CureNet",
|
||||
"THEME": "Theme",
|
||||
"LANGUAGE": "Language"
|
||||
},
|
||||
"ERROR": {
|
||||
"PAGE_NOT_FOUND": "Page not found or not implemented yet!"
|
||||
},
|
||||
"THEME": {
|
||||
"LIGHT": "Light",
|
||||
"DARK": "Dark"
|
||||
},
|
||||
"LANGUAGE": {
|
||||
"PL": "Polish",
|
||||
"EN": "English"
|
||||
},
|
||||
"MENU": {
|
||||
"HOME": "Dashboard",
|
||||
"PROFILE": "Profile",
|
||||
"COMMUNITY": "Community",
|
||||
"GROUP": "My group",
|
||||
"MESSAGES": "Messages",
|
||||
"QUESTIONS": "Q&A",
|
||||
"TESTS": "Laboratory tests",
|
||||
"RESEARCH": "Research",
|
||||
"ADMIN": "Admin",
|
||||
"USERS": "Users"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"APP": {
|
||||
"NAME": "CureNet",
|
||||
"THEME": "Motyw",
|
||||
"LANGUAGE": "Język"
|
||||
},
|
||||
"ERROR": {
|
||||
"PAGE_NOT_FOUND": "Nie znaleziono strony lub nie została ona jeszcze zaimplementowana!"
|
||||
},
|
||||
"THEME": {
|
||||
"LIGHT": "Jasny",
|
||||
"DARK": "Ciemny"
|
||||
},
|
||||
"LANGUAGE": {
|
||||
"PL": "Polski",
|
||||
"EN": "Angielski"
|
||||
},
|
||||
"MENU": {
|
||||
"HOME": "Kokpit",
|
||||
"PROFILE": "Profil",
|
||||
"COMMUNITY": "Społeczność",
|
||||
"GROUP": "Moja grupa",
|
||||
"MESSAGES": "Wiadomości",
|
||||
"QUESTIONS": "Pytania i odpowiedzi",
|
||||
"TESTS": "Wyniki badań",
|
||||
"RESEARCH": "Badania naukowe",
|
||||
"ADMIN": "Panel administratora",
|
||||
"USERS": "Użytkownicy"
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
export const environment = {
|
||||
production: true
|
||||
production: true,
|
||||
buildId: -1001,
|
||||
buildTime: -1002,
|
||||
};
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
// The list of file replacements can be found in `angular.json`.
|
||||
|
||||
export const environment = {
|
||||
production: false
|
||||
production: false,
|
||||
buildId: -1001,
|
||||
buildTime: -1002,
|
||||
};
|
||||
|
||||
/*
|
||||
|
|
|
@ -2,10 +2,14 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Frontend</title>
|
||||
<title>CureNet</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link rel="stylesheet" href="/light.css" id="current-theme">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
|
|
|
@ -1 +1,50 @@
|
|||
/* You can add global styles to this file, and also import other style files */
|
||||
|
||||
html, body { height: 100%; }
|
||||
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
|
||||
|
||||
@import "~bootstrap/dist/css/bootstrap.css";
|
||||
@import "~font-awesome/css/font-awesome.css";
|
||||
|
||||
body .mat-drawer.mat-drawer-side {
|
||||
visibility: visible !important;
|
||||
transform: none !important;
|
||||
width: 250px;
|
||||
&,
|
||||
& .mat-drawer-inner-container{
|
||||
transition-duration: 400ms !important;
|
||||
transition-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !important;
|
||||
transition-property: transform, margin-left, margin-right, width !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
& ~ .mat-drawer-content{
|
||||
margin-left: 250px !important;
|
||||
}
|
||||
&:not(.mat-drawer-opened) {
|
||||
width: 60px;
|
||||
.menu-item{
|
||||
margin-left: 0px !important;
|
||||
&:after{
|
||||
right: 190px;
|
||||
}
|
||||
& > i{
|
||||
width: 40px;
|
||||
font-size: 120%;
|
||||
}
|
||||
}
|
||||
.hide-on-side-opened{
|
||||
display: none;
|
||||
}
|
||||
& ~ .mat-drawer-content {
|
||||
margin-left: 60px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.w-220{
|
||||
width: 220px;
|
||||
}
|
||||
[required]:after{
|
||||
content: "*";
|
||||
font-weight: bold;
|
||||
color: var(--warn-color);
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
name=value
|
||||
name2="value 2"
|
||||
#name3=value3
|
||||
name4 = value4
|
||||
name5 = "value 5"
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
namespace Tools;
|
||||
|
||||
class PutEnv {
|
||||
private string $file;
|
||||
private string $key;
|
||||
private string $value;
|
||||
private array $content;
|
||||
public function __construct() {
|
||||
$argv = $_SERVER['argv'];
|
||||
$argc = sizeof($argv);
|
||||
switch($argc) {
|
||||
case 4:
|
||||
$this->file = $argv[1];
|
||||
$this->key = $argv[2];
|
||||
$this->value = $argv[3];
|
||||
break;
|
||||
|
||||
case 3:
|
||||
$this->file = $argv[1];
|
||||
$this->key = $argv[2];
|
||||
$this->value = fgets(STDIN);
|
||||
break;
|
||||
|
||||
default:
|
||||
echo "usage:
|
||||
php8.0 putenv.php filename key value
|
||||
echo value | php8.0 putenv filename key";
|
||||
throw new Exception("invalid params");
|
||||
}
|
||||
if (!file_exists($this->file)) {
|
||||
throw new Exception('File not found');
|
||||
}
|
||||
$changed = 0;
|
||||
$lineWasEmpty = false;
|
||||
$key = $this->key;
|
||||
$value = $this->value;
|
||||
if (strpos($value, ' ') !== false) {
|
||||
$value = '"' . addslashes($value) . '"';
|
||||
}
|
||||
$this->content = file($this->file);
|
||||
foreach ($this->content as $l => $line) {
|
||||
if (strlen(trim($line)) < 1) {
|
||||
if ($lineWasEmpty) {
|
||||
$this->content[$l] = null;
|
||||
} else {
|
||||
$lineWasEmpty = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$lineWasEmpty = false;
|
||||
if (!str_starts_with($line, $this->key)) {
|
||||
continue;
|
||||
}
|
||||
$next = trim(substr($line, strlen($this->key)));
|
||||
if (strlen($next) === 0 || $next[0] !== '=') {
|
||||
continue;
|
||||
}
|
||||
$this->content[$l] = $key . '=' . $value;
|
||||
$changed++;
|
||||
}
|
||||
if ($changed === 0) {
|
||||
$this->content[] = $key . '=' . $value;
|
||||
} else if ($changed > 1) {
|
||||
echo "Key $key changed $changed times\n";
|
||||
}
|
||||
$content = $this->content;
|
||||
$this->content = [];
|
||||
foreach ($content as $line) {
|
||||
if ($line !== null) {
|
||||
$this->content[] = $line;
|
||||
}
|
||||
}
|
||||
$content = implode('', $this->content);
|
||||
file_put_contents($this->file, $content);
|
||||
}
|
||||
}
|
||||
|
||||
new PutEnv();
|
Loading…
Reference in New Issue