Browse Source

Added simple auth

pull/2/head
Per-Arne 5 years ago
parent
commit
b725c585b0
  1. 25
      wg_dashboard_backend/db/user.py
  2. 143
      wg_dashboard_backend/main.py
  3. 21
      wg_dashboard_backend/schemas.py
  4. 21
      wg_dashboard_frontend/src/app/app-routing.module.ts
  5. 10
      wg_dashboard_frontend/src/app/app.component.ts
  6. 11
      wg_dashboard_frontend/src/app/app.module.ts
  7. 11
      wg_dashboard_frontend/src/app/interfaces/user.ts
  8. 22
      wg_dashboard_frontend/src/app/layouts/common-layout/common-layout.component.html
  9. 17
      wg_dashboard_frontend/src/app/layouts/common-layout/common-layout.component.ts
  10. 1
      wg_dashboard_frontend/src/app/pages/pages/login/index.ts
  11. 85
      wg_dashboard_frontend/src/app/pages/pages/login/login.component.html
  12. 4
      wg_dashboard_frontend/src/app/pages/pages/pages.module.ts
  13. 48
      wg_dashboard_frontend/src/app/pages/user/edit/edit.component.html
  14. 0
      wg_dashboard_frontend/src/app/pages/user/edit/edit.component.scss
  15. 51
      wg_dashboard_frontend/src/app/pages/user/edit/edit.component.ts
  16. 40
      wg_dashboard_frontend/src/app/pages/user/login/login.component.html
  17. 0
      wg_dashboard_frontend/src/app/pages/user/login/login.component.scss
  18. 25
      wg_dashboard_frontend/src/app/pages/user/login/login.component.spec.ts
  19. 26
      wg_dashboard_frontend/src/app/pages/user/login/login.component.ts
  20. 23
      wg_dashboard_frontend/src/app/pages/user/user.module.ts
  21. 2
      wg_dashboard_frontend/src/app/services/auth/auth.guard.ts
  22. 23
      wg_dashboard_frontend/src/app/services/auth/auth.interceptor.ts
  23. 82
      wg_dashboard_frontend/src/app/services/auth/auth.service.ts
  24. 72
      wg_dashboard_frontend/src/app/services/auth/fakebackend.interceptor.ts
  25. 1
      wg_dashboard_frontend/src/app/services/auth/index.ts
  26. 3
      wg_dashboard_frontend/src/theme/components/sidebar/menu-link-item.component.ts

25
wg_dashboard_backend/db/user.py

@ -1,7 +1,11 @@
from typing import Optional
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
import models import models
from passlib.context import CryptContext from passlib.context import CryptContext
import schemas
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
@ -13,13 +17,22 @@ def get_password_hash(password):
return pwd_context.hash(password) return pwd_context.hash(password)
def authenticate_user(sess, username: str, password: str): def update_user(sess: Session, form_data: schemas.UserInDB):
user = get_user_by_name(sess, form_data.username)
user.password = form_data.password
user.full_name = form_data.full_name
user.email = form_data.email # TOD this section should be updated
sess.add(user)
sess.commit()
return get_user_by_name(sess, form_data.username)
def authenticate_user(sess, username: str, password: str) -> Optional[models.User]:
user = get_user_by_name(sess, username) user = get_user_by_name(sess, username)
if not user: if user and verify_password(password, user.password):
return False return user
if not verify_password(password, user.password): return None
return False
return user
def get_user_by_name(db: Session, username: str) -> models.User: def get_user_by_name(db: Session, username: str) -> models.User:

143
wg_dashboard_backend/main.py

