adding form validation on signup.

adding auth/signin and auth/signup api endpoints.
This commit is contained in:
Jason Kulatunga 2022-09-11 23:59:13 -04:00
parent ee65995e44
commit 1a3dce77cb
13 changed files with 284 additions and 12 deletions

54
backend/pkg/auth/utils.go Normal file
View File

@ -0,0 +1,54 @@
package auth
import (
"errors"
"github.com/golang-jwt/jwt"
"time"
)
//TODO: this key should be dynamically generated/taken from config file.
var jwtKey = []byte("supersecretkey")
//TODO: this should match the ID and username for the user.
type JWTClaim struct {
Username string `json:"username"`
Email string `json:"email"`
jwt.StandardClaims
}
func GenerateJWT(username string) (tokenString string, err error) {
expirationTime := time.Now().Add(2 * time.Hour)
claims := &JWTClaim{
Username: username,
Email: username,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expirationTime.Unix(),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err = token.SignedString(jwtKey)
return
}
func ValidateToken(signedToken string) (err error) {
token, err := jwt.ParseWithClaims(
signedToken,
&JWTClaim{},
func(token *jwt.Token) (interface{}, error) {
return []byte(jwtKey), nil
},
)
if err != nil {
return
}
claims, ok := token.Claims.(*JWTClaim)
if !ok {
err = errors.New("couldn't parse claims")
return
}
if claims.ExpiresAt < time.Now().Local().Unix() {
err = errors.New("token expired")
return
}
return
}

View File

@ -8,6 +8,9 @@ import (
//go:generate mockgen -source=interface.go -destination=mock/mock_database.go //go:generate mockgen -source=interface.go -destination=mock/mock_database.go
type DatabaseRepository interface { type DatabaseRepository interface {
Close() error Close() error
CreateUser(context.Context, *models.User) error
GetUserByEmail(context.Context, string) (*models.User, error)
GetCurrentUser() models.User GetCurrentUser() models.User
UpsertResource(context.Context, models.ResourceFhir) error UpsertResource(context.Context, models.ResourceFhir) error

View File

@ -78,6 +78,26 @@ func (sr *sqliteRepository) Close() error {
return nil return nil
} }
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// User
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func (sr *sqliteRepository) CreateUser(ctx context.Context, user *models.User) error {
if err := user.HashPassword(user.Password); err != nil {
return err
}
record := sr.gormClient.Create(&user)
if record.Error != nil {
return record.Error
}
return nil
}
func (sr *sqliteRepository) GetUserByEmail(ctx context.Context, username string) (*models.User, error) {
var foundUser models.User
result := sr.gormClient.Model(models.User{}).Where(models.User{Username: username}).First(&foundUser)
return &foundUser, result.Error
}
func (sr *sqliteRepository) GetCurrentUser() models.User { func (sr *sqliteRepository) GetCurrentUser() models.User {
var currentUser models.User var currentUser models.User
sr.gormClient.Model(models.User{}).First(&currentUser) sr.gormClient.Model(models.User{}).First(&currentUser)

View File

@ -1,6 +1,26 @@
package models package models
import "golang.org/x/crypto/bcrypt"
type User struct { type User struct {
ModelBase ModelBase
Name string `json:"name"`
Username string `json:"username" gorm:"unique"` Username string `json:"username" gorm:"unique"`
Password string `json:"password"`
}
func (user *User) HashPassword(password string) error {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
if err != nil {
return err
}
user.Password = string(bytes)
return nil
}
func (user *User) CheckPassword(providedPassword string) error {
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(providedPassword))
if err != nil {
return err
}
return nil
} }

View File

@ -0,0 +1,64 @@
package handler
import (
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/auth"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/database"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/models"
"github.com/gin-gonic/gin"
"net/http"
)
func AuthSignup(c *gin.Context) {
databaseRepo := c.MustGet("REPOSITORY").(database.DatabaseRepository)
var user models.User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
err := databaseRepo.CreateUser(c, &user)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
// return JWT
tokenString, err := auth.GenerateJWT(user.Username)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "data": tokenString})
}
func AuthSignin(c *gin.Context) {
databaseRepo := c.MustGet("REPOSITORY").(database.DatabaseRepository)
var user models.User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
foundUser, err := databaseRepo.GetUserByEmail(c, user.Username)
if err != nil || foundUser == nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
err = foundUser.CheckPassword(user.Password)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"success": false})
return
}
// return JWT
tokenString, err := auth.GenerateJWT(user.Username)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "data": tokenString})
}

