diff --git a/wg_dashboard_backend/db/user.py b/wg_dashboard_backend/db/user.py index d766d7f..e7b7513 100644 --- a/wg_dashboard_backend/db/user.py +++ b/wg_dashboard_backend/db/user.py @@ -1,7 +1,11 @@ +from typing import Optional + from sqlalchemy.orm import Session import models from passlib.context import CryptContext +import schemas + pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") @@ -13,13 +17,22 @@ def get_password_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) - if not user: - return False - if not verify_password(password, user.password): - return False - return user + if user and verify_password(password, user.password): + return user + return None def get_user_by_name(db: Session, username: str) -> models.User: diff --git a/wg_dashboard_backend/main.py b/wg_dashboard_backend/main.py index 5257c5a..f53a0eb 100644 --- a/wg_dashboard_backend/main.py +++ b/wg_dashboard_backend/main.py @@ -1,6 +1,7 @@ import logging +import os -from sqlalchemy.exc import OperationalError +from sqlalchemy_utils import database_exists logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) @@ -21,7 +22,7 @@ from datetime import datetime, timedelta import db.wireguard import db.user import jwt -from fastapi import Depends, FastAPI, HTTPException, status, Form +from fastapi import Depends, FastAPI, HTTPException, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from jwt import PyJWTError import script.wireguard @@ -35,16 +36,24 @@ engine = sqlalchemy.create_engine( const.DATABASE_URL, connect_args={"check_same_thread": False} ) -try: - models.Base.metadata.create_all(engine) -except OperationalError as e: - pass - -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token") +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/token") 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 def get_db(): @@ -66,28 +75,45 @@ def create_access_token(*, data: dict, expires_delta: timedelta = None): 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( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) + try: payload = jwt.decode(token, const.SECRET_KEY, algorithms=[const.ALGORITHM]) username: str = payload.get("sub") if username is None: raise credentials_exception - token_data = schemas.TokenData(username=username) + except PyJWTError: 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: raise credentials_exception return user -@app.post("/api/token", response_model=schemas.Token) -def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), sess: Session = Depends(get_db)): +@app.get("/api/logout") +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) if not user: @@ -96,17 +122,23 @@ def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), ses detail="Incorrect username or password", headers={"WWW-Authenticate": "Bearer"}, ) + + # Create token access_token_expires = timedelta(minutes=const.ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( 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.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) for iface in interfaces: 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) -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: raise HTTPException(status_code=400, 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) -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"] 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) -def generate_key_pair(): +def generate_key_pair(user: schemas.User = Depends(auth)): private_key, public_key = script.wireguard.generate_keys() return schemas.KeyPair( private_key=private_key, @@ -165,21 +204,28 @@ def generate_key_pair(): @app.get("/api/wg/generate_psk", response_model=schemas.PSK) -def generate_psk(): +def generate_psk(user: schemas.User = Depends(auth)): return schemas.PSK( psk=script.wireguard.generate_psk() ) @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) form_data.is_running = script.wireguard.is_running(form_data) return form_data @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) script.wireguard.start_interface(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) -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) script.wireguard.restart_interface(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) -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 if script.wireguard.is_running(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) -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) # 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) -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): 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) -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) 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") -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) return JSONResponse(content=stats) @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() 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) -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: conf_file = f.read() @@ -266,7 +342,11 @@ def config_server(form_data: schemas.WGServer): @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 already exists @@ -293,6 +373,7 @@ def create_user(form_data: schemas.UserInDB, sess: Session = Depends(get_db)): ), sess) + @app.on_event("startup") async def startup(): await database.connect() diff --git a/wg_dashboard_backend/schemas.py b/wg_dashboard_backend/schemas.py index a12283d..7d60b32 100644 --- a/wg_dashboard_backend/schemas.py +++ b/wg_dashboard_backend/schemas.py @@ -3,21 +3,15 @@ from pydantic import BaseModel, typing import models -class Token(BaseModel): - access_token: str - token_type: str - - -class TokenData(BaseModel): - username: str = None - - class User(BaseModel): username: str = None email: str = None full_name: str = None role: str = None + class Config: + orm_mode = True + class UserInDB(User): password: str @@ -26,6 +20,15 @@ class UserInDB(User): orm_mode = True +class Token(BaseModel): + access_token: str + token_type: str + user: User + + class Config: + orm_mode = True + + class WGPeer(BaseModel): name: str = None address: str = None diff --git a/wg_dashboard_frontend/src/app/app-routing.module.ts b/wg_dashboard_frontend/src/app/app-routing.module.ts index f613c40..53a5223 100644 --- a/wg_dashboard_frontend/src/app/app-routing.module.ts +++ b/wg_dashboard_frontend/src/app/app-routing.module.ts @@ -4,7 +4,11 @@ import { RouterModule } from '@angular/router'; import { LayoutsModule } from './layouts'; import { CommonLayoutComponent } from './layouts/common-layout'; 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({ @@ -14,12 +18,21 @@ import {LoginComponent} from "./pages/pages/login"; { path: '', redirectTo: 'app/dashboard', pathMatch: 'full' }, { 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: '**', 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' }, ], { useHash: true }, diff --git a/wg_dashboard_frontend/src/app/app.component.ts b/wg_dashboard_frontend/src/app/app.component.ts index 5f0bca1..339dc7e 100644 --- a/wg_dashboard_frontend/src/app/app.component.ts +++ b/wg_dashboard_frontend/src/app/app.component.ts @@ -1,7 +1,15 @@ import { Component } from '@angular/core'; +import {AuthService} from "@services/*"; @Component({ selector: 'app-root', template: ``, }) -export class AppComponent { } +export class AppComponent { + + constructor(private auth: AuthService) { + auth.init() + } + + +} diff --git a/wg_dashboard_frontend/src/app/app.module.ts b/wg_dashboard_frontend/src/app/app.module.ts index d586914..c9b7489 100644 --- a/wg_dashboard_frontend/src/app/app.module.ts +++ b/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 { BrowserModule } from '@angular/platform-browser'; -import { AuthInterceptor, AuthService, FakeBackendInterceptor } from '@services/*'; +import { AuthInterceptor, AuthService } from '@services/*'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; @@ -11,6 +11,7 @@ import { DashboardModule } from './pages/dashboard'; import { VarDirective } from './directives/var.directive'; import { QRCodeModule } from 'angularx-qrcode'; import {NgbModule} from "@ng-bootstrap/ng-bootstrap"; +import {UserModule} from "./pages/user/user.module"; @NgModule({ declarations: [AppComponent, VarDirective], @@ -19,6 +20,7 @@ import {NgbModule} from "@ng-bootstrap/ng-bootstrap"; AppRoutingModule, ComponentsModule, DashboardModule, + UserModule, HttpClientModule, NgbModule, QRCodeModule @@ -29,12 +31,7 @@ import {NgbModule} from "@ng-bootstrap/ng-bootstrap"; provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true, - }, - { - provide: HTTP_INTERCEPTORS, - useClass: FakeBackendInterceptor, - multi: true, - }, + } ], bootstrap: [AppComponent], exports: [ diff --git a/wg_dashboard_frontend/src/app/interfaces/user.ts b/wg_dashboard_frontend/src/app/interfaces/user.ts new file mode 100644 index 0000000..47b6c30 --- /dev/null +++ b/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, + +} diff --git a/wg_dashboard_frontend/src/app/layouts/common-layout/common-layout.component.html b/wg_dashboard_frontend/src/app/layouts/common-layout/common-layout.component.html index 8eaf86b..2a5a280 100644 --- a/wg_dashboard_frontend/src/app/layouts/common-layout/common-layout.component.html +++ b/wg_dashboard_frontend/src/app/layouts/common-layout/common-layout.component.html @@ -26,28 +26,28 @@ -
- Logged in as Admin +
+ Logged in as {{auth.user?.username}}
- +
  • exit_to_app Log out
  • - --> +
    diff --git a/wg_dashboard_frontend/src/app/layouts/common-layout/common-layout.component.ts b/wg_dashboard_frontend/src/app/layouts/common-layout/common-layout.component.ts index 99d2fc9..57c8e4c 100644 --- a/wg_dashboard_frontend/src/app/layouts/common-layout/common-layout.component.ts +++ b/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 { public title = 'Wireguard Manager'; 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() { - this.authService.userData.subscribe(user => this.user = user ? user : { - username: 'Luke', - email: 'Luke@skywalker.com', - }); + } public logout() { - this.authService.logout() - .subscribe(res => this.router.navigate(['/pages/login'])); + this.auth.logout() + .subscribe(res => this.router.navigate(['/user/login'])); } } diff --git a/wg_dashboard_frontend/src/app/pages/pages/login/index.ts b/wg_dashboard_frontend/src/app/pages/pages/login/index.ts deleted file mode 100644 index a40d447..0000000 --- a/wg_dashboard_frontend/src/app/pages/pages/login/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { LoginComponent } from './login.component'; diff --git a/wg_dashboard_frontend/src/app/pages/pages/login/login.component.html b/wg_dashboard_frontend/src/app/pages/pages/login/login.component.html deleted file mode 100644 index cb18206..0000000 --- a/wg_dashboard_frontend/src/app/pages/pages/login/login.component.html +++ /dev/null @@ -1,85 +0,0 @@ -
    - - -
    -

    Login to Wireguard Manager

    -
    - - -
    - - - - - - -
    - - -
    - - - Forgot password? - Don't have account? - - - -
    - - -
    - -
    - - -
    - - diff --git a/wg_dashboard_frontend/src/app/pages/pages/pages.module.ts b/wg_dashboard_frontend/src/app/pages/pages/pages.module.ts index db648d6..28816ce 100644 --- a/wg_dashboard_frontend/src/app/pages/pages/pages.module.ts +++ b/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 { ErrorComponent } from './error'; import { ForgotPasswordComponent } from './forgot-password'; -import { LoginComponent } from './login'; + import { PagesRoutingModule } from './pages-routing.module'; import { SignUpComponent } from './sign-up'; + @NgModule({ imports: [ CommonModule, @@ -22,7 +23,6 @@ import { SignUpComponent } from './sign-up'; ], declarations: [ ErrorComponent, - LoginComponent, SignUpComponent, ForgotPasswordComponent, ], diff --git a/wg_dashboard_frontend/src/app/pages/user/edit/edit.component.html b/wg_dashboard_frontend/src/app/pages/user/edit/edit.component.html new file mode 100644 index 0000000..6e2720c --- /dev/null +++ b/wg_dashboard_frontend/src/app/pages/user/edit/edit.component.html @@ -0,0 +1,48 @@ +
    + + + +

    Edit User

    +
    + + +
    + +
    +
    + + +
    + + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + + + + + +
    + + +
    +
    + +
    diff --git a/wg_dashboard_frontend/src/app/pages/user/edit/edit.component.scss b/wg_dashboard_frontend/src/app/pages/user/edit/edit.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/wg_dashboard_frontend/src/app/pages/user/edit/edit.component.ts b/wg_dashboard_frontend/src/app/pages/user/edit/edit.component.ts new file mode 100644 index 0000000..5cd430a --- /dev/null +++ b/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); + } + } + +} diff --git a/wg_dashboard_frontend/src/app/pages/user/login/login.component.html b/wg_dashboard_frontend/src/app/pages/user/login/login.component.html new file mode 100644 index 0000000..ea2cfce --- /dev/null +++ b/wg_dashboard_frontend/src/app/pages/user/login/login.component.html @@ -0,0 +1,40 @@ +
    + + + +

    Edit User

    +
    + + +
    + +
    + + +
    + + +
    + +
    + + +
    + + +
    + + + + + + +
    + + +
    +
    + +
    diff --git a/wg_dashboard_frontend/src/app/pages/user/login/login.component.scss b/wg_dashboard_frontend/src/app/pages/user/login/login.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/wg_dashboard_frontend/src/app/pages/user/login/login.component.spec.ts b/wg_dashboard_frontend/src/app/pages/user/login/login.component.spec.ts new file mode 100644 index 0000000..d6d85a8 --- /dev/null +++ b/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; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ LoginComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/wg_dashboard_frontend/src/app/pages/pages/login/login.component.ts b/wg_dashboard_frontend/src/app/pages/user/login/login.component.ts similarity index 58% rename from wg_dashboard_frontend/src/app/pages/pages/login/login.component.ts rename to wg_dashboard_frontend/src/app/pages/user/login/login.component.ts index 9113d41..0cfee7b 100644 --- a/wg_dashboard_frontend/src/app/pages/pages/login/login.component.ts +++ b/wg_dashboard_frontend/src/app/pages/user/login/login.component.ts @@ -1,37 +1,32 @@ import { Component, OnInit } from '@angular/core'; -import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; -import { Router } from '@angular/router'; - - -import { BlankLayoutCardComponent } from 'app/components/blank-layout-card'; +import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms"; import {AuthService} from "@services/*"; +import {Router} from "@angular/router"; @Component({ selector: 'app-login', - styleUrls: ['../../../components/blank-layout-card/blank-layout-card.component.scss'], templateUrl: './login.component.html', + styleUrls: ['./login.component.scss'] }) -export class LoginComponent extends BlankLayoutCardComponent implements OnInit { +export class LoginComponent implements OnInit { + public loginForm: FormGroup; - public email; + public username; public password; - public emailPattern = '^([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,5})$'; public error: string; constructor(private authService: AuthService, private fb: FormBuilder, private router: Router) { - super(); + this.loginForm = this.fb.group({ password: new FormControl('', Validators.required), - email: new FormControl('', [ + username: new FormControl('', [ 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'); } @@ -47,11 +42,12 @@ export class LoginComponent extends BlankLayoutCardComponent implements OnInit { if (this.loginForm.valid) { this.authService.login(this.loginForm.getRawValue()) .subscribe(res => this.router.navigate(['/app/dashboard']), - error => this.error = error.message); + error => this.error = error.message); } } public onInputChange(event) { event.target.required = true; } + } diff --git a/wg_dashboard_frontend/src/app/pages/user/user.module.ts b/wg_dashboard_frontend/src/app/pages/user/user.module.ts new file mode 100644 index 0000000..cd2cf77 --- /dev/null +++ b/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 { + +} diff --git a/wg_dashboard_frontend/src/app/services/auth/auth.guard.ts b/wg_dashboard_frontend/src/app/services/auth/auth.guard.ts index 9d68626..d1e1898 100644 --- a/wg_dashboard_frontend/src/app/services/auth/auth.guard.ts +++ b/wg_dashboard_frontend/src/app/services/auth/auth.guard.ts @@ -16,7 +16,7 @@ export class AuthGuard implements CanActivate { return true; } // Navigate to the login page with extras - this.router.navigate(['/login']); + this.router.navigate(['/user/login']); return false; } diff --git a/wg_dashboard_frontend/src/app/services/auth/auth.interceptor.ts b/wg_dashboard_frontend/src/app/services/auth/auth.interceptor.ts index cca0f7a..2ce66ea 100644 --- a/wg_dashboard_frontend/src/app/services/auth/auth.interceptor.ts +++ b/wg_dashboard_frontend/src/app/services/auth/auth.interceptor.ts @@ -1,7 +1,8 @@ import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs/index'; +import { Observable } from 'rxjs'; import { + HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, @@ -9,19 +10,31 @@ import { } from '@angular/common/http'; import { AuthService } from './auth.service'; +import {tap} from "rxjs/operators"; +import {Router} from "@angular/router"; @Injectable() export class AuthInterceptor implements HttpInterceptor { - constructor(private auth: AuthService) { - } + constructor(private auth: AuthService, private router: Router) {} public intercept(request: HttpRequest, next: HttpHandler): Observable> { // add authorization token for full api requests if (request.url.includes('api') && this.auth.isLoggedIn) { 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']); + } + })); } } diff --git a/wg_dashboard_frontend/src/app/services/auth/auth.service.ts b/wg_dashboard_frontend/src/app/services/auth/auth.service.ts index 33e7981..c966c0f 100644 --- a/wg_dashboard_frontend/src/app/services/auth/auth.service.ts +++ b/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 { environment } from '../../../environments/environment'; +import {User} from "../../interfaces/user"; const tokenName = 'token'; @@ -12,74 +13,65 @@ const tokenName = 'token'; }) export class AuthService { - private isLogged$ = new BehaviorSubject(false); - private url = `${environment.apiBaseUrl}/api/auth`; - private user = { username: 'Luke', email: 'Luke@skywalker.com' }; // some data about user + public user: User = null; + private url = `${environment.apiBaseUrl}/api`; - constructor(private http: HttpClient) { - - } + constructor(private http: HttpClient) {} public get isLoggedIn(): boolean { - return this.isLogged$.value; + return !!this.user?.access_token } public login(data): Observable { - 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( - map((res: { user: any, token: string }) => { - 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; + map((res: any) => { + this._handleUser(res); })); } + 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() { return this.http.get(`${this.url}/logout`) .pipe(map((data) => { - localStorage.clear(); - this.user = null; - this.isLogged$.next(false); + this.clearData(); return of(false); })); } - public signup(data) { - return this.http.post(`${this.url}/signup`, data) - .pipe( - map((res: { user: any, token: string }) => { - 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 clearData(){ + this.user = null; + localStorage.clear(); + } public get authToken(): string { return localStorage.getItem(tokenName); } - public get userData(): Observable { - // send current user or load data from backend using token - return this.loadUser(); - } - private loadUser(): Observable { - // use request to load user data with token - // 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); + public init() { + this.user = JSON.parse(localStorage.getItem('session')); } } diff --git a/wg_dashboard_frontend/src/app/services/auth/fakebackend.interceptor.ts b/wg_dashboard_frontend/src/app/services/auth/fakebackend.interceptor.ts deleted file mode 100644 index 6e74ea1..0000000 --- a/wg_dashboard_frontend/src/app/services/auth/fakebackend.interceptor.ts +++ /dev/null @@ -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, next: HttpHandler): Observable> { - 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; - } -} diff --git a/wg_dashboard_frontend/src/app/services/auth/index.ts b/wg_dashboard_frontend/src/app/services/auth/index.ts index c0b0a71..cc02d1a 100644 --- a/wg_dashboard_frontend/src/app/services/auth/index.ts +++ b/wg_dashboard_frontend/src/app/services/auth/index.ts @@ -1,4 +1,3 @@ export { AuthGuard } from './auth.guard'; export { AuthService } from './auth.service'; export { AuthInterceptor } from './auth.interceptor'; -export { FakeBackendInterceptor } from './fakebackend.interceptor'; diff --git a/wg_dashboard_frontend/src/theme/components/sidebar/menu-link-item.component.ts b/wg_dashboard_frontend/src/theme/components/sidebar/menu-link-item.component.ts index 116e65c..936cf96 100644 --- a/wg_dashboard_frontend/src/theme/components/sidebar/menu-link-item.component.ts +++ b/wg_dashboard_frontend/src/theme/components/sidebar/menu-link-item.component.ts @@ -30,7 +30,8 @@ export class MenuLinkItemComponent { private navigate() { 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(); } }