@ -1,6 +1,7 @@
import logging import logging
import os
from sqlalchemy.exc import OperationalError from sqlalchemy_utils import database_exists
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
@ -21,7 +22,7 @@ from datetime import datetime, timedelta
import db.wireguard import db.wireguard
import db.user import db.user
import jwt import jwt
from fastapi import Depends, FastAPI, HTTPException, status, Form from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jwt import PyJWTError from jwt import PyJWTError
import script.wireguard import script.wireguard
@ -35,16 +36,24 @@ engine = sqlalchemy.create_engine(
const.DATABASE_URL, connect_args={"check_same_thread": False} const.DATABASE_URL, connect_args={"check_same_thread": False}
) )
try: oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/token")
models.Base.metadata.create_all(engine)
except OperationalError as e:
pass
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
app = FastAPI() if not database_exists(engine.url):
models.Base.metadata.create_all(engine)
# Create default user
_db: Session = SessionLocal()
_db.add(models.User(
username=os.getenv("ADMIN_USERNAME", "admin"),
password=db.user.get_password_hash(os.getenv("ADMIN_PASSWORD", "admin")),
full_name="Admin",
role="admin",
email=""
))
_db.commit()
_db.close()
app = FastAPI()
# Dependency # Dependency
def get_db(): def get_db():
@ -66,28 +75,45 @@ def create_access_token(*, data: dict, expires_delta: timedelta = None):
return encoded_jwt return encoded_jwt
def get_current_user(token: str = Depends(oauth2_scheme), sess: Session = Depends(get_db)): def auth(token: str = Depends(oauth2_scheme), sess: Session = Depends(get_db)):
credentials_exception = HTTPException( credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials", detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
try: try:
payload = jwt.decode(token, const.SECRET_KEY, algorithms=[const.ALGORITHM]) payload = jwt.decode(token, const.SECRET_KEY, algorithms=[const.ALGORITHM])
username: str = payload.get("sub") username: str = payload.get("sub")
if username is None: if username is None:
raise credentials_exception raise credentials_exception
token_data = schemas.TokenData(username=username)
except PyJWTError: except PyJWTError:
raise credentials_exception raise credentials_exception
user = db.user.get_user_by_name(sess, token_data.username) user = db.user.get_user_by_name(sess, username)
if user is None: if user is None:
raise credentials_exception raise credentials_exception
return user return user
@app.post("/api/token", response_model=schemas.Token) @app.get("/api/logout")
def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), sess: Session = Depends(get_db)): def logout(user: schemas.User = Depends(auth)):
# TODO
return {}
@app.post("/api/user/edit", response_model=schemas.User)
def edit(form_data: schemas.UserInDB, user: schemas.User = Depends(auth), sess: Session = Depends(get_db)):
form_data.password = db.user.get_password_hash(form_data.password)
db_user = db.user.update_user(sess, form_data)
return schemas.User.from_orm(db_user)
@app.post("/api/login", response_model=schemas.Token)
def login(form_data: OAuth2PasswordRequestForm = Depends(), sess: Session = Depends(get_db)):
user = db.user.authenticate_user(sess, form_data.username, form_data.password) user = db.user.authenticate_user(sess, form_data.username, form_data.password)
if not user: if not user:
@ -96,17 +122,23 @@ def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), ses
detail="Incorrect username or password", detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
# Create token
access_token_expires = timedelta(minutes=const.ACCESS_TOKEN_EXPIRE_MINUTES) access_token_expires = timedelta(minutes=const.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token( access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires data={"sub": user.username}, expires_delta=access_token_expires
) )
return {"access_token": access_token, "token_type": "bearer"}
return {"access_token": access_token, "token_type": "bearer", "user": schemas.User.from_orm(user)}
# @app.post("/wg/update/", response_model=List[schemas.WireGuard]) # @app.post("/wg/update/", response_model=List[schemas.WireGuard])
@app.get("/api/wg/server/all", response_model=typing.List[schemas.WGServer]) @app.get("/api/wg/server/all", response_model=typing.List[schemas.WGServer])
def get_interfaces(sess: Session = Depends(get_db)): def get_interfaces(
sess: Session = Depends(get_db),
user: schemas.User = Depends(auth)
):
interfaces = db.wireguard.server_get_all(sess) interfaces = db.wireguard.server_get_all(sess)
for iface in interfaces: for iface in interfaces:
iface.is_running = script.wireguard.is_running(iface) iface.is_running = script.wireguard.is_running(iface)
@ -115,7 +147,11 @@ def get_interfaces(sess: Session = Depends(get_db)):
@app.post("/api/wg/server/add", response_model=schemas.WGServer) @app.post("/api/wg/server/add", response_model=schemas.WGServer)
def add_interface(form_data: schemas.WGServer, sess: Session = Depends(get_db)): def add_interface(
form_data: schemas.WGServer,
sess: Session = Depends(get_db),
user: schemas.User = Depends(auth)
):
if form_data.interface is None or form_data.listen_port is None or form_data.address is None: if form_data.interface is None or form_data.listen_port is None or form_data.address is None:
raise HTTPException(status_code=400, raise HTTPException(status_code=400,
detail="Interface, Listen-Port and Address must be included in the schema.") detail="Interface, Listen-Port and Address must be included in the schema.")
@ -136,7 +172,10 @@ def add_interface(form_data: schemas.WGServer, sess: Session = Depends(get_db)):
@app.post("/api/wg/server/edit", response_model=schemas.WGServer) @app.post("/api/wg/server/edit", response_model=schemas.WGServer)
def edit_server(data: dict, sess: Session = Depends(get_db)): def edit_server(
data: dict, sess: Session = Depends(get_db),
user: schemas.User = Depends(auth)
):
interface = data["interface"] interface = data["interface"]
server = schemas.WGServer(**data["server"]) server = schemas.WGServer(**data["server"])
@ -156,7 +195,7 @@ def edit_server(data: dict, sess: Session = Depends(get_db)):
@app.get("/api/wg/generate_keypair", response_model=schemas.KeyPair) @app.get("/api/wg/generate_keypair", response_model=schemas.KeyPair)
def generate_key_pair(): def generate_key_pair(user: schemas.User = Depends(auth)):
private_key, public_key = script.wireguard.generate_keys() private_key, public_key = script.wireguard.generate_keys()
return schemas.KeyPair( return schemas.KeyPair(
private_key=private_key, private_key=private_key,
@ -165,21 +204,28 @@ def generate_key_pair():
@app.get("/api/wg/generate_psk", response_model=schemas.PSK) @app.get("/api/wg/generate_psk", response_model=schemas.PSK)
def generate_psk(): def generate_psk(user: schemas.User = Depends(auth)):
return schemas.PSK( return schemas.PSK(
psk=script.wireguard.generate_psk() psk=script.wireguard.generate_psk()
) )
@app.post("/api/wg/server/stop", response_model=schemas.WGServer) @app.post("/api/wg/server/stop", response_model=schemas.WGServer)
def start_server(form_data: schemas.WGServer, ): def start_server(
form_data: schemas.WGServer,
user: schemas.User = Depends(auth)
):
script.wireguard.stop_interface(form_data) script.wireguard.stop_interface(form_data)
form_data.is_running = script.wireguard.is_running(form_data) form_data.is_running = script.wireguard.is_running(form_data)
return form_data return form_data
@app.post("/api/wg/server/start", response_model=schemas.WGServer) @app.post("/api/wg/server/start", response_model=schemas.WGServer)
def start_server(form_data: schemas.WGServer, sess: Session = Depends(get_db)): def start_server(
form_data: schemas.WGServer,
sess: Session = Depends(get_db),
user: schemas.User = Depends(auth)
):
db.wireguard.server_generate_config(sess, form_data) db.wireguard.server_generate_config(sess, form_data)
script.wireguard.start_interface(form_data) script.wireguard.start_interface(form_data)
form_data.is_running = script.wireguard.is_running(form_data) form_data.is_running = script.wireguard.is_running(form_data)
@ -187,7 +233,11 @@ def start_server(form_data: schemas.WGServer, sess: Session = Depends(get_db)):
@app.post("/api/wg/server/restart", response_model=schemas.WGServer) @app.post("/api/wg/server/restart", response_model=schemas.WGServer)
def start_server(form_data: schemas.WGServer, sess: Session = Depends(get_db)): def start_server(
form_data: schemas.WGServer,
sess: Session = Depends(get_db),
user: schemas.User = Depends(auth)
):
db.wireguard.server_generate_config(sess, form_data) db.wireguard.server_generate_config(sess, form_data)
script.wireguard.restart_interface(form_data) script.wireguard.restart_interface(form_data)
form_data.is_running = script.wireguard.is_running(form_data) form_data.is_running = script.wireguard.is_running(form_data)
@ -195,7 +245,11 @@ def start_server(form_data: schemas.WGServer, sess: Session = Depends(get_db)):
@app.post("/api/wg/server/delete", response_model=schemas.WGServer) @app.post("/api/wg/server/delete", response_model=schemas.WGServer)
def delete_server(form_data: schemas.WGServer, sess: Session = Depends(get_db)): def delete_server(
form_data: schemas.WGServer,
sess: Session = Depends(get_db),
user: schemas.User = Depends(auth)
):
# Stop if running # Stop if running
if script.wireguard.is_running(form_data): if script.wireguard.is_running(form_data):
script.wireguard.stop_interface(form_data) script.wireguard.stop_interface(form_data)
@ -206,7 +260,11 @@ def delete_server(form_data: schemas.WGServer, sess: Session = Depends(get_db)):
@app.post("/api/wg/server/peer/add", response_model=schemas.WGPeer) @app.post("/api/wg/server/peer/add", response_model=schemas.WGPeer)
def add_peer(form_data: schemas.WGServer, sess: Session = Depends(get_db)): def add_peer(
form_data: schemas.WGServer,
sess: Session = Depends(get_db),
user: schemas.User = Depends(auth)
):
wg_peer = schemas.WGPeer(server=form_data.interface) wg_peer = schemas.WGPeer(server=form_data.interface)
# Insert initial peer # Insert initial peer
@ -222,7 +280,11 @@ def add_peer(form_data: schemas.WGServer, sess: Session = Depends(get_db)):
@app.post("/api/wg/server/peer/delete", response_model=schemas.WGPeer) @app.post("/api/wg/server/peer/delete", response_model=schemas.WGPeer)
def delete_peer(form_data: schemas.WGPeer, sess: Session = Depends(get_db)): def delete_peer(
form_data: schemas.WGPeer,
sess: Session = Depends(get_db),
user: schemas.User = Depends(auth)
):
if not db.wireguard.peer_remove(sess, form_data): if not db.wireguard.peer_remove(sess, form_data):
raise HTTPException(400, detail="Were not able to delete peer %s (%s)" % (form_data.name, form_data.public_key)) raise HTTPException(400, detail="Were not able to delete peer %s (%s)" % (form_data.name, form_data.public_key))
@ -234,7 +296,11 @@ def delete_peer(form_data: schemas.WGPeer, sess: Session = Depends(get_db)):
@app.post("/api/wg/server/peer/edit", response_model=schemas.WGPeer) @app.post("/api/wg/server/peer/edit", response_model=schemas.WGPeer)
def edit_peer(form_data: schemas.WGPeer, sess: Session = Depends(get_db)): def edit_peer(
form_data: schemas.WGPeer,
sess: Session = Depends(get_db),
user: schemas.User = Depends(auth)
):
wg_peer = db.wireguard.peer_update(sess, form_data) wg_peer = db.wireguard.peer_update(sess, form_data)
db.wireguard.peer_generate_config(sess, wg_peer) db.wireguard.peer_generate_config(sess, wg_peer)
@ -242,13 +308,20 @@ def edit_peer(form_data: schemas.WGPeer, sess: Session = Depends(get_db)):
@app.post("/api/wg/server/stats") @app.post("/api/wg/server/stats")
def edit_peer(form_data: schemas.WGServer): def edit_peer(
form_data: schemas.WGServer,
user: schemas.User = Depends(auth)
):
stats = script.wireguard.get_stats(form_data) stats = script.wireguard.get_stats(form_data)
return JSONResponse(content=stats) return JSONResponse(content=stats)
@app.post("/api/wg/server/peer/config", response_model=schemas.WGPeerConfig) @app.post("/api/wg/server/peer/config", response_model=schemas.WGPeerConfig)
def config_peer(form_data: schemas.WGPeer, sess: Session = Depends(get_db)): def config_peer(
form_data: schemas.WGPeer,
sess: Session = Depends(get_db),
user: schemas.User = Depends(auth)
):
db_peer = db.wireguard.peer_query_get_by_address(sess, form_data.address, form_data.server).one() db_peer = db.wireguard.peer_query_get_by_address(sess, form_data.address, form_data.server).one()
with open(const.PEER_FILE(db_peer), "r") as f: with open(const.PEER_FILE(db_peer), "r") as f:
@ -258,7 +331,10 @@ def config_peer(form_data: schemas.WGPeer, sess: Session = Depends(get_db)):
@app.post("/api/wg/server/config", response_model=schemas.WGPeerConfig) @app.post("/api/wg/server/config", response_model=schemas.WGPeerConfig)
def config_server(form_data: schemas.WGServer): def config_server(
form_data: schemas.WGServer,
user: schemas.User = Depends(auth)
):
with open(const.SERVER_FILE(form_data.interface), "r") as f: with open(const.SERVER_FILE(form_data.interface), "r") as f:
conf_file = f.read() conf_file = f.read()
@ -266,7 +342,11 @@ def config_server(form_data: schemas.WGServer):
@app.post("/api/users/create/") @app.post("/api/users/create/")
def create_user(form_data: schemas.UserInDB, sess: Session = Depends(get_db)): def create_user(
form_data: schemas.UserInDB,
sess: Session = Depends(get_db),
user: schemas.User = Depends(auth)
):
user = db.user.get_user_by_name(sess, form_data.username) user = db.user.get_user_by_name(sess, form_data.username)
# User already exists # User already exists
@ -293,6 +373,7 @@ def create_user(form_data: schemas.UserInDB, sess: Session = Depends(get_db)):
), sess) ), sess)
@app.on_event("startup") @app.on_event("startup")
async def startup(): async def startup():
await database.connect() await database.connect()

