forked from sh-edraft.de/sh_discord_bot
Repaired register by discord #70
This commit is contained in:
parent
602b1f3f98
commit
ffe7b5e109
@ -12,6 +12,12 @@
|
|||||||
"FileLogLevel": "TRACE"
|
"FileLogLevel": "TRACE"
|
||||||
},
|
},
|
||||||
"BotLoggingSettings": {
|
"BotLoggingSettings": {
|
||||||
|
"Api": {
|
||||||
|
"Path": "logs/",
|
||||||
|
"Filename": "api.log",
|
||||||
|
"ConsoleLogLevel": "TRACE",
|
||||||
|
"FileLogLevel": "TRACE"
|
||||||
|
},
|
||||||
"Command": {
|
"Command": {
|
||||||
"Path": "logs/",
|
"Path": "logs/",
|
||||||
"Filename": "commands.log",
|
"Filename": "commands.log",
|
||||||
@ -21,7 +27,7 @@
|
|||||||
"Database": {
|
"Database": {
|
||||||
"Path": "logs/",
|
"Path": "logs/",
|
||||||
"Filename": "database.log",
|
"Filename": "database.log",
|
||||||
"ConsoleLogLevel": "DEBUG",
|
"ConsoleLogLevel": "TRACE",
|
||||||
"FileLogLevel": "TRACE"
|
"FileLogLevel": "TRACE"
|
||||||
},
|
},
|
||||||
"Message": {
|
"Message": {
|
||||||
|
@ -50,7 +50,7 @@ class AuthServiceABC(ABC):
|
|||||||
async def add_auth_user_by_oauth_async(self, dto: OAuthDTO): pass
|
async def add_auth_user_by_oauth_async(self, dto: OAuthDTO): pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def add_auth_user_by_discord_async(self, user_dto: AuthUserDTO) -> OAuthDTO: pass
|
async def add_auth_user_by_discord_async(self, user_dto: AuthUserDTO, dc_id: int) -> OAuthDTO: pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def update_user_async(self, update_user_dto: UpdateAuthUserDTO): pass
|
async def update_user_async(self, update_user_dto: UpdateAuthUserDTO): pass
|
||||||
|
@ -1 +1,28 @@
|
|||||||
{}
|
{
|
||||||
|
"Api": {
|
||||||
|
"Port": 5000,
|
||||||
|
"Host": "0.0.0.0",
|
||||||
|
"RedirectToHTTPS": false
|
||||||
|
},
|
||||||
|
"Authentication": {
|
||||||
|
"SecretKey": "RjNiNUxEeisjSnZ6Zz1XIUBnc2EleHNG",
|
||||||
|
"Issuer": "http://localhost:5000",
|
||||||
|
"Audience": "http://localhost:4200",
|
||||||
|
"TokenExpireTime": 1,
|
||||||
|
"RefreshTokenExpireTime": 7
|
||||||
|
},
|
||||||
|
"DiscordAuthentication": {
|
||||||
|
"ClientSecret": "V3FTb3JYVFBiVktEeHZxdWJDWW4xcnBCbXRwdmpwcy0=",
|
||||||
|
"_RedirectURL": "http://localhost:5000/api/auth/discord/register",
|
||||||
|
"RedirectURL": "http://localhost:4200/auth/register",
|
||||||
|
"Scope": [
|
||||||
|
"identify",
|
||||||
|
"email"
|
||||||
|
],
|
||||||
|
"TokenURL": "https://discordapp.com/api/oauth2/token",
|
||||||
|
"AuthURL": "https://discordapp.com/api/oauth2/authorize"
|
||||||
|
},
|
||||||
|
"Frontend": {
|
||||||
|
"URL": "http://localhost:4200/"
|
||||||
|
}
|
||||||
|
}
|
@ -78,9 +78,8 @@ class AuthDiscordController:
|
|||||||
response['email'],
|
response['email'],
|
||||||
str(uuid.uuid4()),
|
str(uuid.uuid4()),
|
||||||
None,
|
None,
|
||||||
AuthRoleEnum.normal,
|
AuthRoleEnum.normal
|
||||||
response['id']
|
), response['id'])
|
||||||
))
|
|
||||||
return jsonify(result.to_dict())
|
return jsonify(result.to_dict())
|
||||||
|
|
||||||
@Route.post(f'{BasePath}/register')
|
@Route.post(f'{BasePath}/register')
|
||||||
|
@ -8,8 +8,9 @@ from typing import Optional
|
|||||||
import jwt
|
import jwt
|
||||||
from cpl_core.database.context import DatabaseContextABC
|
from cpl_core.database.context import DatabaseContextABC
|
||||||
from cpl_core.environment import ApplicationEnvironmentABC
|
from cpl_core.environment import ApplicationEnvironmentABC
|
||||||
from cpl_core.mailing import EMailClientABC, EMail
|
from cpl_core.mailing import EMail
|
||||||
from cpl_core.utils import CredentialManager
|
from cpl_core.utils import CredentialManager
|
||||||
|
from cpl_discord.service import DiscordBotServiceABC
|
||||||
from cpl_query.extension import List
|
from cpl_query.extension import List
|
||||||
from cpl_translation import TranslatePipe
|
from cpl_translation import TranslatePipe
|
||||||
from flask import request
|
from flask import request
|
||||||
@ -31,9 +32,12 @@ from bot_api.model.token_dto import TokenDTO
|
|||||||
from bot_api.model.update_auth_user_dto import UpdateAuthUserDTO
|
from bot_api.model.update_auth_user_dto import UpdateAuthUserDTO
|
||||||
from bot_api.transformer.auth_user_transformer import AuthUserTransformer as AUT
|
from bot_api.transformer.auth_user_transformer import AuthUserTransformer as AUT
|
||||||
from bot_data.abc.auth_user_repository_abc import AuthUserRepositoryABC
|
from bot_data.abc.auth_user_repository_abc import AuthUserRepositoryABC
|
||||||
|
from bot_data.abc.server_repository_abc import ServerRepositoryABC
|
||||||
from bot_data.abc.user_repository_abc import UserRepositoryABC
|
from bot_data.abc.user_repository_abc import UserRepositoryABC
|
||||||
from bot_data.model.auth_role_enum import AuthRoleEnum
|
from bot_data.model.auth_role_enum import AuthRoleEnum
|
||||||
from bot_data.model.auth_user import AuthUser
|
from bot_data.model.auth_user import AuthUser
|
||||||
|
from bot_data.model.auth_user_users_relation import AuthUserUsersRelation
|
||||||
|
from bot_data.model.user import User
|
||||||
|
|
||||||
_email_regex = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
|
_email_regex = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
|
||||||
|
|
||||||
@ -44,9 +48,11 @@ class AuthService(AuthServiceABC):
|
|||||||
self,
|
self,
|
||||||
env: ApplicationEnvironmentABC,
|
env: ApplicationEnvironmentABC,
|
||||||
logger: ApiLogger,
|
logger: ApiLogger,
|
||||||
|
bot: DiscordBotServiceABC,
|
||||||
|
db: DatabaseContextABC,
|
||||||
auth_users: AuthUserRepositoryABC,
|
auth_users: AuthUserRepositoryABC,
|
||||||
users: UserRepositoryABC,
|
users: UserRepositoryABC,
|
||||||
db: DatabaseContextABC,
|
servers: ServerRepositoryABC,
|
||||||
mailer: MailThread,
|
mailer: MailThread,
|
||||||
t: TranslatePipe,
|
t: TranslatePipe,
|
||||||
auth_settings: AuthenticationSettings,
|
auth_settings: AuthenticationSettings,
|
||||||
@ -57,9 +63,11 @@ class AuthService(AuthServiceABC):
|
|||||||
|
|
||||||
self._environment = env
|
self._environment = env
|
||||||
self._logger = logger
|
self._logger = logger
|
||||||
|
self._bot = bot
|
||||||
|
self._db = db
|
||||||
self._auth_users = auth_users
|
self._auth_users = auth_users
|
||||||
self._users = users
|
self._users = users
|
||||||
self._db = db
|
self._servers = servers
|
||||||
self._mailer = mailer
|
self._mailer = mailer
|
||||||
self._t = t
|
self._t = t
|
||||||
self._auth_settings = auth_settings
|
self._auth_settings = auth_settings
|
||||||
@ -134,7 +142,7 @@ class AuthService(AuthServiceABC):
|
|||||||
def _create_and_save_refresh_token(self, user: AuthUser) -> str:
|
def _create_and_save_refresh_token(self, user: AuthUser) -> str:
|
||||||
token = str(uuid.uuid4())
|
token = str(uuid.uuid4())
|
||||||
user.refresh_token = token
|
user.refresh_token = token
|
||||||
user.refresh_token_expire_time = datetime.now(tz=timezone.utc) + timedelta(days=self._auth_settings.refresh_token_expire_time)
|
user.refresh_token_expire_time = datetime.now() + timedelta(days=self._auth_settings.refresh_token_expire_time)
|
||||||
self._auth_users.update_auth_user(user)
|
self._auth_users.update_auth_user(user)
|
||||||
self._db.save_changes()
|
self._db.save_changes()
|
||||||
return token
|
return token
|
||||||
@ -244,20 +252,21 @@ class AuthService(AuthServiceABC):
|
|||||||
self._auth_users.update_auth_user(db_user)
|
self._auth_users.update_auth_user(db_user)
|
||||||
self._db.save_changes()
|
self._db.save_changes()
|
||||||
|
|
||||||
async def add_auth_user_by_discord_async(self, user_dto: AuthUserDTO) -> OAuthDTO:
|
async def add_auth_user_by_discord_async(self, user_dto: AuthUserDTO, dc_id: int) -> OAuthDTO:
|
||||||
db_user = self._auth_users.find_auth_user_by_email(user_dto.email)
|
db_auth_user = self._auth_users.find_auth_user_by_email(user_dto.email)
|
||||||
|
|
||||||
# user exists
|
# user exists
|
||||||
if db_user is not None and db_user.user_id is not None:
|
if db_auth_user is not None and db_auth_user.users.count() > 0:
|
||||||
# raise ServiceException(ServiceErrorCode.InvalidUser, 'User already exists')
|
# raise ServiceException(ServiceErrorCode.InvalidUser, 'User already exists')
|
||||||
self._logger.debug(__name__, f'Discord user already exists')
|
self._logger.debug(__name__, f'Discord user already exists')
|
||||||
return OAuthDTO(AUT.to_dto(db_user), None)
|
return OAuthDTO(AUT.to_dto(db_auth_user), None)
|
||||||
|
|
||||||
# user exists but discord user id not set
|
# user exists but discord user id not set
|
||||||
elif db_user is not None and db_user.user_id is None:
|
elif db_auth_user is not None and db_auth_user.users.count() == 0:
|
||||||
self._logger.debug(__name__, f'Auth user exists but not linked with discord')
|
self._logger.debug(__name__, f'Auth user exists but not linked with discord')
|
||||||
user = self._users.get_users_by_discord_id(user_dto.user_id).single()
|
# users = self._users.get_users_by_discord_id(user_dto.user_id)
|
||||||
db_user.user_id = user.user_id
|
# add auth_user to user refs
|
||||||
db_user.oauth_id = None
|
db_auth_user.oauth_id = None
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# user does not exists
|
# user does not exists
|
||||||
@ -269,12 +278,24 @@ class AuthService(AuthServiceABC):
|
|||||||
user_dto.user_id = None
|
user_dto.user_id = None
|
||||||
|
|
||||||
await self.add_auth_user_async(user_dto)
|
await self.add_auth_user_async(user_dto)
|
||||||
db_user = self._auth_users.get_auth_user_by_email(user_dto.email)
|
db_auth_user = self._auth_users.get_auth_user_by_email(user_dto.email)
|
||||||
db_user.oauth_id = uuid.uuid4()
|
db_auth_user.oauth_id = uuid.uuid4()
|
||||||
|
|
||||||
self._auth_users.update_auth_user(db_user)
|
for g in self._bot.guilds:
|
||||||
|
member = g.get_member(int(dc_id))
|
||||||
|
if member is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
server = self._servers.get_server_by_discord_id(g.id)
|
||||||
|
users = self._users.get_users_by_discord_id(dc_id)
|
||||||
|
for user in users:
|
||||||
|
if user.server.server_id != server.server_id:
|
||||||
|
continue
|
||||||
|
self._auth_users.add_auth_user_user_rel(AuthUserUsersRelation(db_auth_user, user))
|
||||||
|
|
||||||
|
self._auth_users.update_auth_user(db_auth_user)
|
||||||
self._db.save_changes()
|
self._db.save_changes()
|
||||||
return OAuthDTO(AUT.to_dto(db_user), db_user.oauth_id)
|
return OAuthDTO(AUT.to_dto(db_auth_user), db_auth_user.oauth_id)
|
||||||
|
|
||||||
async def update_user_async(self, update_user_dto: UpdateAuthUserDTO):
|
async def update_user_async(self, update_user_dto: UpdateAuthUserDTO):
|
||||||
if update_user_dto is None:
|
if update_user_dto is None:
|
||||||
|
@ -20,7 +20,7 @@ class AuthUserTransformer(TransformerABC):
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
datetime.now(tz=timezone.utc),
|
datetime.now(),
|
||||||
AuthRoleEnum.normal if dto.auth_role is None else AuthRoleEnum(dto.auth_role),
|
AuthRoleEnum.normal if dto.auth_role is None else AuthRoleEnum(dto.auth_role),
|
||||||
dto.user_id,
|
dto.user_id,
|
||||||
auth_user_id=0 if dto.id is None else dto.id
|
auth_user_id=0 if dto.id is None else dto.id
|
||||||
|
@ -6,6 +6,7 @@ from cpl_query.extension import List
|
|||||||
from bot_api.filter.auth_user_select_criteria import AuthUserSelectCriteria
|
from bot_api.filter.auth_user_select_criteria import AuthUserSelectCriteria
|
||||||
from bot_data.filtered_result import FilteredResult
|
from bot_data.filtered_result import FilteredResult
|
||||||
from bot_data.model.auth_user import AuthUser
|
from bot_data.model.auth_user import AuthUser
|
||||||
|
from bot_data.model.auth_user_users_relation import AuthUserUsersRelation
|
||||||
|
|
||||||
|
|
||||||
class AuthUserRepositoryABC(ABC):
|
class AuthUserRepositoryABC(ABC):
|
||||||
@ -39,3 +40,12 @@ class AuthUserRepositoryABC(ABC):
|
|||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def delete_auth_user(self, user: AuthUser): pass
|
def delete_auth_user(self, user: AuthUser): pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def add_auth_user_user_rel(self, rel: AuthUserUsersRelation): pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def update_auth_user_user_rel(self, rel: AuthUserUsersRelation): pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def delete_auth_user_user_rel(self, rel: AuthUserUsersRelation): pass
|
||||||
|
@ -30,8 +30,8 @@ class ApiMigration(MigrationABC):
|
|||||||
`OAuthId` VARCHAR(255) DEFAULT NULL,
|
`OAuthId` VARCHAR(255) DEFAULT NULL,
|
||||||
`RefreshTokenExpiryTime` DATETIME(6) NOT NULL,
|
`RefreshTokenExpiryTime` DATETIME(6) NOT NULL,
|
||||||
`AuthRole` INT NOT NULL DEFAULT '0',
|
`AuthRole` INT NOT NULL DEFAULT '0',
|
||||||
`CreatedOn` DATETIME(6) NOT NULL,
|
`CreatedAt` DATETIME(6) NOT NULL,
|
||||||
`LastModifiedOn` DATETIME(6) NOT NULL,
|
`LastModifiedAt` DATETIME(6) NOT NULL,
|
||||||
PRIMARY KEY(`Id`)
|
PRIMARY KEY(`Id`)
|
||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
@ -43,8 +43,8 @@ class ApiMigration(MigrationABC):
|
|||||||
`Id` BIGINT NOT NULL AUTO_INCREMENT,
|
`Id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
`AuthUserId` BIGINT DEFAULT NULL,
|
`AuthUserId` BIGINT DEFAULT NULL,
|
||||||
`UserId` BIGINT DEFAULT NULL,
|
`UserId` BIGINT DEFAULT NULL,
|
||||||
`CreatedOn` DATETIME(6) NOT NULL,
|
`CreatedAt` DATETIME(6) NOT NULL,
|
||||||
`LastModifiedOn` DATETIME(6) NOT NULL,
|
`LastModifiedAt` DATETIME(6) NOT NULL,
|
||||||
PRIMARY KEY(`Id`),
|
PRIMARY KEY(`Id`),
|
||||||
FOREIGN KEY (`AuthUserId`) REFERENCES `AuthUsers`(`Id`),
|
FOREIGN KEY (`AuthUserId`) REFERENCES `AuthUsers`(`Id`),
|
||||||
FOREIGN KEY (`UserId`) REFERENCES `Users`(`UserId`)
|
FOREIGN KEY (`UserId`) REFERENCES `Users`(`UserId`)
|
||||||
|
@ -207,8 +207,8 @@ class AuthUser(TableABC):
|
|||||||
`OAuthId`,
|
`OAuthId`,
|
||||||
`RefreshTokenExpiryTime`,
|
`RefreshTokenExpiryTime`,
|
||||||
`AuthRole`,
|
`AuthRole`,
|
||||||
`CreatedOn`,
|
`CreatedAt`,
|
||||||
`LastModifiedOn`
|
`LastModifiedAt`
|
||||||
) VALUES (
|
) VALUES (
|
||||||
{self._auth_user_id},
|
{self._auth_user_id},
|
||||||
'{self._first_name}',
|
'{self._first_name}',
|
||||||
@ -220,7 +220,7 @@ class AuthUser(TableABC):
|
|||||||
'{"NULL" if self._confirmation_id is None else self._confirmation_id}',
|
'{"NULL" if self._confirmation_id is None else self._confirmation_id}',
|
||||||
'{"NULL" if self._forgot_password_id is None else self._forgot_password_id}',
|
'{"NULL" if self._forgot_password_id is None else self._forgot_password_id}',
|
||||||
'{"NULL" if self._oauth_id is None else self._oauth_id}',
|
'{"NULL" if self._oauth_id is None else self._oauth_id}',
|
||||||
'{self._refresh_token_expire_time}',
|
'{self._refresh_token_expire_time.isoformat()}',
|
||||||
{self._auth_role_id.value},
|
{self._auth_role_id.value},
|
||||||
'{self._created_at}',
|
'{self._created_at}',
|
||||||
'{self._modified_at}'
|
'{self._modified_at}'
|
||||||
@ -240,9 +240,9 @@ class AuthUser(TableABC):
|
|||||||
`ConfirmationId` = '{"NULL" if self._confirmation_id is None else self._confirmation_id}',
|
`ConfirmationId` = '{"NULL" if self._confirmation_id is None else self._confirmation_id}',
|
||||||
`ForgotPasswordId` = '{"NULL" if self._forgot_password_id is None else self._forgot_password_id}',
|
`ForgotPasswordId` = '{"NULL" if self._forgot_password_id is None else self._forgot_password_id}',
|
||||||
`OAuthId` = '{"NULL" if self._oauth_id is None else self._oauth_id}',
|
`OAuthId` = '{"NULL" if self._oauth_id is None else self._oauth_id}',
|
||||||
`RefreshTokenExpiryTime` = '{self._refresh_token_expire_time}',
|
`RefreshTokenExpiryTime` = '{self._refresh_token_expire_time.isoformat()}',
|
||||||
`AuthRole` = {self._auth_role_id.value},
|
`AuthRole` = {self._auth_role_id.value},
|
||||||
`LastModifiedOn` = '{self._modified_at}'
|
`LastModifiedAt` = '{self._modified_at}'
|
||||||
WHERE `AuthUsers`.`Id` = {self._auth_user_id};
|
WHERE `AuthUsers`.`Id` = {self._auth_user_id};
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from cpl_core.database import TableABC
|
from cpl_core.database import TableABC
|
||||||
|
|
||||||
from bot_data.model.auth_user import AuthUser
|
from bot_data.model.auth_user import AuthUser
|
||||||
from bot_data.model.user import User
|
from bot_data.model.user import User
|
||||||
from bot_data.model.server import Server
|
|
||||||
|
|
||||||
|
|
||||||
class AuthUserUsersRelation(TableABC):
|
class AuthUserUsersRelation(TableABC):
|
||||||
|
@ -10,6 +10,7 @@ from bot_data.abc.user_repository_abc import UserRepositoryABC
|
|||||||
from bot_data.filtered_result import FilteredResult
|
from bot_data.filtered_result import FilteredResult
|
||||||
from bot_data.model.auth_role_enum import AuthRoleEnum
|
from bot_data.model.auth_role_enum import AuthRoleEnum
|
||||||
from bot_data.model.auth_user import AuthUser
|
from bot_data.model.auth_user import AuthUser
|
||||||
|
from bot_data.model.auth_user_users_relation import AuthUserUsersRelation
|
||||||
from bot_data.model.user import User
|
from bot_data.model.user import User
|
||||||
|
|
||||||
|
|
||||||
@ -145,3 +146,15 @@ class AuthUserRepositoryService(AuthUserRepositoryABC):
|
|||||||
def delete_auth_user(self, user: AuthUser):
|
def delete_auth_user(self, user: AuthUser):
|
||||||
self._logger.trace(__name__, f'Send SQL command: {user.delete_string}')
|
self._logger.trace(__name__, f'Send SQL command: {user.delete_string}')
|
||||||
self._context.cursor.execute(user.delete_string)
|
self._context.cursor.execute(user.delete_string)
|
||||||
|
|
||||||
|
def add_auth_user_user_rel(self, rel: AuthUserUsersRelation):
|
||||||
|
self._logger.trace(__name__, f'Send SQL command: {rel.insert_string}')
|
||||||
|
self._context.cursor.execute(rel.insert_string)
|
||||||
|
|
||||||
|
def update_auth_user_user_rel(self, rel: AuthUserUsersRelation):
|
||||||
|
self._logger.trace(__name__, f'Send SQL command: {rel.udpate_string}')
|
||||||
|
self._context.cursor.execute(rel.udpate_string)
|
||||||
|
|
||||||
|
def delete_auth_user_user_rel(self, rel: AuthUserUsersRelation):
|
||||||
|
self._logger.trace(__name__, f'Send SQL command: {rel.delete_string}')
|
||||||
|
self._context.cursor.execute(rel.delete_string)
|
||||||
|
@ -101,5 +101,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"cli": {
|
||||||
|
"analytics": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
4
kdb-web/package-lock.json
generated
4
kdb-web/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "kdb-web",
|
"name": "kdb-web",
|
||||||
"version": "0.3.0",
|
"version": "0.3.dev70",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "kdb-web",
|
"name": "kdb-web",
|
||||||
"version": "0.3.0",
|
"version": "0.3.dev70",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^14.0.0",
|
"@angular/animations": "^14.0.0",
|
||||||
"@angular/common": "^14.0.0",
|
"@angular/common": "^14.0.0",
|
||||||
|
Loading…
Reference in New Issue
Block a user