├── .gitignore ├── README.md ├── client ├── app │ ├── app-routing.module.ts │ ├── base.component.ts │ ├── base.html │ ├── base.module.ts │ ├── components │ │ ├── campgrounds │ │ │ ├── campground.detail.component.html │ │ │ ├── campground.detail.component.ts │ │ │ ├── campground.form.component.html │ │ │ ├── campground.form.component.ts │ │ │ ├── campgrounds.component.css │ │ │ ├── campgrounds.component.html │ │ │ ├── campgrounds.component.ts │ │ │ ├── comment.form.component.html │ │ │ └── comment.form.component.ts │ │ └── user │ │ │ ├── profile.component.html │ │ │ ├── profile.component.ts │ │ │ ├── user.component.html │ │ │ └── user.component.ts │ ├── main.ts │ ├── models │ │ ├── campground.ts │ │ ├── campgroundDetail.ts │ │ └── comment.ts │ └── services │ │ ├── api.service.ts │ │ ├── campgounds.service.ts │ │ └── user.service.ts ├── index.html ├── polyfills.ts └── systemjs.config.js ├── gulpfile.js ├── gulpfile.ts ├── package-lock.json ├── package.json ├── server └── src │ ├── Authentication.ts │ ├── Passport.ts │ ├── Server.ts │ ├── bin │ └── www │ ├── database │ ├── DatabaseSchema.ts │ └── DatabaseService.ts │ ├── model │ ├── Campground.ts │ ├── Comment.ts │ └── User.ts │ ├── repository │ ├── BaseRepository.ts │ ├── CampgroundRepository.ts │ ├── CommentRepository.ts │ └── UserRepository.ts │ └── routes │ ├── Campground.ts │ ├── Comment.ts │ └── User.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | *.iml 3 | node_modules/* 4 | dist/* 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fullstack web application using Angular4, Bootstrap4, Primeng, Express, Mysql, Gulp and Passport. 2 | 3 | ## Introduction 4 | This project demonstrates the basic CRUD functions combining Angular4, Bootstrap4, Primeng, Express, Mysql, Gulp4 and Passport. 5 | It also uses Gulp for running task and TsLint. You can use this web application doing user signup/login, campground create/edit/delete, 6 | and comment create/edit/delete. 7 | 8 | ![Screen Shot](https://bitbucket.org/repo/xbqg8L/images/3191778081-Screen%20Shot%202018-07-28%20at%2018.45.57.png) 9 | 10 | ## Prerequisites 11 | 1. The latest version of Nodejs need to be installed. 12 | 2. Docker MySQL5 container 13 | 14 | ### Docker MySQL container preparation 15 | 1. Run `docker pull mysql:5` 16 | 2. Run `docker volume create mysqldata` 17 | 3. Run `docker run --name mysql5 -p 3306:3306 -v mysqldata:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:5` 18 | 4. Run `docker run -it --link mysql5:mysql --rm mysql:5 sh -c 'exec mysql -h"$MYSQL_PORT_3306_TCP_ADDR" -P"$MYSQL_PORT_3306_TCP_PORT" -uroot -p"$MYSQL_ENV_MYSQL_ROOT_PASSWORD"'` 19 | 5. Setup database and user 20 | ``` 21 | mysql> CREATE DATABASE yelpcamp; 22 | mysql> CREATE USER 'sa'@'172.17.0.1' IDENTIFIED BY '(IJN8uhb'; 23 | mysql> GRANT ALL ON yelpcamp.* TO 'sa'@'172.17.0.1'; 24 | mysql> FLUSH PRIVILEGES; 25 | ``` 26 | 27 | 6. Check privileges `mysql> SHOW GRANTS FOR 'sa'@'172.17.0.1';` 28 | 7. If you want to rerun docker container, run `docker run mysql5` 29 | 30 | ## Api Document (from Express's view) 31 | ``` 32 | 1. getAllCampgrounds (GET) http://localhost:8080/api/campground 33 | 2. createCampground (POST) http://localhost:8080/api/campground 34 | 3. getOneCampground (GET) http://localhost:8080/api/campground/:id 35 | 4. getOneCampground (GET) http://localhost:8080/api/campground/:id/edit 36 | 5. editCampground (PUT) http://localhost:8080/api/campground/:id/edit 37 | 5. deleteCampground (DELETE) http://localhost:8080/api/campground/:id 38 | 6. createComment (POST) http://localhost:8080/api/comment 39 | 7. getComment (GET) http://localhost:8080/api/comment/:comment_id/edit 40 | 8. editComment (PUT) http://localhost:8080/api/comment/:comment_id/edit 41 | 9. deleteComment (DELETE) http://localhost:8080/api/comment/:comment_id 42 | 10. signup (POST) http://localhost:8080/api/signup 43 | 11. login (POST) http://localhost:8080/api/login 44 | 12. logout (GET) http://localhost:8080/api/logout 45 | 13. profile (GET) http://localhost:8080/api/profile 46 | ``` 47 | 48 | ### How do I get set up? ### 49 | 50 | * Clone the repo: 51 | ``` 52 | git clone https://LaurenceHo@bitbucket.org/LaurenceHo/angular4-express-mysql.git 53 | ``` 54 | or 55 | ``` 56 | git clone https://github.com/LaurenceHo/angular4-express-mysql.git 57 | ``` 58 | 59 | * Install npm package. Build and compile the dist folder: 60 | ``` 61 | npm install 62 | npm run build 63 | ``` 64 | 65 | * Launch the whole web application: 66 | ``` 67 | npm run start-all 68 | ``` 69 | 70 | * Visit in your browser: `http://localhost:8080` 71 | 72 | ### Reference 73 | This project is based on https://github.com/moizKachwala/Angular2-express-mongoose-gulp-node-typescript. I learnt a lot from this project. 74 | 75 | ### Existing major issues: 76 | 1. browserSync cannot reload the browser automatically when modifying client side code. 77 | 2. When user refreshes the browser, the login session would be gone, and user needs to login again. 78 | 3. Some UI issues did not solve yet: 79 | * When user do place comment, the UI does display comment properly 80 | -------------------------------------------------------------------------------- /client/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Laurence Ho on 07-02-2017. 3 | */ 4 | import { NgModule } from '@angular/core'; 5 | import { RouterModule, Routes } from '@angular/router'; 6 | 7 | import { CampgroundDetailComponent } from './components/campgrounds/campground.detail.component'; 8 | import { CampgroundFormComponent } from './components/campgrounds/campground.form.component'; 9 | import { CampgroundsComponent } from './components/campgrounds/campgrounds.component'; 10 | import { CommentFormComponent } from './components/campgrounds/comment.form.component'; 11 | import { ProfileComponent } from './components/user/profile.component'; 12 | import { UserComponent } from './components/user/user.component'; 13 | 14 | const appRoutes: Routes = [ 15 | { 16 | path: '', 17 | pathMatch: 'full', 18 | redirectTo: 'campground' 19 | }, 20 | { 21 | path: 'campground', 22 | component: CampgroundsComponent 23 | }, 24 | { 25 | path: 'campground/detail/:id', 26 | component: CampgroundDetailComponent 27 | }, 28 | { 29 | path: 'campground/new', 30 | component: CampgroundFormComponent 31 | }, 32 | { 33 | path: 'campground/detail/:id/edit', 34 | component: CampgroundFormComponent 35 | }, 36 | { 37 | path: 'campground/detail/:id/comment/new', 38 | component: CommentFormComponent 39 | }, 40 | { 41 | path: 'campground/detail/:id/comment/:comment_id/edit', 42 | component: CommentFormComponent 43 | }, 44 | { 45 | path: 'login', 46 | component: UserComponent 47 | }, 48 | { 49 | path: 'signup', 50 | component: UserComponent 51 | }, 52 | { 53 | path: 'logout', 54 | component: UserComponent 55 | }, 56 | { 57 | path: 'profile', 58 | component: ProfileComponent 59 | }, 60 | { 61 | path: '**', 62 | component: CampgroundsComponent 63 | } 64 | ]; 65 | 66 | @NgModule({ 67 | imports: [ RouterModule.forRoot(appRoutes) ], 68 | exports: [ RouterModule ] 69 | }) 70 | 71 | export class AppRoutingModule { 72 | } 73 | -------------------------------------------------------------------------------- /client/app/base.component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Laurence Ho on 07-02-2017. 3 | */ 4 | 5 | import { Component, OnInit } from '@angular/core'; 6 | import { NavigationEnd, Router } from '@angular/router'; 7 | import 'rxjs/add/operator/filter'; 8 | import { UserService } from './services/user.service'; 9 | 10 | @Component({ 11 | selector: 'my-app', 12 | templateUrl: './app/base.html' 13 | }) 14 | 15 | export class BaseComponent implements OnInit { 16 | title = 'YelpCamp'; 17 | userdata: any; 18 | 19 | constructor(private userService: UserService, private router: Router) { 20 | } 21 | 22 | ngOnInit() { 23 | this.router.events.filter(event => event instanceof NavigationEnd) 24 | .subscribe(event => { 25 | this.userService.getProfile().subscribe(data => { 26 | if (data) { 27 | this.userdata = this.userService.getUserData(); 28 | } else { 29 | this.userService.flush(); 30 | this.userdata = null; 31 | } 32 | }); 33 | } 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /client/app/base.html: -------------------------------------------------------------------------------- 1 | 16 | 17 |
18 | 19 |
20 | -------------------------------------------------------------------------------- /client/app/base.module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Laurence Ho on 07-02-2017. 3 | */ 4 | 5 | import { HashLocationStrategy, LocationStrategy } from '@angular/common'; 6 | import { NgModule } from '@angular/core'; 7 | import { FormsModule } from '@angular/forms'; 8 | import { HttpModule } from '@angular/http'; 9 | import { BrowserModule } from '@angular/platform-browser'; 10 | import { ButtonModule } from 'primeng/primeng'; 11 | 12 | import { AppRoutingModule } from './app-routing.module'; 13 | import { BaseComponent } from './base.component'; 14 | 15 | import { ApiService } from './services/api.service'; 16 | import { CampgroundService } from './services/campgounds.service'; 17 | import { UserService } from './services/user.service'; 18 | 19 | import { CampgroundDetailComponent } from './components/campgrounds/campground.detail.component'; 20 | import { CampgroundFormComponent } from './components/campgrounds/campground.form.component'; 21 | import { CampgroundsComponent } from './components/campgrounds/campgrounds.component'; 22 | import { CommentFormComponent } from './components/campgrounds/comment.form.component'; 23 | import { ProfileComponent } from './components/user/profile.component'; 24 | import { UserComponent } from './components/user/user.component'; 25 | 26 | @NgModule({ 27 | imports: [ 28 | // Angular Module 29 | BrowserModule, 30 | HttpModule, 31 | FormsModule, 32 | AppRoutingModule, 33 | // Primeng Module 34 | ButtonModule, 35 | ], 36 | declarations: [ 37 | BaseComponent, 38 | CampgroundsComponent, 39 | CampgroundDetailComponent, 40 | CampgroundFormComponent, 41 | CommentFormComponent, 42 | UserComponent, 43 | ProfileComponent 44 | ], 45 | providers: [ 46 | ApiService, 47 | CampgroundService, 48 | UserService, 49 | {provide: LocationStrategy, useClass: HashLocationStrategy} 50 | ], 51 | bootstrap: [ BaseComponent ] 52 | }) 53 | export class BaseModule { 54 | } 55 | -------------------------------------------------------------------------------- /client/app/components/campgrounds/campground.detail.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

YelpCamp

5 |
    6 |
  • Campground Info
  • 7 |
  • Other Info
  • 8 |
9 |
10 |
11 |
12 | 13 |
14 |

${{campDetail?.campground?.price ? campDetail?.campground?.price : 15 | 'Unknown'}}NZD/night

16 |

17 | {{campDetail?.campground?.name}} 18 |

19 |

20 | {{campDetail?.campground?.description}} 21 |

22 |

Submitted By {{campDetail?.campground?.username}}

23 |
24 | 29 | 34 |
35 |
36 |
37 |
38 |

Comments

39 |
40 | 45 |
46 | 47 | 50 | 51 |
52 |
53 |
54 | {{comment.username}} 55 |

56 | {{comment.text}} 57 |

58 |
59 | 64 | 69 |
70 |
71 |
72 |
73 | Go Back 74 |
75 |
76 |
77 | -------------------------------------------------------------------------------- /client/app/components/campgrounds/campground.detail.component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Laurence Ho on 08-02-2017. 3 | */ 4 | 5 | import { Component, OnInit } from '@angular/core'; 6 | import { ActivatedRoute, Params, Router } from '@angular/router'; 7 | import 'rxjs/add/operator/switchMap'; 8 | 9 | import * as _ from 'lodash'; 10 | import { CampgroundDetail } from '../../models/campgroundDetail'; 11 | import { Comment } from '../../models/comment'; 12 | import { CampgroundService } from '../../services/campgounds.service'; 13 | import { UserService } from '../../services/user.service'; 14 | 15 | @Component({ 16 | selector: 'campDetail', 17 | templateUrl: './app/components/campgrounds/campground.detail.component.html', 18 | styleUrls: [ './app/components/campgrounds/campgrounds.component.css' ] 19 | }) 20 | 21 | export class CampgroundDetailComponent implements OnInit { 22 | selectedComment: Comment; 23 | 24 | campDetail: CampgroundDetail = new CampgroundDetail(); 25 | userdata: any; 26 | 27 | constructor(private campgroundService: CampgroundService, 28 | private userService: UserService, 29 | private route: ActivatedRoute, 30 | private router: Router) { 31 | } 32 | 33 | ngOnInit() { 34 | this.route.params 35 | .switchMap((params: Params) => this.campgroundService.getCampgroundDetail(params.id)) 36 | .subscribe(data => this.campDetail = data); 37 | this.userdata = this.userService.getUserData(); 38 | } 39 | 40 | doLogin() { 41 | this.router.navigateByUrl('/login'); 42 | } 43 | 44 | doEditCampground(id: number) { 45 | this.router.navigateByUrl('/campground/detail/' + id + '/edit'); 46 | } 47 | 48 | doDeleteCampground(id: number) { 49 | if (this.route.snapshot.url[ 0 ].path === 'campground') { 50 | this.campgroundService.deleteCampground(id).subscribe(data => { 51 | if (data.message === 'OK') { 52 | this.router.navigate([ '/campground' ]); 53 | } 54 | }); 55 | } 56 | } 57 | 58 | doEditComment(comment: Comment) { 59 | this.selectedComment = comment; 60 | } 61 | 62 | doDeleteComment(comment_id: number) { 63 | this.campgroundService.deleteComment(comment_id) 64 | .subscribe(data => { 65 | if (data.message === 'OK') { 66 | _.remove(this.campDetail.comments, comment => { 67 | return comment.id === comment_id; 68 | }); 69 | } 70 | }); 71 | } 72 | 73 | updateUI(comment: Comment) { 74 | // FIXME let tempComment = comment[ 'comment' ]; 75 | this.campDetail.comments.push(comment); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /client/app/components/campgrounds/campground.form.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Create a new campground

5 |

7 | Edit "{{campground.name}}"

8 |
9 |
10 |
11 |
12 |
13 |
14 | 16 |
17 |
18 | 20 |
21 |
22 | 24 |
25 |
26 | 28 |
29 |
30 | 34 |
35 |
36 | Go Back 37 | Go Back 39 |
40 |
41 |
42 | -------------------------------------------------------------------------------- /client/app/components/campgrounds/campground.form.component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by laurence-ho on 2/07/17. 3 | */ 4 | import { Component, OnInit } from '@angular/core'; 5 | import { ActivatedRoute, Router } from '@angular/router'; 6 | import { Campground } from '../../models/campground'; 7 | import { CampgroundService } from '../../services/campgounds.service'; 8 | import { UserService } from '../../services/user.service'; 9 | 10 | @Component({ 11 | selector: 'campDetail', 12 | templateUrl: './app/components/campgrounds/campground.form.component.html', 13 | styleUrls: [ './app/components/campgrounds/campgrounds.component.css' ] 14 | }) 15 | 16 | export class CampgroundFormComponent implements OnInit { 17 | userdata: any; 18 | campground: Campground = new Campground(); 19 | 20 | constructor(private campgroundService: CampgroundService, 21 | private userService: UserService, 22 | private route: ActivatedRoute, 23 | private router: Router) { 24 | } 25 | 26 | ngOnInit() { 27 | this.userdata = this.userService.getUserData(); 28 | 29 | if (!this.userdata) { 30 | this.router.navigate([ '/login' ]); 31 | } 32 | 33 | if (this.route.snapshot.url[ 1 ].path !== 'new' && this.route.snapshot.url[ 3 ].path === 'edit') { 34 | this.campgroundService.getCampground(Number(this.route.snapshot.url[ 2 ].path)) 35 | .subscribe(data => this.campground = data.campground); 36 | } 37 | } 38 | 39 | doSubmit() { 40 | if (this.route.snapshot.url[ 0 ].path === 'campground') { 41 | if (this.route.snapshot.url[ 1 ].path === 'new') { 42 | this.campground.user_id = this.userdata.id; 43 | this.campground.username = this.userdata.username; 44 | 45 | this.campgroundService.createCampground(this.campground) 46 | .subscribe(data => { 47 | if (data.message === 'OK') { 48 | this.router.navigate([ '/campground/detail', data.campground_id ]); 49 | } 50 | }); 51 | } else if (this.route.snapshot.url[ 1 ].path !== 'new' && this.route.snapshot.url[ 3 ].path === 'edit') { 52 | this.campgroundService.editCampground(this.campground) 53 | .subscribe(data => { 54 | if (data.message === 'OK') { 55 | this.router.navigate([ '/campground/detail', this.campground.id ]); 56 | } 57 | }); 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /client/app/components/campgrounds/campgrounds.component.css: -------------------------------------------------------------------------------- 1 | .campground-item { 2 | display: flex; 3 | flex-wrap: wrap; 4 | } 5 | 6 | .thumbnail { 7 | border: lightgray 0.05rem solid; 8 | border-radius: 0.3rem; 9 | position: relative; 10 | padding: 0.4rem; 11 | margin-bottom: 1.8rem; 12 | } 13 | 14 | .thumbnail img { 15 | width: 100%; 16 | } 17 | 18 | .thumbnail .caption-full { 19 | padding: 2rem; 20 | } 21 | 22 | #delete { 23 | display: inline; 24 | } 25 | -------------------------------------------------------------------------------- /client/app/components/campgrounds/campgrounds.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Welcome to YelpCamp

5 |

View our hand-picked campgrounds from all over the world

6 |

7 | 12 |

13 |
14 |
15 | 16 |
17 |
18 |
19 | Card image cap 20 |
21 |
{{camp.name}}
22 |

Price: ${{camp.price}}NZD/night

23 | 28 |
29 |
30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /client/app/components/campgrounds/campgrounds.component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Laurence Ho on 07-02-2017. 3 | */ 4 | import { Component, OnInit } from '@angular/core'; 5 | import { Router } from '@angular/router'; 6 | 7 | import { Campground } from '../../models/campground'; 8 | import { CampgroundService } from '../../services/campgounds.service'; 9 | import { UserService } from '../../services/user.service'; 10 | 11 | @Component({ 12 | selector: 'camps', 13 | templateUrl: './app/components/campgrounds/campgrounds.component.html', 14 | styleUrls: [ './app/components/campgrounds/campgrounds.component.css' ] 15 | }) 16 | 17 | export class CampgroundsComponent implements OnInit { 18 | camps: Campground[]; 19 | userdata: any; 20 | 21 | constructor(private router: Router, private campService: CampgroundService, private userService: UserService) { 22 | } 23 | 24 | ngOnInit() { 25 | this.getCampgrounds(); 26 | this.userdata = this.userService.getUserData(); 27 | } 28 | 29 | getCampgrounds() { 30 | this.campService.getCampgrounds().subscribe(camps => this.camps = camps); 31 | } 32 | 33 | moreInfo(campId: number) { 34 | this.router.navigateByUrl('/campground/detail/' + campId); 35 | } 36 | 37 | addNewCampground() { 38 | this.router.navigateByUrl('/campground/new'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /client/app/components/campgrounds/comment.form.component.html: -------------------------------------------------------------------------------- 1 |

Leave your comment:

2 |
3 |
4 |
5 | 7 |
8 |
9 | 12 |
13 |
14 |
15 | -------------------------------------------------------------------------------- /client/app/components/campgrounds/comment.form.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; 2 | import { NgForm } from '@angular/forms'; 3 | import { ActivatedRoute, Router } from '@angular/router'; 4 | import { Comment } from '../../models/comment'; 5 | import { CampgroundService } from '../../services/campgounds.service'; 6 | import { UserService } from '../../services/user.service'; 7 | 8 | /** 9 | * Created by laurence-ho on 3/07/17. 10 | */ 11 | 12 | @Component({ 13 | selector: 'app-comment', 14 | templateUrl: './app/components/campgrounds/comment.form.component.html', 15 | styleUrls: [ './app/components/campgrounds/campgrounds.component.css' ] 16 | }) 17 | 18 | export class CommentFormComponent implements OnInit { 19 | @ViewChild('f') commentForm: NgForm; 20 | @Input() comment: Comment; 21 | @Output() insertedComment = new EventEmitter(); 22 | 23 | userdata: any; 24 | campground_id: number; 25 | 26 | constructor(private campgroundService: CampgroundService, 27 | private userService: UserService, 28 | private route: ActivatedRoute, 29 | private router: Router) { 30 | } 31 | 32 | ngOnInit() { 33 | this.userdata = this.userService.getUserData(); 34 | 35 | if (!this.userdata) { 36 | this.router.navigate([ '/login' ]); 37 | } 38 | 39 | if (!isNaN(Number(this.route.snapshot.url[ 2 ].path))) { 40 | this.campground_id = Number(this.route.snapshot.url[ 2 ].path); 41 | } 42 | 43 | if (!this.comment) { 44 | this.comment = new Comment(); 45 | } 46 | } 47 | 48 | doSubmit() { 49 | this.comment.user_id = this.userdata.id; 50 | this.comment.username = this.userdata.username; 51 | this.comment.campground_id = this.campground_id; 52 | 53 | if (this.comment.text) { 54 | if (!this.comment.id) { 55 | this.campgroundService.createComment(this.comment) 56 | // FIXME 57 | // .then(data => { 58 | // this.campgroundService.getComment(data.comment_id) 59 | // .subscribe(comment => this.insertedComment.emit(comment)); 60 | // }) 61 | .subscribe(data => console.log(data)); 62 | } else { 63 | this.campgroundService.editComment(this.comment) 64 | .subscribe(data => console.log(data)); // FIXME 65 | } 66 | } 67 | this.commentForm.reset(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /client/app/components/user/profile.component.html: -------------------------------------------------------------------------------- 1 |
2 | 10 |
11 |
12 |
13 |

Local

14 |

15 | id: 16 | {{id}} 17 |
18 | username: 19 | {{username}} 20 |
21 | password: 22 | {{password}} 23 |

24 |
25 |
26 |
27 |
28 | -------------------------------------------------------------------------------- /client/app/components/user/profile.component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by laurence-ho on 29/06/17. 3 | */ 4 | 5 | import { Component, OnInit } from '@angular/core'; 6 | import { Router } from '@angular/router'; 7 | import { UserService } from '../../services/user.service'; 8 | 9 | @Component({ 10 | selector: 'login', 11 | templateUrl: './app/components/user/profile.component.html' 12 | }) 13 | 14 | export class ProfileComponent implements OnInit { 15 | id: number = 0; 16 | username: string = ''; 17 | password: string = ''; 18 | 19 | constructor(private userService: UserService, private router: Router) { 20 | } 21 | 22 | ngOnInit() { 23 | this.userService.getProfile() 24 | .subscribe(data => { 25 | this.id = data.id; 26 | this.username = data.username; 27 | this.password = data.password; 28 | }); 29 | } 30 | 31 | doLogout() { 32 | this.router.navigateByUrl('/logout'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /client/app/components/user/user.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Login

5 |

6 | Sign Up 7 |

8 | 9 | 12 | 13 | 16 | 17 |
18 |
19 | 20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 | 28 | 29 |
30 | 31 | 35 | 40 |
41 | 42 |
43 | 44 |

Need an account? Sign Up

45 |

Already have an account? Login 46 |

47 |

Or go home

48 |
49 |
50 |
51 | -------------------------------------------------------------------------------- /client/app/components/user/user.component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by laurence-ho on 7/06/17. 3 | */ 4 | 5 | import { Component, OnInit } from '@angular/core'; 6 | import { ActivatedRoute, Router } from '@angular/router'; 7 | import { UserService } from '../../services/user.service'; 8 | 9 | @Component({ 10 | selector: 'login', 11 | templateUrl: './app/components/user/user.component.html' 12 | }) 13 | 14 | export class UserComponent implements OnInit { 15 | username: string = ''; 16 | password: string = ''; 17 | remember: boolean = false; 18 | 19 | error: boolean = false; 20 | signupSuccessful: boolean = false; 21 | 22 | message: string = ''; 23 | 24 | constructor(private route: ActivatedRoute, private router: Router, private userService: UserService) { 25 | 26 | } 27 | 28 | ngOnInit() { 29 | if (this.route.snapshot.url[ 0 ].path === 'login') { 30 | if (this.userService.getUserData()) { 31 | this.router.navigate([ '/login' ]); 32 | } 33 | } 34 | 35 | if (this.route.snapshot.url[ 0 ].path === 'logout') { 36 | this.doLogout(); 37 | } 38 | } 39 | 40 | doSomething() { 41 | if (this.route.snapshot.url[ 0 ].path === 'login') { 42 | this.doLogin(); 43 | } else { 44 | this.doSignup(); 45 | } 46 | } 47 | 48 | doLogin() { 49 | this.userService.doLogin(this.username, this.password, this.remember) 50 | .subscribe(data => { 51 | this.userService.setUserData(data); 52 | this.router.navigate([ '/campground' ]); 53 | }); 54 | } 55 | 56 | doSignup() { 57 | this.userService.doSignup(this.username, this.password) 58 | .subscribe(data => { 59 | this.signupSuccessful = true; 60 | this.router.navigate([ '/profile' ]); 61 | }); 62 | } 63 | 64 | doLogout() { 65 | this.userService.doLogout(); 66 | this.router.navigate([ '/campground' ]); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /client/app/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Laurence Ho on 07-02-2017. 3 | */ 4 | 5 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 6 | import { BaseModule } from './base.module'; 7 | 8 | platformBrowserDynamic().bootstrapModule(BaseModule).catch(err => console.log(err)); 9 | -------------------------------------------------------------------------------- /client/app/models/campground.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Laurence Ho on 07-02-2017. 3 | */ 4 | 5 | export class Campground { 6 | id: number; 7 | name: string; 8 | image: string; 9 | description: string; 10 | price: number; 11 | username: string; 12 | user_id: number; 13 | } 14 | -------------------------------------------------------------------------------- /client/app/models/campgroundDetail.ts: -------------------------------------------------------------------------------- 1 | import { Campground } from './campground'; 2 | 3 | export class CampgroundDetail { 4 | campground: Campground; 5 | comments: any[]; 6 | } 7 | -------------------------------------------------------------------------------- /client/app/models/comment.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Laurence Ho on 08-02-2017. 3 | */ 4 | 5 | export class Comment { 6 | id: number; 7 | text: string; 8 | campground_id: number; 9 | username: string; 10 | user_id: number; 11 | } 12 | -------------------------------------------------------------------------------- /client/app/services/api.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Headers, Http, RequestOptionsArgs, Response, URLSearchParams } from '@angular/http'; 3 | import { Router } from '@angular/router'; 4 | import * as _ from 'lodash'; 5 | import 'rxjs/add/observable/throw'; 6 | import { Observable } from 'rxjs/Observable'; 7 | import 'rxjs/Rx'; 8 | 9 | @Injectable() 10 | export class ApiService { 11 | 12 | constructor(private http: Http, private router: Router) { 13 | } 14 | 15 | perform(method: string, urlPath: string, body: any, params: any, formParams: any): Observable { 16 | const requestOptions: RequestOptionsArgs = {}; 17 | const headers = new Headers({Accept: '*/*'}); 18 | 19 | if (!_.isEmpty(formParams)) { 20 | // Form submit 21 | headers.append('Content-Type', 'application/x-www-form-urlencoded'); 22 | 23 | const formBody = new URLSearchParams(); 24 | 25 | for (const formParam of Object.keys(formParams)) { 26 | formBody.append(formParam, formParams[ formParam ]); 27 | } 28 | 29 | requestOptions.body = formBody.toString(); 30 | } else { 31 | // JSON content 32 | headers.append('Content-Type', 'application/json'); 33 | 34 | if (body) { 35 | requestOptions.body = JSON.stringify(body); 36 | } 37 | } 38 | 39 | const searchParams = new URLSearchParams(); 40 | 41 | if (!_.isEmpty(params)) { 42 | for (const param of Object.keys(params)) { 43 | searchParams.append(param, params[ param ]); 44 | } 45 | } 46 | 47 | requestOptions.search = searchParams; 48 | requestOptions.method = method.toLowerCase(); 49 | requestOptions.headers = headers; 50 | 51 | const methodName = method.toLowerCase(); 52 | 53 | if (!this.http[ methodName ]) { 54 | throw new Error(`Unknown HTTP method: ${method}`); 55 | } 56 | 57 | return this.http.request(urlPath, requestOptions) 58 | .catch((error) => { 59 | // FIXME, need to handle error 60 | return Observable.throw(error); 61 | }) 62 | .map(this.getJson); 63 | } 64 | 65 | private getJson(response: Response) { 66 | let formattedResponse; 67 | 68 | try { 69 | formattedResponse = response.json(); 70 | } catch (error) { 71 | formattedResponse = response.text(); 72 | } 73 | return formattedResponse; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /client/app/services/campgounds.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Laurence Ho on 07-02-2017. 3 | */ 4 | 5 | import { Injectable } from '@angular/core'; 6 | import { Observable } from 'rxjs/Observable'; 7 | import 'rxjs/Rx'; 8 | import { Campground } from '../models/campground'; 9 | import { CampgroundDetail } from '../models/campgroundDetail'; 10 | import { Comment } from '../models/comment'; 11 | import { ApiService } from './api.service'; 12 | 13 | @Injectable() 14 | export class CampgroundService { 15 | 16 | private campgroundsUrl = 'api/campground/'; // URL to web api 17 | private commentUrl = 'api/comment/'; 18 | 19 | constructor(private apiService: ApiService) { 20 | } 21 | 22 | getCampgrounds(): Observable { 23 | const _params: any = {}; 24 | const _formParams: any = {}; 25 | const _bodyData: any = {}; 26 | 27 | return this.apiService.perform('get', this.campgroundsUrl, _bodyData, _params, _formParams); 28 | } 29 | 30 | getCampgroundDetail(id: number): Observable { 31 | const _params: any = {}; 32 | const _formParams: any = {}; 33 | const _bodyData: any = {}; 34 | 35 | return this.apiService.perform('get', this.campgroundsUrl + id, _bodyData, _params, _formParams); 36 | } 37 | 38 | getCampground(id: number): Observable { 39 | const _params: any = {}; 40 | const _formParams: any = {}; 41 | const _bodyData: any = {}; 42 | const url = this.campgroundsUrl + id + '/edit'; 43 | 44 | return this.apiService.perform('get', url, _bodyData, _params, _formParams); 45 | } 46 | 47 | editCampground(campground: Campground): Observable { 48 | const _params: any = {}; 49 | const _formParams: any = {}; 50 | const url = this.campgroundsUrl + campground.id + '/edit'; 51 | 52 | return this.apiService.perform('put', url, campground, _params, _formParams); 53 | } 54 | 55 | createCampground(campground: Campground): Observable { 56 | const _params: any = {}; 57 | const _formParams: any = {}; 58 | 59 | return this.apiService.perform('post', this.campgroundsUrl, campground, _params, _formParams); 60 | } 61 | 62 | deleteCampground(id: number): Observable { 63 | const _params: any = {}; 64 | const _formParams: any = {}; 65 | const _bodyData: any = {}; 66 | 67 | return this.apiService.perform('delete', this.campgroundsUrl + id, _bodyData, _params, _formParams); 68 | } 69 | 70 | getComment(id: number): Observable { 71 | const _params: any = {}; 72 | const _formParams: any = {}; 73 | const _bodyData: any = {}; 74 | const url = this.commentUrl + id + '/edit'; 75 | 76 | return this.apiService.perform('get', url, _bodyData, _params, _formParams); 77 | } 78 | 79 | editComment(comment: Comment): Observable { 80 | const _params: any = {}; 81 | const _formParams: any = {}; 82 | const url = this.commentUrl + comment.id + '/edit'; 83 | 84 | return this.apiService.perform('put', url, comment, _params, _formParams); 85 | } 86 | 87 | createComment(comment: Comment): Observable { 88 | const _params: any = {}; 89 | const _formParams: any = {}; 90 | 91 | return this.apiService.perform('post', this.commentUrl, comment, _params, _formParams); 92 | } 93 | 94 | deleteComment(id: number): Observable { 95 | const _params: any = {}; 96 | const _formParams: any = {}; 97 | const _bodyData: any = {}; 98 | 99 | return this.apiService.perform('delete', this.commentUrl + id, _bodyData, _params, _formParams); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /client/app/services/user.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by laurence-ho on 7/06/17. 3 | */ 4 | 5 | import { Injectable } from '@angular/core'; 6 | import { Observable } from 'rxjs/Observable'; 7 | import 'rxjs/Rx'; 8 | import { ApiService } from './api.service'; 9 | 10 | export interface InternalStateType { 11 | [ key: string ]: any; 12 | } 13 | 14 | @Injectable() 15 | export class UserService { 16 | _state: InternalStateType = {}; 17 | USER_DATA_KEY: string = 'user_data'; 18 | 19 | private loginUrl = 'api/login'; 20 | private signupUrl = 'api/signup'; 21 | private logoutUrl = 'api/logout'; 22 | private profileUrl = 'api/profile'; 23 | 24 | constructor(private apiService: ApiService) { 25 | const userdata = window.localStorage.getItem(this.USER_DATA_KEY); 26 | if (userdata) { 27 | try { 28 | this.setUserData(JSON.parse(userdata)); 29 | } catch (error) { 30 | console.error('error parsing user data json'); 31 | } 32 | } 33 | } 34 | 35 | get state() { 36 | return this._state = this._clone(this._state); 37 | } 38 | 39 | set state(value) { 40 | throw new Error('do not mutate the `.state` directly'); 41 | } 42 | 43 | flush() { 44 | this._state = {}; 45 | window.localStorage.removeItem(this.USER_DATA_KEY); 46 | } 47 | 48 | private _clone(object: InternalStateType) { 49 | return JSON.parse(JSON.stringify(object)); 50 | } 51 | 52 | setUserData(data: any) { 53 | if (data) { 54 | window.localStorage.setItem(this.USER_DATA_KEY, data); 55 | return this._state.user = data; 56 | } 57 | } 58 | 59 | getUserData(): any { 60 | const state = this.state; 61 | return state.hasOwnProperty('user') ? state.user : undefined; 62 | } 63 | 64 | doLogin(username: string, password: string, remember: boolean): Observable { 65 | const _params: any = {}; 66 | const _bodyData: any = {}; 67 | const _formParams: any = {}; 68 | 69 | if (username !== undefined) { 70 | _formParams.username = username; 71 | } 72 | 73 | if (password !== undefined) { 74 | _formParams.password = password; 75 | } 76 | 77 | if (remember !== undefined && remember === false) { 78 | _formParams.remember = remember; 79 | } 80 | 81 | return this.apiService.perform('post', this.loginUrl, _bodyData, _params, _formParams); 82 | } 83 | 84 | doSignup(username: string, password: string): Observable { 85 | const _params: any = {}; 86 | const _bodyData: any = {}; 87 | const _formParams: any = {}; 88 | 89 | if (username !== undefined) { 90 | _formParams.username = username; 91 | } 92 | 93 | if (password !== undefined) { 94 | _formParams.password = password; 95 | } 96 | 97 | return this.apiService.perform('post', this.signupUrl, _bodyData, _params, _formParams); 98 | } 99 | 100 | doLogout() { 101 | this.flush(); 102 | 103 | const _params: any = {}; 104 | const _formParams: any = {}; 105 | const _bodyData: any = {}; 106 | 107 | return this.apiService.perform('get', this.logoutUrl, _bodyData, _params, _formParams); 108 | } 109 | 110 | getProfile() { 111 | const _params: any = {}; 112 | const _formParams: any = {}; 113 | const _bodyData: any = {}; 114 | 115 | return this.apiService.perform('get', this.profileUrl, _bodyData, _params, _formParams); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | YelpCamp 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 | Loading... 39 |
40 |
41 |
42 |

© 2018 YelpCamp

43 |
44 | 45 | 46 | -------------------------------------------------------------------------------- /client/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by laurence-ho on 29/06/17. 3 | */ 4 | /** 5 | * This file includes polyfills needed by Angular and is loaded before the app. 6 | * You can add your own extra polyfills to this file. 7 | * 8 | * This file is divided into 2 sections: 9 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 10 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 11 | * file. 12 | * 13 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 14 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 15 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 16 | * 17 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 18 | */ 19 | 20 | /*************************************************************************************************** 21 | * BROWSER POLYFILLS 22 | */ 23 | 24 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 25 | // import 'core-js/es6/symbol'; 26 | // import 'core-js/es6/object'; 27 | // import 'core-js/es6/function'; 28 | // import 'core-js/es6/parse-int'; 29 | // import 'core-js/es6/parse-float'; 30 | // import 'core-js/es6/number'; 31 | // import 'core-js/es6/math'; 32 | // import 'core-js/es6/string'; 33 | // import 'core-js/es6/date'; 34 | // import 'core-js/es6/array'; 35 | // import 'core-js/es6/regexp'; 36 | // import 'core-js/es6/map'; 37 | // import 'core-js/es6/set'; 38 | 39 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 40 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 41 | 42 | /** IE10 and IE11 requires the following to support `@angular/animation`. */ 43 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 44 | 45 | 46 | /** Evergreen browsers require these. **/ 47 | import 'core-js/es6/reflect'; 48 | import 'core-js/es7/reflect'; 49 | 50 | 51 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/ 52 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 53 | 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | 65 | /** 66 | * Date, currency, decimal and percent pipes. 67 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 68 | */ 69 | // import 'intl'; // Run `npm install --save intl`. 70 | -------------------------------------------------------------------------------- /client/systemjs.config.js: -------------------------------------------------------------------------------- 1 | var isPublic = typeof window != "undefined"; 2 | 3 | (function (global) { 4 | // map tells the System loader where to look for things 5 | var map = { 6 | 'app': 'app', // 'dist', 7 | '@angular': (isPublic) ? '@angular' : 'node_modules/@angular', 8 | 'lodash': 'http://localhost:8080/libs/lodash/lodash.min.js', 9 | 'primeng': 'http://localhost:8080/libs/primeng/', 10 | 'rxjs': (isPublic) ? 'rxjs' : 'node_modules/rxjs' 11 | }; 12 | // packages tells the System loader how to load when no filename and/or no extension 13 | var packages = { 14 | 'app': {main: 'main.js', defaultExtension: 'js'}, 15 | 'rxjs': {defaultExtension: 'js'}, 16 | 'lodash': {defaultExtension: 'js'}, 17 | 'primeng': {defaultExtension: 'js'} 18 | }; 19 | var ngPackageNames = [ 20 | 'animations', 21 | 'common', 22 | 'compiler', 23 | 'core', 24 | 'forms', 25 | 'http', 26 | 'platform-browser', 27 | 'platform-browser-dynamic', 28 | 'platform-server', 29 | 'router', 30 | 'upgrade' 31 | ]; 32 | 33 | // Individual files (~300 requests): 34 | function packIndex(pkgName) { 35 | packages[ '@angular/' + pkgName ] = {main: 'index.js', defaultExtension: 'js'}; 36 | } 37 | 38 | // Bundled (~40 requests): 39 | function packUmd(pkgName) { 40 | packages[ '@angular/' + pkgName ] = {main: 'bundles/' + pkgName + '.umd.js', defaultExtension: 'js'}; 41 | } 42 | 43 | // Most environments should use UMD; some (Karma) need the individual index files 44 | var setPackageConfig = System.packageWithIndex ? packIndex : packUmd; 45 | // Add package entries for angular packages 46 | ngPackageNames.forEach(setPackageConfig); 47 | var config = { 48 | map: map, 49 | packages: packages 50 | }; 51 | System.config(config); 52 | })(this); 53 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by laurence-ho on 29/06/17. 3 | */ 4 | 5 | require('ts-node').register({project: false, disableWarnings: true}); 6 | require('./gulpfile.ts'); -------------------------------------------------------------------------------- /gulpfile.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Laurence Ho on 07-02-2017. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | const gulp = require('gulp'); 8 | const browserSync = require('browser-sync').create(); 9 | const sass = require('gulp-sass'); 10 | const del = require('del'); 11 | const tsc = require('gulp-typescript'); 12 | const sourcemaps = require('gulp-sourcemaps'); 13 | const tsProject = tsc.createProject('tsconfig.json'); 14 | const tslint = require('gulp-tslint'); 15 | const nodemon = require('gulp-nodemon'); 16 | 17 | /** 18 | * Remove build directory. 19 | */ 20 | gulp.task('clean', (cb: any) => del([ 'dist' ], cb)); 21 | 22 | /** 23 | * Lint all custom TypeScript files. 24 | */ 25 | gulp.task('tslint', () => 26 | gulp.src('client/app/**/*.ts') 27 | .pipe(tslint({ 28 | formatter: 'prose' 29 | })) 30 | .pipe(tslint.report()) 31 | ); 32 | 33 | /** 34 | * Build Express server 35 | */ 36 | gulp.task('build:server', () => { 37 | const tsResult = gulp.src('server/src/**/*.ts') 38 | .pipe(sourcemaps.init()) 39 | .pipe(tsProject()); 40 | return tsResult.js 41 | .pipe(sourcemaps.write()) 42 | .pipe(gulp.dest('dist/server')); 43 | }); 44 | 45 | /* 46 | * Build frontend app 47 | */ 48 | gulp.task('build:client', () => { 49 | const tsResult = gulp.src('client/**/*.ts') 50 | .pipe(sourcemaps.init({loadMaps: true})) 51 | .pipe(tsProject()); 52 | return tsResult.js 53 | .pipe(sourcemaps.write()) 54 | .pipe(gulp.dest('dist/client')) 55 | .pipe(browserSync.stream()); 56 | }); 57 | 58 | /** 59 | * Copy all resources that are not TypeScript files into build directory. e.g. index.html, css, images 60 | */ 61 | gulp.task('clientResources', () => 62 | gulp.src([ 'client/**/*', '!**/*.ts', '!client/*.json' ]) 63 | .pipe(gulp.dest('dist/client')) 64 | .pipe(browserSync.stream()) 65 | ); 66 | 67 | /** 68 | * Copy bin directory for www 69 | */ 70 | gulp.task('serverResources', () => 71 | gulp.src([ 'server/src/bin/**' ]) 72 | .pipe(gulp.dest('dist/server/bin')) 73 | ); 74 | 75 | /** 76 | * Copy all required libraries into build directory. 77 | */ 78 | gulp.task('libs', () => 79 | gulp.src([ 80 | 'core-js/client/**', 81 | 'zone.js/dist/zone.js', 82 | 'reflect-metadata/Reflect.js', 83 | 'reflect-metadata/Reflect.js.map', 84 | 'systemjs/dist/system.src.js', 85 | 'lodash/lodash.min.js', 86 | 'primeng/**' 87 | ], {cwd: 'node_modules/**'}) /* Glob required here. */ 88 | .pipe(gulp.dest('dist/client/libs')) 89 | ); 90 | 91 | /** 92 | * Copy all required libraries into build directory. 93 | */ 94 | gulp.task('font-awesome', () => 95 | gulp.src([ 96 | 'font-awesome/**/**' 97 | ], {cwd: 'node_modules/**'}) /* Glob required here. */ 98 | .pipe(gulp.dest('dist/client/vendors')) 99 | ); 100 | 101 | /** 102 | * Copy and compile bootstrap 103 | */ 104 | gulp.task('sass', () => 105 | gulp.src([ 106 | 'bootstrap/scss/bootstrap.scss' 107 | ], {cwd: 'node_modules/**'}) 108 | .pipe(sass()) 109 | .pipe(gulp.dest('dist/client/vendors')) 110 | ); 111 | 112 | gulp.task('bootstrap', () => 113 | gulp.src([ 114 | 'bootstrap/dist/js/bootstrap.min.js', 115 | 'jquery/dist/jquery.slim.js', 116 | 'popper.js/dist/popper.min.js' 117 | ], {cwd: 'node_modules/**'}) 118 | .pipe(gulp.dest('dist/client/vendors')) 119 | ); 120 | 121 | /** 122 | * Start the express server 123 | */ 124 | gulp.task('start', () => { 125 | nodemon({ 126 | script: 'dist/server/bin/www', 127 | ext: 'html js', 128 | ignore: [ 'ignored.js' ], 129 | tasks: [ 'tslint' ] 130 | }) 131 | .on('restart', () => { 132 | console.log('restarted!'); 133 | }); 134 | }); 135 | 136 | /** 137 | * Build the project. 138 | * 1. Clean the build directory 139 | * 2. Build Express server 140 | * 3. Build the Angular app 141 | * 4. Copy the resources 142 | * 5. Copy the dependencies. 143 | */ 144 | 145 | gulp.task( 146 | 'build', 147 | gulp.series( 148 | 'clean', 149 | 'build:server', 150 | 'build:client', 151 | 'clientResources', 152 | 'serverResources', 153 | 'libs', 154 | 'font-awesome', 155 | 'sass', 156 | 'bootstrap' 157 | ) 158 | ); 159 | 160 | /** 161 | * Compile TypeScript sources and create sourcemaps in build directory. 162 | */ 163 | gulp.task('compile', gulp.series('tslint'), () => { 164 | const tsResult = gulp.src('client/**/*.ts') 165 | .pipe(sourcemaps.init({loadMaps: true})) 166 | .pipe(tsProject()); 167 | return tsResult.js 168 | .pipe(sourcemaps.write('.')) 169 | .pipe(gulp.dest('dist/client')) 170 | .pipe(browserSync.stream()); 171 | }); 172 | 173 | /** 174 | * Watch for changes in TypeScript, HTML and CSS files. 175 | */ 176 | gulp.task('watch', () => { 177 | gulp.watch([ 'client/**/*.ts' ], gulp.series('compile')).on('change', browserSync.reload); 178 | gulp.watch([ 'client/**/*.html', 'client/**/*.css' ], 179 | gulp.series('clientResources')).on('change', browserSync.reload); 180 | }); 181 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular4-express-mysql", 3 | "version": "1.0.0", 4 | "author": "Laurence Ho", 5 | "description": "Fullstack web application using Angular4, Primeng, Express, Mysql, Gulp and Passport", 6 | "scripts": { 7 | "clean": "gulp clean", 8 | "build": "gulp build", 9 | "start-all": "concurrently --kill-others \"gulp watch\" \"gulp start\"" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://LaurenceHo@bitbucket.org/LaurenceHo/angular4-express-mysql.git" 14 | }, 15 | "keywords": [ 16 | "Angular4", 17 | "Express", 18 | "Gulp", 19 | "Typescript", 20 | "Mysql" 21 | ], 22 | "license": "MIT", 23 | "dependencies": { 24 | "@angular/animations": "^4.4.7", 25 | "@angular/common": "^4.4.7", 26 | "@angular/compiler": "^4.4.7", 27 | "@angular/core": "^4.4.7", 28 | "@angular/forms": "^4.4.7", 29 | "@angular/http": "^4.4.7", 30 | "@angular/platform-browser": "^4.4.7", 31 | "@angular/platform-browser-dynamic": "^4.4.7", 32 | "@angular/platform-server": "^4.4.7", 33 | "@angular/router": "^4.4.7", 34 | "@angular/upgrade": "^4.4.7", 35 | "bcrypt-nodejs": "0.0.3", 36 | "body-parser": "^1.18.3", 37 | "bootstrap": "^4.2.1", 38 | "express": "^4.16.4", 39 | "express-sanitizer": "^1.0.4", 40 | "express-session": "^1.15.6", 41 | "font-awesome": "^4.7.0", 42 | "jquery": "^3.3.1", 43 | "lodash": "^4.17.11", 44 | "mysql": "^2.16.0", 45 | "passport": "^0.4.0", 46 | "passport-local": "^1.0.0", 47 | "popper.js": "^1.14.6", 48 | "primeng": "^4.3.0", 49 | "request": "^2.88.0", 50 | "rxjs": "^5.5.12" 51 | }, 52 | "devDependencies": { 53 | "@types/bcrypt-nodejs": "0.0.30", 54 | "@types/body-parser": "^1.17.0", 55 | "@types/express": "^4.16.0", 56 | "@types/express-session": "^1.15.11", 57 | "@types/lodash": "^4.14.119", 58 | "@types/mysql": "0.0.34", 59 | "@types/node": "^10.12.18", 60 | "@types/passport": "^0.3.5", 61 | "@types/passport-local": "^1.0.33", 62 | "browser-sync": "^2.26.3", 63 | "concurrently": "^3.6.1", 64 | "core-js": "^2.6.2", 65 | "del": "^3.0.0", 66 | "gulp": "^4.0.0", 67 | "gulp-nodemon": "^2.4.2", 68 | "gulp-sass": "^4.0.2", 69 | "gulp-sourcemaps": "^2.6.4", 70 | "gulp-tslint": "^8.1.3", 71 | "gulp-typescript": "^3.2.4", 72 | "reflect-metadata": "^0.1.13", 73 | "systemjs": "^0.21.6", 74 | "ts-node": "^7.0.1", 75 | "tslint": "^5.12.1", 76 | "typescript": "^2.9.2", 77 | "zone.js": "^0.8.28" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /server/src/Authentication.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by laurence-ho on 21/07/17. 3 | */ 4 | 5 | import CampgroundRepository from './repository/CampgroundRepository'; 6 | import CommentRepository from './repository/CommentRepository'; 7 | 8 | const campgroundRepository = new CampgroundRepository(); 9 | const commentRepository = new CommentRepository(); 10 | 11 | const authentication: any = {}; 12 | 13 | authentication.checkCampOwner = (req: any, res: any, next: any) => { 14 | if (req.isAuthenticated()) { 15 | try { 16 | campgroundRepository.findOneById(req.params.id, (callback: any) => { 17 | if (callback.campground.user_id !== req.user.id) { 18 | res.status(403).send({message: 'You have no permission'}); 19 | } else { 20 | next(); 21 | } 22 | }); 23 | } catch (err) { 24 | res.status(500).send({message: err}); 25 | } 26 | } else { 27 | res.status(403).send({message: 'Please Login First'}); 28 | } 29 | }; 30 | 31 | authentication.checkCommentOwner = (req: any, res: any, next: any) => { 32 | if (req.isAuthenticated()) { 33 | try { 34 | commentRepository.findOneById(req.params.comment_id, (callback: any) => { 35 | if (callback.comment.user_id !== req.user.id) { 36 | res.status(403).send({message: 'You have no permission'}); 37 | } else { 38 | next(); 39 | } 40 | }); 41 | } catch (err) { 42 | res.status(500).send({message: err}); 43 | } 44 | } else { 45 | res.status(403).send({message: 'Please Login First'}); 46 | } 47 | }; 48 | 49 | authentication.isLoggedIn = (req: any, res: any, next: any) => { 50 | if (req.isAuthenticated()) { 51 | return next(); 52 | } 53 | res.status(403).send({message: 'Please Login First'}); 54 | }; 55 | 56 | export = authentication; 57 | -------------------------------------------------------------------------------- /server/src/Passport.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by laurence-ho on 21/07/17. 3 | */ 4 | 5 | import * as bcrypt from 'bcrypt-nodejs'; 6 | import { Passport } from 'passport'; 7 | import { Strategy } from 'passport-local'; 8 | import UserRepository from './repository/UserRepository'; 9 | 10 | const userRepository = new UserRepository(); 11 | 12 | export = (passport: Passport) => { 13 | // ========================================================================= 14 | // passport session setup ================================================== 15 | // ========================================================================= 16 | // required for persistent login sessions 17 | // passport needs ability to serialize and unserialize users out of session 18 | 19 | // used to serialize the user for the session 20 | passport.serializeUser((user: any, done: any) => { 21 | done(null, user.id); 22 | }); 23 | 24 | // used to deserialize the user 25 | passport.deserializeUser((id: any, done: any) => { 26 | try { 27 | userRepository.findOneById(id, (result: any) => { 28 | done(null, result); 29 | }); 30 | } catch (err) { 31 | return done(err); 32 | } 33 | }); 34 | 35 | // ========================================================================= 36 | // LOCAL SIGNUP ============================================================ 37 | // ========================================================================= 38 | // we are using named strategies since we have one for login and one for signup 39 | // by default, if there was no name, it would just be called 'local' 40 | 41 | passport.use('local-signup', 42 | new Strategy({ 43 | usernameField: 'username', 44 | passwordField: 'password', 45 | passReqToCallback: true // allows us to pass back the entire request to the callback 46 | }, 47 | (req: any, username: string, password: string, done: any) => { 48 | try { 49 | userRepository.findOneByUsername(username, (result: any) => { 50 | if (result) { 51 | return done(false, {message: 'This username is already taken.'}); 52 | } else { 53 | // if there is no user with that username, create the user 54 | const newUser = { 55 | id: 0, 56 | username, 57 | password: bcrypt.hashSync(password, null) 58 | }; 59 | userRepository.createOne(newUser, (result: any) => { 60 | newUser.id = result.user_id; 61 | done(null, newUser); 62 | }); 63 | return done; 64 | } 65 | }); 66 | } catch (err) { 67 | return done(err); 68 | } 69 | }) 70 | ); 71 | 72 | // ========================================================================= 73 | // LOCAL LOGIN ============================================================= 74 | // ========================================================================= 75 | // we are using named strategies since we have one for login and one for signup 76 | // by default, if there was no name, it would just be called 'local' 77 | 78 | passport.use('local-login', 79 | new Strategy({ 80 | // by default, local strategy uses username and password, we will override with email 81 | usernameField: 'username', 82 | passwordField: 'password', 83 | passReqToCallback: true // allows us to pass back the entire request to the callback 84 | }, 85 | (req: any, username: string, password: string, done: any) => { // callback with email and password from our form 86 | try { 87 | userRepository.findOneByUsername(username, (result: any) => { 88 | if (result) { 89 | // if the user is found but the password is wrong 90 | if (!bcrypt.compareSync(password, result.password)) { 91 | return done(null, false, {message: 'User name or password is wrong'}); 92 | } else { 93 | // all is well, return successful user 94 | return done(null, result); 95 | } 96 | } else { 97 | return done(null, false, {message: 'User name or password is wrong'}); 98 | } 99 | }); 100 | } catch (err) { 101 | return done(err); 102 | } 103 | }) 104 | ); 105 | }; 106 | -------------------------------------------------------------------------------- /server/src/Server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by laurence-ho on 21/07/17. 3 | */ 4 | import * as bodyParser from 'body-parser'; 5 | import * as express from 'express'; 6 | import * as session from 'express-session'; 7 | import * as passport from 'passport'; 8 | import * as path from 'path'; 9 | 10 | import * as campgroundRoutes from './routes/Campground'; 11 | import * as commentRoutes from './routes/Comment'; 12 | import * as userRoutes from './routes/User'; 13 | 14 | const expressSanitizer = require('express-sanitizer'); 15 | 16 | // initial database schema 17 | require('./database/DatabaseSchema').schema(); 18 | require('./Passport')(passport); 19 | 20 | const app = express(); 21 | 22 | app.use(bodyParser.urlencoded({extended: false})); 23 | app.use(bodyParser.json()); 24 | 25 | app.use('/app', express.static(path.resolve(__dirname, '../client/app'))); 26 | app.use('/libs', express.static(path.resolve(__dirname, '../client/libs'))); 27 | 28 | // for system.js to work. Can be removed if bundling. 29 | app.use(express.static(path.resolve(__dirname, '../client'))); 30 | app.use(express.static(path.resolve(__dirname, '../../node_modules'))); 31 | 32 | app.use(expressSanitizer()); 33 | 34 | app.use(session({ 35 | secret: 'mySecretKey', 36 | resave: true, 37 | saveUninitialized: true 38 | })); 39 | app.use(passport.initialize()); 40 | app.use(passport.session()); 41 | 42 | // initial routes 43 | app.use('/api', userRoutes); 44 | app.use('/api', campgroundRoutes); 45 | app.use('/api', commentRoutes); 46 | 47 | app.get('/', (req: any, res: any) => { 48 | res.sendFile(path.resolve(__dirname, '../client/index.html')); 49 | }); 50 | 51 | app.get('*', (req: any, res: any) => { 52 | res.send('Sorry, page not found!'); 53 | }); 54 | 55 | export = app; 56 | -------------------------------------------------------------------------------- /server/src/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const http = require('http'); 4 | const app = require('../Server'); 5 | 6 | const server = http.createServer(app); 7 | 8 | server.listen(8080, () => { 9 | const port = server.address().port; 10 | console.log('This express angular app is listening on port:' + port); 11 | }); 12 | -------------------------------------------------------------------------------- /server/src/database/DatabaseSchema.ts: -------------------------------------------------------------------------------- 1 | import DatabaseService from './DatabaseService'; 2 | 3 | const database = new DatabaseService(); 4 | 5 | export const schema = () => { 6 | let sql_user = 'CREATE TABLE IF NOT EXISTS `users` (' + 7 | '`id` int(11) NOT NULL AUTO_INCREMENT,' + 8 | '`username` varchar(255) NOT NULL,' + 9 | '`password` varchar(255) NOT NULL,' + 10 | 'PRIMARY KEY(`id`)' + 11 | ') ENGINE = InnoDB DEFAULT CHARSET = utf8'; 12 | 13 | let sql_camp = 'CREATE TABLE IF NOT EXISTS `campgrounds` (' + 14 | '`id` int(11) NOT NULL AUTO_INCREMENT,' + 15 | '`name` varchar(255) NOT NULL,' + 16 | '`image` varchar(255) NOT NULL,' + 17 | '`username` varchar(255) NOT NULL,' + 18 | '`price` int(11),' + 19 | '`user_id` int(11) NOT NULL,' + 20 | '`description` TEXT,' + 21 | 'PRIMARY KEY(`id`),' + 22 | 'CONSTRAINT `FK_USER_CAMP` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)' + 23 | ') ENGINE = InnoDB DEFAULT CHARSET = utf8'; 24 | 25 | let sql_comment = 'CREATE TABLE IF NOT EXISTS `comments` (' + 26 | '`id` int(11) NOT NULL AUTO_INCREMENT,' + 27 | '`username` varchar(255) NOT NULL,' + 28 | '`user_id` int(11) NOT NULL,' + 29 | '`text` TEXT NOT NULL,' + 30 | '`campground_id` int(11) NOT NULL,' + 31 | 'PRIMARY KEY(`id`),' + 32 | 'CONSTRAINT `FK_USER_COMMENT` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),' + 33 | 'CONSTRAINT `FK_CAMP_COMMENT` FOREIGN KEY (`campground_id`) REFERENCES `campgrounds` (`id`)' + 34 | ') ENGINE = InnoDB DEFAULT CHARSET = utf8'; 35 | 36 | database.query(sql_user, null); 37 | database.query(sql_camp, null); 38 | database.query(sql_comment, null); 39 | }; 40 | -------------------------------------------------------------------------------- /server/src/database/DatabaseService.ts: -------------------------------------------------------------------------------- 1 | import * as mysql from 'mysql'; 2 | 3 | export default class DatabaseService { 4 | pool: any = {}; 5 | 6 | constructor() { 7 | this.pool = mysql.createPool({ 8 | connectionLimit: 12, 9 | host: 'localhost', 10 | user: 'sa', 11 | password: '(IJN8uhb', 12 | database: 'yelpcamp', 13 | charset: 'utf8' 14 | }); 15 | } 16 | 17 | getConnection = (callback: any) => { 18 | this.pool.getConnection(callback); 19 | }; 20 | 21 | query = (sql: string, values: any) => new Promise((resolve, reject) => { 22 | this.pool.getConnection((err: any, connection: any) => { 23 | if (err) { 24 | reject(err); 25 | throw err; 26 | } else { 27 | connection.query(sql, values, (err: any, results: any) => { 28 | console.log('SQL: ', sql); 29 | console.log('Query values: ', values); 30 | connection.release(); 31 | 32 | if (err) { 33 | reject(err); 34 | throw err; 35 | } else { 36 | console.log('Query results:', JSON.stringify(results)); 37 | resolve(results); 38 | } 39 | }); 40 | } 41 | }); 42 | }); 43 | 44 | destroy = () => { 45 | this.pool.end((err: any) => console.error(err)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /server/src/model/Campground.ts: -------------------------------------------------------------------------------- 1 | export interface Campground { 2 | id: number; 3 | name: string; 4 | image: string; 5 | description: string; 6 | price: number; 7 | username: string; 8 | user_id: number; 9 | } 10 | -------------------------------------------------------------------------------- /server/src/model/Comment.ts: -------------------------------------------------------------------------------- 1 | export interface Comment { 2 | id: number; 3 | text: string; 4 | campground_id: number; 5 | username: string; 6 | user_id: number; 7 | } 8 | -------------------------------------------------------------------------------- /server/src/model/User.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: number; 3 | username: string; 4 | password: string; 5 | } 6 | -------------------------------------------------------------------------------- /server/src/repository/BaseRepository.ts: -------------------------------------------------------------------------------- 1 | export interface BaseRepository { 2 | 3 | findAll(callback: any): void; 4 | 5 | findOneById(id: number, callback: any): void; 6 | 7 | createOne(item: T, callback: any): void; 8 | 9 | updateOne(item: T): void; 10 | 11 | deleteOne(id: number): void; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /server/src/repository/CampgroundRepository.ts: -------------------------------------------------------------------------------- 1 | import DatabaseService from '../database/DatabaseService'; 2 | import { Campground } from '../model/Campground'; 3 | import { Comment } from '../model/Comment'; 4 | import { BaseRepository } from './BaseRepository'; 5 | 6 | const database = new DatabaseService(); 7 | 8 | export default class CampgroundRepository implements BaseRepository { 9 | findAll(callback: any): void { 10 | database.query('SELECT * FROM campgrounds', null).then( 11 | (results: Campground[]) => callback(results) 12 | ); 13 | } 14 | 15 | findOneById(id: number, callback: any): void { 16 | database.query('SELECT * FROM campgrounds WHERE id = ?', [ id ]).then((campResult: Campground) => { 17 | let campground: any = []; 18 | let comments: any = []; 19 | 20 | database.query('SELECT * FROM comments WHERE campground_id = ?', [ id ]).then((commentResult: Comment[]) => { 21 | campground = campResult[ 0 ]; 22 | comments = commentResult; 23 | 24 | callback({campground, comments}); 25 | }); 26 | }); 27 | } 28 | 29 | createOne(campground: Campground, callback: any): void { 30 | database.query('INSERT INTO campgrounds SET ?', campground).then( 31 | (result: any) => callback({campground_id: result.insertId}) 32 | ); 33 | } 34 | 35 | updateOne(campground: Campground): void { 36 | database.query('UPDATE campgrounds SET ? WHERE id = ?', [ campground, campground.id ]); 37 | } 38 | 39 | deleteOne(id: number): void { 40 | database.query('DELETE FROM campgrounds WHERE id = ?', [ id ]); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /server/src/repository/CommentRepository.ts: -------------------------------------------------------------------------------- 1 | import DatabaseService from '../database/DatabaseService'; 2 | import { Comment } from '../model/Comment'; 3 | import { BaseRepository } from './BaseRepository'; 4 | 5 | const database = new DatabaseService(); 6 | 7 | export default class CommentRepository implements BaseRepository { 8 | findAll(callback: any): void { 9 | // TODO 10 | } 11 | 12 | findOneById(id: number, callback: any): void { 13 | database.query('SELECT * FROM comments WHERE id = ?', [ id ]).then( 14 | (result: any) => callback({comment: result[ 0 ]}) 15 | ); 16 | } 17 | 18 | createOne(item: Comment, callback: any): void { 19 | database.query('INSERT INTO comments SET ?', item).then( 20 | (result: any) => callback({comment_id: result.insertId}) 21 | ); 22 | } 23 | 24 | updateOne(item: Comment): void { 25 | database.query('UPDATE comments SET ? WHERE id = ?', [ item, item.id ]); 26 | } 27 | 28 | deleteOne(id: number): void { 29 | database.query('DELETE FROM comments WHERE id = ?', [ id ]); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /server/src/repository/UserRepository.ts: -------------------------------------------------------------------------------- 1 | import DatabaseService from '../database/DatabaseService'; 2 | import { User } from '../model/User'; 3 | import { BaseRepository } from './BaseRepository'; 4 | 5 | const database = new DatabaseService(); 6 | 7 | export default class UserRepository implements BaseRepository { 8 | findAll(callback: any): void { 9 | // TODO 10 | } 11 | 12 | findOneById(id: number, callback: any): void { 13 | database.query('SELECT * FROM users WHERE id = ?', [ id ]).then( 14 | (result: any) => callback(result[ 0 ]) 15 | ); 16 | } 17 | 18 | findOneByUsername(username: string, callback: any): void { 19 | database.query('SELECT * FROM users WHERE username = ?', [ username ]).then( 20 | (result: any) => callback(result[ 0 ]) 21 | ); 22 | } 23 | 24 | createOne(item: User, callback: any): void { 25 | database.query('INSERT INTO users ( username, password ) values (?,?)', [ item.username, item.password ]).then( 26 | (result: any) => callback({user_id: result.insertId}) 27 | ); 28 | } 29 | 30 | updateOne(item: User): void { 31 | // TODO 32 | } 33 | 34 | deleteOne(id: number): void { 35 | // TODO 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /server/src/routes/Campground.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by laurence-ho on 21/07/17. 3 | */ 4 | 5 | import * as express from 'express'; 6 | import * as authentication from '../Authentication'; 7 | import CampgroundRepository from '../repository/CampgroundRepository'; 8 | 9 | const router = express.Router(); 10 | const campgroundRepository = new CampgroundRepository(); 11 | 12 | // get all campgrounds 13 | router.get('/campground', (req: any, res: any) => { 14 | try { 15 | campgroundRepository.findAll((callback: any) => { 16 | callback.message = 'OK'; 17 | res.status(200).send(callback); 18 | }); 19 | } catch (err) { 20 | res.status(500).send({message: err}); 21 | } 22 | }); 23 | 24 | // create one campground 25 | router.post('/campground', authentication.isLoggedIn, (req: any, res: any) => { 26 | req.body.name = req.sanitize(req.body.name); 27 | req.body.image = req.sanitize(req.body.image); 28 | req.body.description = req.sanitize(req.body.description); 29 | req.body.price = req.sanitize(req.body.price); 30 | 31 | try { 32 | campgroundRepository.createOne(req.body, (callback: any) => { 33 | callback.message = 'OK'; 34 | res.status(200).send(callback); 35 | }); 36 | } catch (err) { 37 | res.status(500).send({message: err}); 38 | } 39 | }); 40 | 41 | router.get('/campground/:id', (req: any, res: any) => { 42 | try { 43 | campgroundRepository.findOneById(req.params.id, (callback: any) => { 44 | callback.message = 'OK'; 45 | res.status(200).send(callback); 46 | }); 47 | } catch (err) { 48 | res.status(500).send({message: err}); 49 | } 50 | }); 51 | 52 | router.get('/campground/:id/edit', authentication.checkCampOwner, (req: any, res: any) => { 53 | try { 54 | campgroundRepository.findOneById(req.params.id, (callback: any) => { 55 | callback.message = 'OK'; 56 | res.status(200).send(callback); 57 | }); 58 | } catch (err) { 59 | res.status(500).send({message: err}); 60 | } 61 | }); 62 | 63 | // edit one campground 64 | router.put('/campground/:id/edit', authentication.checkCampOwner, (req: any, res: any) => { 65 | req.body.name = req.sanitize(req.body.name); 66 | req.body.image = req.sanitize(req.body.image); 67 | req.body.description = req.sanitize(req.body.description); 68 | req.body.price = req.sanitize(req.body.price); 69 | 70 | try { 71 | campgroundRepository.updateOne(req.body); 72 | res.status(200).send({message: 'OK'}); 73 | } catch (err) { 74 | res.status(500).send({message: err}); 75 | } 76 | }); 77 | 78 | // delete one campground 79 | router.delete('/campground/:id', authentication.checkCampOwner, (req: any, res: any) => { 80 | try { 81 | campgroundRepository.deleteOne(req.params.id); 82 | res.status(200).send({message: 'OK'}); 83 | } catch (err) { 84 | res.status(500).send({message: err}); 85 | } 86 | }); 87 | 88 | export = router; 89 | -------------------------------------------------------------------------------- /server/src/routes/Comment.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by laurence-ho on 22/07/17. 3 | */ 4 | 5 | import * as express from 'express'; 6 | import * as authentication from '../Authentication'; 7 | import CommentRepository from '../repository/CommentRepository'; 8 | 9 | const router = express.Router(); 10 | const commentRepository = new CommentRepository(); 11 | 12 | // get one comment for edit 13 | router.get('/comment/:comment_id/edit', authentication.checkCommentOwner, (req: any, res: any) => { 14 | try { 15 | commentRepository.findOneById(req.params.comment_id, (callback: any) => { 16 | callback.message = 'OK'; 17 | res.status(200).send(callback); 18 | }); 19 | } catch (err) { 20 | res.status(500).send({message: err}); 21 | } 22 | }); 23 | 24 | // create a new comment by campground id 25 | router.post('/comment', authentication.isLoggedIn, (req: any, res: any) => { 26 | req.body.text = req.sanitize(req.body.text); 27 | 28 | try { 29 | commentRepository.createOne(req.body, (callback: any) => { 30 | callback.message = 'OK'; 31 | res.status(200).send(callback); 32 | }); 33 | } catch (err) { 34 | res.status(500).send({message: err}); 35 | } 36 | }); 37 | 38 | // edit one comment by comment id 39 | router.put('/comment/:comment_id/edit', authentication.checkCommentOwner, (req: any, res: any) => { 40 | req.body.text = req.sanitize(req.body.text); 41 | 42 | try { 43 | commentRepository.updateOne(req.body); 44 | res.status(200).send({message: 'OK'}); 45 | } catch (err) { 46 | res.status(500).send({message: err}); 47 | } 48 | }); 49 | 50 | // delete one comment 51 | router.delete('/comment/:comment_id', authentication.checkCommentOwner, (req: any, res: any) => { 52 | try { 53 | commentRepository.deleteOne(req.params.comment_id); 54 | res.status(200).send({message: 'OK'}); 55 | } catch (err) { 56 | res.status(500).send({message: err}); 57 | } 58 | }); 59 | 60 | export = router; 61 | -------------------------------------------------------------------------------- /server/src/routes/User.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by laurence-ho on 22/07/17. 3 | */ 4 | 5 | import * as express from 'express'; 6 | import * as passport from 'passport'; 7 | import * as authentication from '../Authentication'; 8 | 9 | const router = express.Router(); 10 | 11 | router.post('/login', (req: any, res: any, next: any) => { 12 | passport.authenticate('local-login', (err: any, user: any, info: any) => { 13 | if (err) { 14 | return res.status(401).send({message: err.message}); 15 | } 16 | if (!user) { 17 | return res.status(401).send({message: info.message}); 18 | } 19 | req.logIn(user, (err: any) => { 20 | if (err) { 21 | return res.status(401).send({message: err.message}); 22 | } 23 | 24 | let milliseconds = 0; 25 | if (req.body.remember) { 26 | milliseconds = 1000 * 60 * 30; // 30 minutes 27 | 28 | req.session.cookie.expires = new Date(Date.now() + milliseconds); 29 | req.session.cookie.maxAge = milliseconds; 30 | } else { 31 | milliseconds = 1000 * 60 * 60 * 24; // 1 day 32 | 33 | req.session.cookie.expires = new Date(Date.now() + milliseconds); 34 | req.session.cookie.maxAge = milliseconds; 35 | } 36 | return res.status(200).json(req.user); 37 | }); 38 | })(req, res, next); 39 | }); 40 | 41 | router.post('/signup', (req: any, res: any, next: any) => { 42 | passport.authenticate('local-signup', (err: any, user: any, info: any) => { 43 | if (err) { 44 | return res.status(403).send({message: err.message}); 45 | } 46 | 47 | if (!user) { 48 | return res.status(403).send({message: info.message}); 49 | } 50 | 51 | return res.status(200).send({message: 'OK'}); 52 | })(req, res, next); 53 | } 54 | ); 55 | 56 | router.get('/profile', authentication.isLoggedIn, (req: any, res: any) => { 57 | res.json(req.user); 58 | }); 59 | 60 | router.get('/logout', (req: any) => { 61 | req.session.destroy((err: any) => { 62 | console.error(err); 63 | }); 64 | req.logout(); 65 | }); 66 | 67 | export = router; 68 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "skipLibCheck": true, 10 | "lib": [ 11 | "es2015", 12 | "dom" 13 | ], 14 | "types": [ 15 | "node", 16 | "lodash" 17 | ], 18 | "noImplicitAny": true, 19 | "suppressImplicitAnyIndexErrors": true, 20 | "typeRoots": [ 21 | "./node_modules/@types/" 22 | ] 23 | }, 24 | "compileOnSave": true, 25 | "exclude": [ 26 | "node_modules/*" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:latest" 4 | ], 5 | "rules": { 6 | "arrow-parens": false, 7 | "interface-name": [ 8 | true, 9 | "never-prefix" 10 | ], 11 | "member-access": false, 12 | "member-ordering": false, 13 | "no-console": false, 14 | "no-debugger": false, 15 | "no-implicit-dependencies": false, 16 | "no-shadowed-variable": false, 17 | "no-submodule-imports": false, 18 | "no-trailing-whitespace": false, 19 | "no-var-requires": false, 20 | "object-literal-sort-keys": false, 21 | "prefer-for-of": false, 22 | "quotemark": [ 23 | true, 24 | "single" 25 | ], 26 | "trailing-comma": false, 27 | "variable-name": false 28 | } 29 | } 30 | --------------------------------------------------------------------------------