21
wg_dashboard_backend/schemas.py

@ -3,21 +3,15 @@ from pydantic import BaseModel, typing
import models import models
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: str = None
class User(BaseModel): class User(BaseModel):
username: str = None username: str = None
email: str = None email: str = None
full_name: str = None full_name: str = None
role: str = None role: str = None
class Config:
orm_mode = True
class UserInDB(User): class UserInDB(User):
password: str password: str
@ -26,6 +20,15 @@ class UserInDB(User):
orm_mode = True orm_mode = True
class Token(BaseModel):
access_token: str
token_type: str
user: User
class Config:
orm_mode = True
class WGPeer(BaseModel): class WGPeer(BaseModel):
name: str = None name: str = None
address: str = None address: str = None

21
wg_dashboard_frontend/src/app/app-routing.module.ts

@ -4,7 +4,11 @@ import { RouterModule } from '@angular/router';
import { LayoutsModule } from './layouts'; import { LayoutsModule } from './layouts';
import { CommonLayoutComponent } from './layouts/common-layout'; import { CommonLayoutComponent } from './layouts/common-layout';
import { DashboardComponent } from './pages/dashboard'; import { DashboardComponent } from './pages/dashboard';
import {LoginComponent} from "./pages/pages/login";
import {AuthGuard} from "@services/*";
import {EditComponent} from "./pages/user/edit/edit.component";
import {LoginComponent} from "./pages/user/login/login.component";
@NgModule({ @NgModule({
@ -14,12 +18,21 @@ import {LoginComponent} from "./pages/pages/login";
{ path: '', redirectTo: 'app/dashboard', pathMatch: 'full' }, { path: '', redirectTo: 'app/dashboard', pathMatch: 'full' },
{ path: 'app', component: CommonLayoutComponent, children: { path: 'app', component: CommonLayoutComponent, children:
[ [
{ path: 'dashboard', component: DashboardComponent, pathMatch: 'full'}, // canActivate: [AuthGuard] { path: 'dashboard', component: DashboardComponent, pathMatch: 'full', canActivate: [AuthGuard]},
{ path: '**', redirectTo: '/pages/404'},
]
},
{ path: 'user', component: CommonLayoutComponent, children:
[
{ path: 'login', component: LoginComponent, pathMatch: 'full'}, { path: 'login', component: LoginComponent, pathMatch: 'full'},
{ path: '**', redirectTo: '/pages/404' }, { path: 'edit', component: EditComponent, pathMatch: 'full', canActivate: [AuthGuard]},
] ]
}, },
{ path: 'pages', loadChildren: () => import('./pages/pages/pages.module').then(m => m.PagesModule) },
{ path: '**', redirectTo: '/pages/404' }, { path: '**', redirectTo: '/pages/404' },
], ],
{ useHash: true }, { useHash: true },

10
wg_dashboard_frontend/src/app/app.component.ts

@ -1,7 +1,15 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import {AuthService} from "@services/*";
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
template: `<router-outlet></router-outlet>`, template: `<router-outlet></router-outlet>`,
}) })
export class AppComponent { } export class AppComponent {
constructor(private auth: AuthService) {
auth.init()
}
}

11
wg_dashboard_frontend/src/app/app.module.ts

@ -2,7 +2,7 @@ import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { AuthInterceptor, AuthService, FakeBackendInterceptor } from '@services/*'; import { AuthInterceptor, AuthService } from '@services/*';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
@ -11,6 +11,7 @@ import { DashboardModule } from './pages/dashboard';
import { VarDirective } from './directives/var.directive'; import { VarDirective } from './directives/var.directive';
import { QRCodeModule } from 'angularx-qrcode'; import { QRCodeModule } from 'angularx-qrcode';
import {NgbModule} from "@ng-bootstrap/ng-bootstrap"; import {NgbModule} from "@ng-bootstrap/ng-bootstrap";
import {UserModule} from "./pages/user/user.module";
@NgModule({ @NgModule({
declarations: [AppComponent, VarDirective], declarations: [AppComponent, VarDirective],
@ -19,6 +20,7 @@ import {NgbModule} from "@ng-bootstrap/ng-bootstrap";
AppRoutingModule, AppRoutingModule,
ComponentsModule, ComponentsModule,
DashboardModule, DashboardModule,
UserModule,
HttpClientModule, HttpClientModule,
NgbModule, NgbModule,
QRCodeModule QRCodeModule
@ -29,12 +31,7 @@ import {NgbModule} from "@ng-bootstrap/ng-bootstrap";
provide: HTTP_INTERCEPTORS, provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor, useClass: AuthInterceptor,
multi: true, multi: true,
}, }
{
provide: HTTP_INTERCEPTORS,
useClass: FakeBackendInterceptor,
multi: true,
},
], ],
bootstrap: [AppComponent], bootstrap: [AppComponent],
exports: [ exports: [

11
wg_dashboard_frontend/src/app/interfaces/user.ts

@ -0,0 +1,11 @@
import {Peer} from "./peer";
export interface User {
full_name: string;
email: string;
role: string;
username: string;
access_token: string,
token_type: string,
}

22
wg_dashboard_frontend/src/app/layouts/common-layout/common-layout.component.html

@ -26,28 +26,28 @@
<div class="avatar-dropdown" id="icon"> <div class="avatar-dropdown" id="icon" *ngIf="auth.user">
<span>Logged in as Admin</span> <span>Logged in as {{auth.user?.username}}</span>
<!--<img src="assets/images/Icon_header.png">--> <!--<img src="assets/images/Icon_header.png">-->
</div> </div>
<!--<ul <ul
class="mdl-menu mdl-list mdl-menu--bottom-right mdl-js-menu mdl-js-ripple-effect mdl-shadow--2dp account-dropdown" class="mdl-menu mdl-list mdl-menu--bottom-right mdl-js-menu mdl-js-ripple-effect mdl-shadow--2dp account-dropdown"
for="icon"> for="icon">
<li class="mdl-list__item mdl-list__item--two-line"> <li class="mdl-list__item mdl-list__item--two-line">
<span class="mdl-list__item-primary-content"> <span class="mdl-list__item-primary-content">
<span class="material-icons mdl-list__item-avatar"></span> <span class="material-icons mdl-list__item-avatar"></span>
<span>{{ user.username }}</span> <span>{{ auth.user?.username }}</span>
<span class="mdl-list__item-sub-title">{{ user.email }}</span> <span class="mdl-list__item-sub-title">{{ auth.user?.email }}</span>
</span> </span>
</li> </li>
<li class="list__item--border-top"></li> <li class="list__item--border-top"></li>
<li class="mdl-menu__item mdl-list__item"> <li class="mdl-menu__item mdl-list__item">
<span class="mdl-list__item-primary-content"> <span class="mdl-list__item-primary-content" (click)="router.navigate(['/user/edit'])">
<i class="material-icons mdl-list__item-icon">account_circle</i> <i class="material-icons mdl-list__item-icon">account_circle</i>
My account My account
</span> </span>
</li> </li>
<li class="mdl-menu__item mdl-list__item"> <!--<li class="mdl-menu__item mdl-list__item">
<span class="mdl-list__item-primary-content"> <span class="mdl-list__item-primary-content">
<i class="material-icons mdl-list__item-icon">check_box</i> <i class="material-icons mdl-list__item-icon">check_box</i>
My tasks My tasks
@ -61,21 +61,21 @@
<i class="material-icons mdl-list__item-icon">perm_contact_calendar</i> <i class="material-icons mdl-list__item-icon">perm_contact_calendar</i>
My events My events
</span> </span>
</li> </li>-->
<li class="list__item--border-top"></li> <!--<li class="list__item--border-top"></li>
<li class="mdl-menu__item mdl-list__item"> <li class="mdl-menu__item mdl-list__item">
<span class="mdl-list__item-primary-content"> <span class="mdl-list__item-primary-content">
<i class="material-icons mdl-list__item-icon">settings</i> <i class="material-icons mdl-list__item-icon">settings</i>
Settings Settings
</span> </span>
</li> </li>-->
<li class="mdl-menu__item mdl-list__item"> <li class="mdl-menu__item mdl-list__item">
<span class="mdl-list__item-primary-content" (click)="logout()"> <span class="mdl-list__item-primary-content" (click)="logout()">
<i class="material-icons mdl-list__item-icon text-color--secondary">exit_to_app</i> <i class="material-icons mdl-list__item-icon text-color--secondary">exit_to_app</i>
Log out Log out
</span> </span>
</li> </li>
</ul>--> </ul>
</base-page-top> </base-page-top>
</div> </div>

17
wg_dashboard_frontend/src/app/layouts/common-layout/common-layout.component.ts

@ -10,22 +10,19 @@ import { AuthService } from '@services/*';
export class CommonLayoutComponent implements OnInit { export class CommonLayoutComponent implements OnInit {
public title = 'Wireguard Manager'; public title = 'Wireguard Manager';
public menu = [ public menu = [
{ name: 'Dashboard', link: 'dashboard', icon: 'dashboard' }, { name: 'Dashboard', link: '/app/dashboard', icon: 'dashboard' },
]; ];
public user;
constructor(private authService: AuthService,
private router: Router) {} constructor(public auth: AuthService,
public router: Router) {}
public ngOnInit() { public ngOnInit() {
this.authService.userData.subscribe(user => this.user = user ? user : {
username: 'Luke',
email: 'Luke@skywalker.com',
});
} }
public logout() { public logout() {
this.authService.logout() this.auth.logout()
.subscribe(res => this.router.navigate(['/pages/login'])); .subscribe(res => this.router.navigate(['/user/login']));
} }
} }

1
wg_dashboard_frontend/src/app/pages/pages/login/index.ts

@ -1 +0,0 @@
export { LoginComponent } from './login.component';

85
wg_dashboard_frontend/src/app/pages/pages/login/login.component.html

@ -1,85 +0,0 @@
<div class="mdl-card mdl-shadow--2dp">
<div class="mdl-card__title">
<h2 class="mdl-card__title-text">Login to Wireguard Manager</h2>
</div>
<div class="mdl-card__supporting-text">
<form class="login-form" [formGroup]="loginForm" (submit)="login()" autocomplete="off" novalidate>
<div class="mdl-cell mdl-cell--12-col mdl-cell--4-col-phone">
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label full-size"
[class.is-invalid]="email.invalid && (email.dirty || email.touched)"
[class.is-valid]="email.valid && (email.dirty || email.touched)">
<input formControlName="email"
pattern="{{emailPattern}}"
(change)="onInputChange($event)"
class="mdl-textfield__input" type="text" id="email">
<label class="mdl-textfield__label" for="email">Email</label>
<div *ngIf="email.invalid && (email.dirty || email.touched)">
<span *ngIf="email.errors.required" class="mdl-textfield__error">
Email is required. <span class="color-text--orange"> Please, write any valid email.</span>
</span>
<span *ngIf="email.errors.pattern" class="mdl-textfield__error">
Email is invalid.
</span>
</div>
</div>
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label full-size"
[class.is-invalid]="password.invalid && (password.dirty || password.touched)"
[class.is-valid]="password.valid && (password.dirty || password.touched)"
id="forPass">
<input formControlName="password"
(change)="onInputChange($event)"
class="mdl-textfield__input" type="password" id="password">
<label class="mdl-textfield__label" for="password">Password</label>
<div *ngIf="password.invalid && (password.dirty || password.touched)">
<span *ngIf="password.errors.required" class="mdl-textfield__error">
Password is required. <span class="color-text--orange"> Please, write any password.</span>
</span>
</div>
</div>
<div class="full-size color-text--red" *ngIf="error"> {{ error }}</div>
</div>
<div class="mdl-cell mdl-cell--12-col mdl-cell--4-col-phone submit-cell">
</div>
</form>
</div>
<div class="mdl-card__actions mdl-card--border">
<a class="mdl-button mdl-button--colored mdl-js-button mdl-js-ripple-effect">
<a routerLink="/pages/forgot-password" class="blank-layout-card-link">Forgot password?</a>
<a routerLink="/pages/sign-up" class="blank-layout-card-link">Don't have account?</a>
<button class="mdl-button mdl-js-button mdl-button--raised color--light-blue"
type="submit" [disabled]="loginForm.invalid">
SIGN IN
</button>
</a>
</div>
<div class="mdl-card__menu">
<button class="mdl-button mdl-button--icon mdl-js-button mdl-js-ripple-effect">
<i class="material-icons">share</i>
</button>
</div>
</div>

4
wg_dashboard_frontend/src/app/pages/pages/pages.module.ts

@ -7,10 +7,11 @@ import { ThemeModule } from 'theme';
import { TooltipModule } from '../../../theme/directives/tooltip/tooltip.module'; import { TooltipModule } from '../../../theme/directives/tooltip/tooltip.module';
import { ErrorComponent } from './error'; import { ErrorComponent } from './error';
import { ForgotPasswordComponent } from './forgot-password'; import { ForgotPasswordComponent } from './forgot-password';
import { LoginComponent } from './login';
import { PagesRoutingModule } from './pages-routing.module'; import { PagesRoutingModule } from './pages-routing.module';
import { SignUpComponent } from './sign-up'; import { SignUpComponent } from './sign-up';
@NgModule({ @NgModule({
imports: [ imports: [
CommonModule, CommonModule,
@ -22,7 +23,6 @@ import { SignUpComponent } from './sign-up';
], ],
declarations: [ declarations: [
ErrorComponent, ErrorComponent,
LoginComponent,
SignUpComponent, SignUpComponent,
ForgotPasswordComponent, ForgotPasswordComponent,
], ],

48
wg_dashboard_frontend/src/app/pages/user/edit/edit.component.html

@ -0,0 +1,48 @@
<div class="container">
<base-card>
<base-card-title>
<h2 class="mdl-card__title-text">Edit User</h2>
</base-card-title>
<base-card-body>
<form [formGroup]="editForm" (ngSubmit)="editForm.valid && edit()" class="form">
<div class="mdl-grid">
<div class="mdl-cell mdl-cell--6-col mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
<input formControlName="full_name" class="mdl-textfield__input" type="text" id="full_name" value=""/>
<label class="mdl-textfield__label" for="full_name">Full Name</label>
</div>
<div class="mdl-cell mdl-cell--6-col mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
<input formControlName="username" class="mdl-textfield__input" type="text" id="username" />
<label class="mdl-textfield__label" for="username">Username</label>
</div>
<div class="mdl-cell mdl-cell--6-col mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
<input formControlName="password" class="mdl-textfield__input" type="text" id="password"/>
<label class="mdl-textfield__label" for="password">Password</label>
</div>
<div class="mdl-cell mdl-cell--6-col mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
<input formControlName="email" class="mdl-textfield__input" type="text" id="email"/>
<label class="mdl-textfield__label" for="email">Email</label>
</div>
</div>
<button [disabled]="!editForm.valid" type="submit" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect button--colored-light-blue">
Edit User
</button>
</form>
</base-card-body>
</base-card>
</div>

0
wg_dashboard_frontend/src/app/pages/user/edit/edit.component.scss

51
wg_dashboard_frontend/src/app/pages/user/edit/edit.component.ts

@ -0,0 +1,51 @@
import { Component, OnInit } from '@angular/core';
import {FormControl, FormGroup, Validators} from "@angular/forms";
import {AuthService} from "@services/*";
import {Router} from "@angular/router";
@Component({
selector: 'app-edit',
templateUrl: './edit.component.html',
styleUrls: ['./edit.component.scss']
})
export class EditComponent implements OnInit {
public editForm: FormGroup = new FormGroup({
full_name: new FormControl(''),
password: new FormControl('', Validators.required),
email: new FormControl('', [
Validators.required,
Validators.pattern('^([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,5})$'),
Validators.maxLength(20),
]),
username: new FormControl('', [Validators.required, Validators.maxLength(20)]),
});
public user: any;
public error: string;
constructor(private authService: AuthService,
private router: Router) {
}
public ngOnInit() {
this.user = this.authService.user;
this.editForm.setValue({
full_name: this.user.full_name,
password: "",
email: this.user.email,
username: this.user.username
})
}
public edit() {
if (this.editForm.valid) {
this.authService.edit(this.editForm.getRawValue())
.subscribe(res => this.router.navigate(['/app/dashboard']),
error => this.error = error.message);
}
}
}

40
wg_dashboard_frontend/src/app/pages/user/login/login.component.html

@ -0,0 +1,40 @@
<div class="container">
<base-card>
<base-card-title>
<h2 class="mdl-card__title-text">Edit User</h2>
</base-card-title>
<base-card-body>
<form [formGroup]="loginForm" (ngSubmit)="loginForm.valid && login()" class="form">
<div class="mdl-grid">
<div class="mdl-cell mdl-cell--12-col mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
<input formControlName="username" class="mdl-textfield__input" type="text" id="username" />
<label class="mdl-textfield__label" for="username">Username</label>
</div>
<div class="mdl-cell mdl-cell--12-col mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
<input formControlName="password" class="mdl-textfield__input" type="text" id="password"/>
<label class="mdl-textfield__label" for="password">Password</label>
</div>
</div>
<button [disabled]="!loginForm.valid" type="submit" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect button--colored-light-blue">
SIGN IN
</button>
</form>
</base-card-body>
</base-card>
</div>

0
wg_dashboard_frontend/src/app/pages/user/login/login.component.scss

25
wg_dashboard_frontend/src/app/pages/user/login/login.component.spec.ts

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LoginComponent } from './login.component';
describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LoginComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

26
wg_dashboard_frontend/src/app/pages/pages/login/login.component.ts → wg_dashboard_frontend/src/app/pages/user/login/login.component.ts

@ -1,37 +1,32 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms";
import { Router } from '@angular/router';
import { BlankLayoutCardComponent } from 'app/components/blank-layout-card';
import {AuthService} from "@services/*"; import {AuthService} from "@services/*";
import {Router} from "@angular/router";
@Component({ @Component({
selector: 'app-login', selector: 'app-login',
styleUrls: ['../../../components/blank-layout-card/blank-layout-card.component.scss'],
templateUrl: './login.component.html', templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
}) })
export class LoginComponent extends BlankLayoutCardComponent implements OnInit { export class LoginComponent implements OnInit {
public loginForm: FormGroup; public loginForm: FormGroup;
public email; public username;
public password; public password;
public emailPattern = '^([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,5})$';
public error: string; public error: string;
constructor(private authService: AuthService, constructor(private authService: AuthService,
private fb: FormBuilder, private fb: FormBuilder,
private router: Router) { private router: Router) {
super();
this.loginForm = this.fb.group({ this.loginForm = this.fb.group({
password: new FormControl('', Validators.required), password: new FormControl('', Validators.required),
email: new FormControl('', [ username: new FormControl('', [
Validators.required, Validators.required,
Validators.pattern(this.emailPattern),
Validators.maxLength(20),
]), ]),
}); });
this.email = this.loginForm.get('email'); this.username = this.loginForm.get('username');
this.password = this.loginForm.get('password'); this.password = this.loginForm.get('password');
} }
@ -47,11 +42,12 @@ export class LoginComponent extends BlankLayoutCardComponent implements OnInit {
if (this.loginForm.valid) { if (this.loginForm.valid) {
this.authService.login(this.loginForm.getRawValue()) this.authService.login(this.loginForm.getRawValue())
.subscribe(res => this.router.navigate(['/app/dashboard']), .subscribe(res => this.router.navigate(['/app/dashboard']),
error => this.error = error.message); error => this.error = error.message);
} }
} }
public onInputChange(event) { public onInputChange(event) {
event.target.required = true; event.target.required = true;
} }
} }

23
wg_dashboard_frontend/src/app/pages/user/user.module.ts

@ -0,0 +1,23 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { EditComponent } from './edit/edit.component';
import {ReactiveFormsModule} from "@angular/forms";
import {CardModule} from "../../../theme/components/card";
import { LoginComponent } from './login/login.component';
@NgModule({
declarations: [
EditComponent,
LoginComponent
],
imports: [
CommonModule,
ReactiveFormsModule,
CardModule
]
})
export class UserModule {
}

2
wg_dashboard_frontend/src/app/services/auth/auth.guard.ts

@ -16,7 +16,7 @@ export class AuthGuard implements CanActivate {
return true; return true;
} }
// Navigate to the login page with extras // Navigate to the login page with extras
this.router.navigate(['/login']); this.router.navigate(['/user/login']);
return false; return false;
} }

23
wg_dashboard_frontend/src/app/services/auth/auth.interceptor.ts

@ -1,7 +1,8 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/index'; import { Observable } from 'rxjs';
import { import {
HttpErrorResponse,
HttpEvent, HttpEvent,
HttpHandler, HttpHandler,
HttpInterceptor, HttpInterceptor,
@ -9,19 +10,31 @@ import {
} from '@angular/common/http'; } from '@angular/common/http';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import {tap} from "rxjs/operators";
import {Router} from "@angular/router";
@Injectable() @Injectable()
export class AuthInterceptor implements HttpInterceptor { export class AuthInterceptor implements HttpInterceptor {
constructor(private auth: AuthService) { constructor(private auth: AuthService, private router: Router) {}
}
public intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { public intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// add authorization token for full api requests // add authorization token for full api requests
if (request.url.includes('api') && this.auth.isLoggedIn) { if (request.url.includes('api') && this.auth.isLoggedIn) {
request = request.clone({ request = request.clone({
setHeaders: { Authorization: `Bearer ${this.auth.authToken}` }, setHeaders: { Authorization: `Bearer ${this.auth.user.access_token}`},
}); });
} }
return next.handle(request);
return next.handle(request).pipe( tap(() => {},
(err: any) => {
if (err instanceof HttpErrorResponse) {
if (err.status !== 401 && err.status !== 403) {
return;
}
this.auth.clearData();
this.router.navigate(['user/login']);
}
}));
} }
} }

82
wg_dashboard_frontend/src/app/services/auth/auth.service.ts

@ -4,6 +4,7 @@ import { BehaviorSubject, Observable, of } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import {User} from "../../interfaces/user";
const tokenName = 'token'; const tokenName = 'token';
@ -12,74 +13,65 @@ const tokenName = 'token';
}) })
export class AuthService { export class AuthService {
private isLogged$ = new BehaviorSubject(false); public user: User = null;
private url = `${environment.apiBaseUrl}/api/auth`; private url = `${environment.apiBaseUrl}/api`;
private user = { username: 'Luke', email: 'Luke@skywalker.com' }; // some data about user
constructor(private http: HttpClient) { constructor(private http: HttpClient) {}
}
public get isLoggedIn(): boolean { public get isLoggedIn(): boolean {
return this.isLogged$.value; return !!this.user?.access_token
} }
public login(data): Observable<any> { public login(data): Observable<any> {
return this.http.post(`${this.url}/login`, data) // Create form
let formData: FormData = new FormData();
formData.append('username', data.username);
formData.append('password', data.password);
return this.http.post(`${this.url}/login`, formData)
.pipe( .pipe(
map((res: { user: any, token: string }) => { map((res: any) => {
this.user = res.user; this._handleUser(res);
localStorage.setItem(tokenName, res.token);
// only for example
localStorage.setItem('username', res.user.username);
localStorage.setItem('email', res.user.email);
this.isLogged$.next(true);
return this.user;
})); }));
} }
public edit(formData: any){
return this.http.post(`${this.url}/user/edit`, formData)
.pipe(map((res: any) => {
this._handleUser(res);
}));
}
_handleUser(res: any){
const user: any = res.user;
user.access_token = res.access_token;
user.token_type = res.token_type;
localStorage.setItem("session", JSON.stringify(user));
this.init();
}
public logout() { public logout() {
return this.http.get(`${this.url}/logout`) return this.http.get(`${this.url}/logout`)
.pipe(map((data) => { .pipe(map((data) => {
localStorage.clear(); this.clearData();
this.user = null;
this.isLogged$.next(false);
return of(false); return of(false);
})); }));
} }
public signup(data) {
return this.http.post(`${this.url}/signup`, data) public clearData(){
.pipe( this.user = null;
map((res: { user: any, token: string }) => { localStorage.clear();
this.user = res.user;
localStorage.setItem(tokenName, res.token);
// only for example
localStorage.setItem('username', res.user.username);
localStorage.setItem('email', res.user.email);
this.isLogged$.next(true);
return this.user;
}));
} }
public get authToken(): string { public get authToken(): string {
return localStorage.getItem(tokenName); return localStorage.getItem(tokenName);
} }
public get userData(): Observable<any> {
// send current user or load data from backend using token
return this.loadUser();
}
private loadUser(): Observable<any> { public init() {
// use request to load user data with token this.user = JSON.parse(localStorage.getItem('session'));
// it's fake and useing only for example
if (localStorage.getItem('username') && localStorage.getItem('email')) {
this.user = {
username: localStorage.getItem('username'),
email: localStorage.getItem('email'),
};
}
return of(this.user);
} }
} }