View File

@ -7,6 +7,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"net/http" "net/http"
"strings"
) )
func CreateSource(c *gin.Context) { func CreateSource(c *gin.Context) {
@ -109,7 +110,7 @@ func RawRequestSource(c *gin.Context) {
} }
var resp map[string]interface{} var resp map[string]interface{}
err = client.GetRequest(c.Param("path"), &resp) err = client.GetRequest(strings.TrimSuffix(c.Param("path"), "/"), &resp)
if err != nil { if err != nil {
logger.Errorf("Error making raw request", err) logger.Errorf("Error making raw request", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false}) c.JSON(http.StatusInternalServerError, gin.H{"success": false})

View File

@ -39,6 +39,10 @@ func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine {
"success": true, "success": true,
}) })
}) })
api.POST("/auth/signup", handler.AuthSignup)
api.POST("/auth/signin", handler.AuthSignin)
api.POST("/source", handler.CreateSource) api.POST("/source", handler.CreateSource)
api.GET("/source", handler.ListSource) api.GET("/source", handler.ListSource)
api.GET("/source/raw/:sourceType/*path", handler.RawRequestSource) api.GET("/source/raw/:sourceType/*path", handler.RawRequestSource)

View File

@ -17,7 +17,7 @@ import { far } from '@fortawesome/free-regular-svg-icons';
import { ResourceDetailComponent } from './pages/resource-detail/resource-detail.component'; import { ResourceDetailComponent } from './pages/resource-detail/resource-detail.component';
import { AuthSignupComponent } from './pages/auth-signup/auth-signup.component'; import { AuthSignupComponent } from './pages/auth-signup/auth-signup.component';
import { AuthSigninComponent } from './pages/auth-signin/auth-signin.component'; import { AuthSigninComponent } from './pages/auth-signin/auth-signin.component';
import { FormsModule } from '@angular/forms';
@NgModule({ @NgModule({
declarations: [ declarations: [
AppComponent, AppComponent,
@ -30,6 +30,7 @@ import { AuthSigninComponent } from './pages/auth-signin/auth-signin.component';
AuthSigninComponent, AuthSigninComponent,
], ],
imports: [ imports: [
FormsModule,
BrowserModule, BrowserModule,
FontAwesomeModule, FontAwesomeModule,
SharedModule, SharedModule,

View File

@ -0,0 +1,6 @@
export class User {
user_id?: number
name?: string
username?: string
password?: string
}

View File

@ -7,6 +7,8 @@
<p>We are excited to launch our new company and product Azia. After being featured in too many magazines to mention and having created an online stir, we know that BootstrapDash is going to be big. We also hope to win Startup Fictional Business of the Year this year.</p> <p>We are excited to launch our new company and product Azia. After being featured in too many magazines to mention and having created an online stir, we know that BootstrapDash is going to be big. We also hope to win Startup Fictional Business of the Year this year.</p>
<p>Browse our site and see for yourself why you need Azia.</p> <p>Browse our site and see for yourself why you need Azia.</p>
<a routerLink="/" class="btn btn-outline-indigo">Learn More</a> <a routerLink="/" class="btn btn-outline-indigo">Learn More</a>
{{ newUser | json }}
</div> </div>
</div><!-- az-column-signup-left --> </div><!-- az-column-signup-left -->
<div class="az-column-signup"> <div class="az-column-signup">
@ -15,24 +17,56 @@
<h2>Get Started</h2> <h2>Get Started</h2>
<h4>It's free to signup and only takes a minute.</h4> <h4>It's free to signup and only takes a minute.</h4>
<form> <form (ngSubmit)="signupSubmit()" #userForm="ngForm">
<div class="form-group"> <div class="form-group">
<label>Firstname &amp; Lastname</label> <label>Firstname &amp; Lastname</label>
<input type="text" class="form-control" placeholder="Enter your firstname and lastname"> <input [(ngModel)]="newUser.name" name="name" #name="ngModel" required minlength="2" type="text" class="form-control" placeholder="Enter your firstname and lastname">
<div *ngIf="name.invalid && (name.dirty || name.touched)" class="alert alert-danger">
<div *ngIf="name.errors?.['required']">
Name is required.
</div>
<div *ngIf="name.errors?.['minlength']">
Name must be at least 4 characters long.
</div>
</div>
</div><!-- form-group --> </div><!-- form-group -->
<div class="form-group"> <div class="form-group">
<label>Email</label> <label>Email</label>
<input type="text" class="form-control" placeholder="Enter your email"> <input [(ngModel)]="newUser.username" name="username" #username="ngModel" required email="true" minlength="5" type="text" class="form-control" placeholder="Enter your email">
<div *ngIf="username.invalid && (username.dirty || username.touched)" class="alert alert-danger">
<div *ngIf="username.errors?.['required']">
Email is required.
</div>
<div *ngIf="username.errors?.['minlength']">
Email must be at least 4 characters long.
</div>
<div *ngIf="username.errors?.['email']">
Email is not a valid email address.
</div>
</div>
</div><!-- form-group --> </div><!-- form-group -->
<div class="form-group"> <div class="form-group">
<label>Password</label> <label>Password</label>
<input type="password" class="form-control" placeholder="Enter your password"> <input [(ngModel)]="newUser.password" name="password" #password="ngModel" required minlength="8" type="password" class="form-control" placeholder="Enter your password">
<div *ngIf="password.invalid && (password.dirty || password.touched)" class="alert alert-danger">
<div *ngIf="password.errors?.['required']">
Password is required.
</div>
<div *ngIf="password.errors?.['minlength']">
Name must be at least 8 characters long.
</div>
</div>
</div><!-- form-group --> </div><!-- form-group -->
<a class="btn btn-az-primary btn-block" routerLink="/">Create Account</a> <button [disabled]="!userForm.form.valid" type="submit" class="btn btn-az-primary btn-block">Create Account</button>
</form> </form>
</div><!-- az-signup-header --> </div><!-- az-signup-header -->
<div class="az-signup-footer"> <div class="az-signup-footer">
<p>Already have an account? <a routerLink="general-pages/signin">Sign In</a></p> <p>Already have an account? <a routerLink="auth/signin">Sign In</a></p>
</div><!-- az-signin-footer --> </div><!-- az-signin-footer -->
</div><!-- az-column-signup --> </div><!-- az-column-signup -->
</div><!-- az-signup-wrapper --> </div><!-- az-signup-wrapper -->

View File

@ -1,4 +1,8 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import {FastenApiService} from '../../services/fasten-api.service';
import {ProviderConfig} from '../../models/passport/provider-config';
import {User} from '../../models/fasten/user';
import {Router} from '@angular/router';
@Component({ @Component({
selector: 'app-auth-signup', selector: 'app-auth-signup',
@ -6,10 +10,23 @@ import { Component, OnInit } from '@angular/core';
styleUrls: ['./auth-signup.component.scss'] styleUrls: ['./auth-signup.component.scss']
}) })
export class AuthSignupComponent implements OnInit { export class AuthSignupComponent implements OnInit {
submitted: boolean = false
newUser: User = new User()
constructor() { } constructor(private fastenApi: FastenApiService, private router: Router) { }
ngOnInit(): void { ngOnInit(): void {
} }
signupSubmit(){
this.submitted = true;
this.fastenApi.signup(this.newUser).subscribe((tokenResp: any) => {
console.log(tokenResp);
this.router.navigateByUrl('/dashboard');
})
}
} }

View File

@ -1,24 +1,71 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import {HttpClient} from '@angular/common/http'; import { HttpClient, HttpHeaders } from '@angular/common/http';
import {Observable} from 'rxjs'; import {Observable} from 'rxjs';
import { Router } from '@angular/router';
import {ProviderConfig} from '../models/passport/provider-config'; import {ProviderConfig} from '../models/passport/provider-config';
import {environment} from '../../environments/environment'; import {environment} from '../../environments/environment';
import {map} from 'rxjs/operators'; import {map} from 'rxjs/operators';
import {ResponseWrapper} from '../models/response-wrapper'; import {ResponseWrapper} from '../models/response-wrapper';
import {Source} from '../models/fasten/source'; import {Source} from '../models/fasten/source';
import {User} from '../models/fasten/user';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class FastenApiService { export class FastenApiService {
constructor(private _httpClient: HttpClient) { AUTH_TOKEN_KEY = 'token';
constructor(private _httpClient: HttpClient, private router: Router) {
} }
getBasePath(): string { getBasePath(): string {
return window.location.pathname.split('/web').slice(0, 1)[0]; return window.location.pathname.split('/web').slice(0, 1)[0];
} }
// Auth functions
token() {
return localStorage.getItem(this.AUTH_TOKEN_KEY);
}
isAuthenticated() {
return !!localStorage.getItem(this.AUTH_TOKEN_KEY);
}
logout() {
localStorage.removeItem(this.AUTH_TOKEN_KEY);
this.router.navigateByUrl('/');
}
signup(newUser: User): Observable<any> {
return this._httpClient.post<any>(`${this.getBasePath()}/api/auth/signup`, newUser).pipe(
map((res: any) => {
localStorage.setItem(this.AUTH_TOKEN_KEY, res.data);
return res.data
}
));
}
signin(email: string, pass: string) {
const headers = {
headers: new HttpHeaders({ 'Content-Type': 'application/json', 'Cache-Control': 'no-cache' })
};
const data = {
email: email,
password: pass
};
this._httpClient.post<any>(`${this.getBasePath()}/api/auth/signin`, data, headers).subscribe(
(res: any) => {
localStorage.setItem(this.AUTH_TOKEN_KEY, res.token);
this.router.navigateByUrl('/dashboard');
}
);
}
createSource(source: Source): Observable<Source> { createSource(source: Source): Observable<Source> {
return this._httpClient.post<any>(`${this.getBasePath()}/api/source`, source) return this._httpClient.post<any>(`${this.getBasePath()}/api/source`, source)
.pipe( .pipe(

3
go.mod
View File

@ -7,6 +7,7 @@ require (
github.com/fastenhealth/gofhir-models v0.0.4 github.com/fastenhealth/gofhir-models v0.0.4
github.com/gin-gonic/gin v1.8.1 github.com/gin-gonic/gin v1.8.1
github.com/glebarez/sqlite v1.4.6 github.com/glebarez/sqlite v1.4.6
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang/mock v1.4.4 github.com/golang/mock v1.4.4
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/samber/lo v1.27.1 github.com/samber/lo v1.27.1
@ -15,6 +16,7 @@ require (
github.com/spf13/viper v1.12.0 github.com/spf13/viper v1.12.0
github.com/stretchr/testify v1.7.1 github.com/stretchr/testify v1.7.1
github.com/urfave/cli/v2 v2.11.2 github.com/urfave/cli/v2 v2.11.2
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
gorm.io/datatypes v1.0.7 gorm.io/datatypes v1.0.7
gorm.io/gorm v1.23.8 gorm.io/gorm v1.23.8
@ -58,7 +60,6 @@ require (
github.com/subosito/gotenv v1.3.0 // indirect github.com/subosito/gotenv v1.3.0 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect github.com/ugorji/go/codec v1.2.7 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect