adding form validation on signup.
adding auth/signin and auth/signup api endpoints.
This commit is contained in:
parent
ee65995e44
commit
1a3dce77cb
|
@ -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
|
||||
}
|
|
@ -8,6 +8,9 @@ import (
|
|||
//go:generate mockgen -source=interface.go -destination=mock/mock_database.go
|
||||
type DatabaseRepository interface {
|
||||
Close() error
|
||||
|
||||
CreateUser(context.Context, *models.User) error
|
||||
GetUserByEmail(context.Context, string) (*models.User, error)
|
||||
GetCurrentUser() models.User
|
||||
|
||||
UpsertResource(context.Context, models.ResourceFhir) error
|
||||
|
|
|
@ -78,6 +78,26 @@ func (sr *sqliteRepository) Close() error {
|
|||
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 {
|
||||
var currentUser models.User
|
||||
sr.gormClient.Model(models.User{}).First(¤tUser)
|
||||
|
|
|
@ -1,6 +1,26 @@
|
|||
package models
|
||||
|
||||
import "golang.org/x/crypto/bcrypt"
|
||||
|
||||
type User struct {
|
||||
ModelBase
|
||||
Name string `json:"name"`
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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})
|
||||
}
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CreateSource(c *gin.Context) {
|
||||
|
@ -109,7 +110,7 @@ func RawRequestSource(c *gin.Context) {
|
|||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
err = client.GetRequest(c.Param("path"), &resp)
|
||||
err = client.GetRequest(strings.TrimSuffix(c.Param("path"), "/"), &resp)
|
||||
if err != nil {
|
||||
logger.Errorf("Error making raw request", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false})
|
||||
|
|
|
@ -39,6 +39,10 @@ func (ae *AppEngine) Setup(logger *logrus.Entry) *gin.Engine {
|
|||
"success": true,
|
||||
})
|
||||
})
|
||||
|
||||
api.POST("/auth/signup", handler.AuthSignup)
|
||||
api.POST("/auth/signin", handler.AuthSignin)
|
||||
|
||||
api.POST("/source", handler.CreateSource)
|
||||
api.GET("/source", handler.ListSource)
|
||||
api.GET("/source/raw/:sourceType/*path", handler.RawRequestSource)
|
||||
|
|
|
@ -17,7 +17,7 @@ import { far } from '@fortawesome/free-regular-svg-icons';
|
|||
import { ResourceDetailComponent } from './pages/resource-detail/resource-detail.component';
|
||||
import { AuthSignupComponent } from './pages/auth-signup/auth-signup.component';
|
||||
import { AuthSigninComponent } from './pages/auth-signin/auth-signin.component';
|
||||
|
||||
import { FormsModule } from '@angular/forms';
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
|
@ -30,6 +30,7 @@ import { AuthSigninComponent } from './pages/auth-signin/auth-signin.component';
|
|||
AuthSigninComponent,
|
||||
],
|
||||
imports: [
|
||||
FormsModule,
|
||||
BrowserModule,
|
||||
FontAwesomeModule,
|
||||
SharedModule,
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
export class User {
|
||||
user_id?: number
|
||||
name?: string
|
||||
username?: string
|
||||
password?: string
|
||||
}
|
|
@ -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>Browse our site and see for yourself why you need Azia.</p>
|
||||
<a routerLink="/" class="btn btn-outline-indigo">Learn More</a>
|
||||
|
||||
{{ newUser | json }}
|
||||
</div>
|
||||
</div><!-- az-column-signup-left -->
|
||||
<div class="az-column-signup">
|
||||
|
@ -15,24 +17,56 @@
|
|||
<h2>Get Started</h2>
|
||||
<h4>It's free to signup and only takes a minute.</h4>
|
||||
|
||||
<form>
|
||||
<form (ngSubmit)="signupSubmit()" #userForm="ngForm">
|
||||
<div class="form-group">
|
||||
<label>Firstname & 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 class="form-group">
|
||||
<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 class="form-group">
|
||||
<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 -->
|
||||
<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>
|
||||
</div><!-- az-signup-header -->
|
||||
<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-column-signup -->
|
||||
</div><!-- az-signup-wrapper -->
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
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({
|
||||
selector: 'app-auth-signup',
|
||||
|
@ -6,10 +10,23 @@ import { Component, OnInit } from '@angular/core';
|
|||
styleUrls: ['./auth-signup.component.scss']
|
||||
})
|
||||
export class AuthSignupComponent implements OnInit {
|
||||
submitted: boolean = false
|
||||
newUser: User = new User()
|
||||
|
||||
constructor() { }
|
||||
constructor(private fastenApi: FastenApiService, private router: Router) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
signupSubmit(){
|
||||
this.submitted = true;
|
||||
|
||||
this.fastenApi.signup(this.newUser).subscribe((tokenResp: any) => {
|
||||
console.log(tokenResp);
|
||||
|
||||
this.router.navigateByUrl('/dashboard');
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,24 +1,71 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import {Observable} from 'rxjs';
|
||||
import { Router } from '@angular/router';
|
||||
import {ProviderConfig} from '../models/passport/provider-config';
|
||||
import {environment} from '../../environments/environment';
|
||||
import {map} from 'rxjs/operators';
|
||||
import {ResponseWrapper} from '../models/response-wrapper';
|
||||
import {Source} from '../models/fasten/source';
|
||||
import {User} from '../models/fasten/user';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class FastenApiService {
|
||||
|
||||
constructor(private _httpClient: HttpClient) {
|
||||
AUTH_TOKEN_KEY = 'token';
|
||||
|
||||
constructor(private _httpClient: HttpClient, private router: Router) {
|
||||
}
|
||||
|
||||
getBasePath(): string {
|
||||
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> {
|
||||
return this._httpClient.post<any>(`${this.getBasePath()}/api/source`, source)
|
||||
.pipe(
|
||||
|
|
3
go.mod
3
go.mod
|
@ -7,6 +7,7 @@ require (
|
|||
github.com/fastenhealth/gofhir-models v0.0.4
|
||||
github.com/gin-gonic/gin v1.8.1
|
||||
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/google/uuid v1.3.0
|
||||
github.com/samber/lo v1.27.1
|
||||
|
@ -15,6 +16,7 @@ require (
|
|||
github.com/spf13/viper v1.12.0
|
||||
github.com/stretchr/testify v1.7.1
|
||||
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
|
||||
gorm.io/datatypes v1.0.7
|
||||
gorm.io/gorm v1.23.8
|
||||
|
@ -58,7 +60,6 @@ require (
|
|||
github.com/subosito/gotenv v1.3.0 // indirect
|
||||
github.com/ugorji/go/codec v1.2.7 // 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/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||
|
|
Loading…
Reference in New Issue