72
wg_dashboard_frontend/src/app/services/auth/fakebackend.interceptor.ts

@ -1,72 +0,0 @@
import {
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest,
HttpResponse,
} from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { delay, dematerialize, materialize, mergeMap } from 'rxjs/operators';
@Injectable()
export class FakeBackendInterceptor implements HttpInterceptor {
private username = 'Luke';
private email = 'Luke@skywalker.com';
constructor() { }
// with real backend you don't need it at all
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return of(null).pipe(mergeMap(() => {
// signup
if (request.url.endsWith('/api/auth/signup') && request.method === 'POST') {
const body = {
token: 'token_' + this.makeID(),
user: {
username: request.body['username'],
email: request.body['email'],
},
};
return of(new HttpResponse({ body, status: 200 }));
}
// login
if (request.url.endsWith('/api/auth/login') && request.method === 'POST') {
const body = {
token: 'token_' + this.makeID(),
user: {
username: this.username,
email: request.body['email'],
},
};
return of(new HttpResponse({ body, status: 200 }));
}
// logout
if (request.url.endsWith('/api/auth/logout') && request.method === 'GET') {
return of(new HttpResponse({ body: { success: true }, status: 200 }));
}
// at default just process the request
return next.handle(request);
}))
.pipe(materialize())
.pipe(delay(500))
.pipe(dematerialize());
}
// generate random token
private makeID(): string {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 25; i = i + 1) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
}

1
wg_dashboard_frontend/src/app/services/auth/index.ts

@ -1,4 +1,3 @@
export { AuthGuard } from './auth.guard'; export { AuthGuard } from './auth.guard';
export { AuthService } from './auth.service'; export { AuthService } from './auth.service';
export { AuthInterceptor } from './auth.interceptor'; export { AuthInterceptor } from './auth.interceptor';
export { FakeBackendInterceptor } from './fakebackend.interceptor';

3
wg_dashboard_frontend/src/theme/components/sidebar/menu-link-item.component.ts

@ -30,7 +30,8 @@ export class MenuLinkItemComponent {
private navigate() { private navigate() {
const layout = (document.querySelector('.mdl-layout') as any).MaterialLayout; const layout = (document.querySelector('.mdl-layout') as any).MaterialLayout;
if (layout.drawer_.getAttribute('aria-hidden') !== 'true') {
if (layout.drawer_ && layout.drawer_.getAttribute('aria-hidden') !== 'true') {
layout.toggleDrawer(); layout.toggleDrawer();
} }
} }

Loading…
Cancel
Save