commit
						d504997def
					
				 172 changed files with 66423 additions and 0 deletions
			
			
		@ -0,0 +1,47 @@ | 
			
		|||||
 | 
				# See http://help.github.com/ignore-files/ for more about ignoring files. | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				# compiled output | 
			
		||||
 | 
				/tapit-frontend/dist | 
			
		||||
 | 
				/tapit-frontend/tmp | 
			
		||||
 | 
				/tapit-frontend/out-tsc | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				# postgres data | 
			
		||||
 | 
				/postgres-data | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				# dependencies | 
			
		||||
 | 
				/tapit-frontend/node_modules | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				# profiling files | 
			
		||||
 | 
				chrome-profiler-events.json | 
			
		||||
 | 
				speed-measure-plugin.json | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				# IDEs and editors | 
			
		||||
 | 
				/.idea | 
			
		||||
 | 
				.project | 
			
		||||
 | 
				.classpath | 
			
		||||
 | 
				.c9/ | 
			
		||||
 | 
				*.launch | 
			
		||||
 | 
				.settings/ | 
			
		||||
 | 
				*.sublime-workspace | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				# IDE - VSCode | 
			
		||||
 | 
				.vscode/* | 
			
		||||
 | 
				!.vscode/settings.json | 
			
		||||
 | 
				!.vscode/tasks.json | 
			
		||||
 | 
				!.vscode/launch.json | 
			
		||||
 | 
				!.vscode/extensions.json | 
			
		||||
 | 
				.history/* | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				# misc | 
			
		||||
 | 
				/.sass-cache | 
			
		||||
 | 
				/connect.lock | 
			
		||||
 | 
				/coverage | 
			
		||||
 | 
				/libpeerconnection.log | 
			
		||||
 | 
				npm-debug.log | 
			
		||||
 | 
				yarn-error.log | 
			
		||||
 | 
				testem.log | 
			
		||||
 | 
				/typings | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				# System Files | 
			
		||||
 | 
				.DS_Store | 
			
		||||
 | 
				Thumbs.db | 
			
		||||
@ -0,0 +1,14 @@ | 
			
		|||||
 | 
				#!/bin/bash | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				cd ./tapit-backend | 
			
		||||
 | 
				go build | 
			
		||||
 | 
				cd .. | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				cd ./tapit-frontend | 
			
		||||
 | 
				ng build --optimization | 
			
		||||
 | 
				cd .. | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				cp -r ./tapit-frontend/dist/tapit-frontend/* ./tapit-build/static/ | 
			
		||||
 | 
				cp ./tapit-backend/tapit-backend ./tapit-build/tapit | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				./tapit-build/tapit | 
			
		||||
@ -0,0 +1,26 @@ | 
			
		|||||
 | 
				version: '3' | 
			
		||||
 | 
				services: | 
			
		||||
 | 
				  ####################################### | 
			
		||||
 | 
				  # TapIt Application | 
			
		||||
 | 
				  ####################################### | 
			
		||||
 | 
				  app: | 
			
		||||
 | 
				    build: | 
			
		||||
 | 
				      context: ./tapit-build/ | 
			
		||||
 | 
				      dockerfile: Dockerfile | 
			
		||||
 | 
				    ports: | 
			
		||||
 | 
				      - "127.0.0.1:8000:8000" | 
			
		||||
 | 
				    restart: always | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  ####################################### | 
			
		||||
 | 
				  # Postgres server | 
			
		||||
 | 
				  ####################################### | 
			
		||||
 | 
				  postgres-tapit: | 
			
		||||
 | 
				    image: postgres | 
			
		||||
 | 
				    restart: always | 
			
		||||
 | 
				    volumes: | 
			
		||||
 | 
				      - ./postgres-data:/var/lib/postgresql/data | 
			
		||||
 | 
				    environment: | 
			
		||||
 | 
				      - POSTGRES_USER=tapit | 
			
		||||
 | 
				      - POSTGRES_PASSWORD=secret-tapit-password | 
			
		||||
 | 
				      - POSTGRES_DB=tapit | 
			
		||||
 | 
				
 | 
			
		||||
@ -0,0 +1,4 @@ | 
			
		|||||
 | 
				#!/bin/bash | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				sudo docker rm postgres --force | 
			
		||||
 | 
				sudo docker run -d -e POSTGRES_USER="tapit" -e POSTGRES_PASSWORD="secret-tapit-password" -e POSTGRES_DB="tapit" --name postgres postgres | 
			
		||||
@ -0,0 +1,389 @@ | 
			
		|||||
 | 
				package main | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				import ( | 
			
		||||
 | 
				    "net/http" | 
			
		||||
 | 
				    "strings" | 
			
		||||
 | 
				    "io/ioutil" | 
			
		||||
 | 
				    "encoding/json" | 
			
		||||
 | 
				    "github.com/jinzhu/gorm" | 
			
		||||
 | 
				    "math/rand" | 
			
		||||
 | 
				    "time" | 
			
		||||
 | 
				    "golang.org/x/crypto/bcrypt" | 
			
		||||
 | 
				) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				type UserJson struct { | 
			
		||||
 | 
				    Username string             `json:"username"` | 
			
		||||
 | 
				    Password string             `json:"password"` | 
			
		||||
 | 
				    Name string                 `json:"name"` | 
			
		||||
 | 
				    Email string                `json:"email"` | 
			
		||||
 | 
				    SecretCode string           `json:"secretCode"` | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				type User struct { | 
			
		||||
 | 
				    gorm.Model | 
			
		||||
 | 
				    Username string | 
			
		||||
 | 
				    PasswordHash string | 
			
		||||
 | 
				    Name string | 
			
		||||
 | 
				    Email string | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				type Session struct { | 
			
		||||
 | 
				    gorm.Model | 
			
		||||
 | 
				    SessionID string | 
			
		||||
 | 
				    UserID uint | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) login(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    if strings.ToUpper(r.Method) == "POST" { | 
			
		||||
 | 
				        // start doing work
 | 
			
		||||
 | 
				        requestBody, err:= ioutil.ReadAll(r.Body) | 
			
		||||
 | 
				        if err != nil { | 
			
		||||
 | 
				            http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				        userJson := UserJson{} | 
			
		||||
 | 
				        err = json.Unmarshal(requestBody, &userJson) | 
			
		||||
 | 
				        if err != nil { | 
			
		||||
 | 
				            http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				        currUser := User{} | 
			
		||||
 | 
				        tapit.db.Where(&User{Username:userJson.Username}).First(&currUser) | 
			
		||||
 | 
				        // user exists
 | 
			
		||||
 | 
				        if currUser.Username == userJson.Username { | 
			
		||||
 | 
				            // checking hash...
 | 
			
		||||
 | 
				            if checkPasswordHash(currUser.PasswordHash, userJson.Password) { | 
			
		||||
 | 
				                userJson.Password = "" | 
			
		||||
 | 
				                userJson.Name = currUser.Name | 
			
		||||
 | 
				                userJson.Email = currUser.Email | 
			
		||||
 | 
				                messageOutput := NotificationJson{ | 
			
		||||
 | 
				                    Text: "Successfully logged in!", | 
			
		||||
 | 
				                    ResultType:   "success", | 
			
		||||
 | 
				                    Payload: userJson, | 
			
		||||
 | 
				                    } | 
			
		||||
 | 
				                jsonResults, err := json.Marshal(messageOutput) | 
			
		||||
 | 
				                if err!=nil { | 
			
		||||
 | 
				                    http.Error(w, "Internal server error", 500) | 
			
		||||
 | 
				                    return | 
			
		||||
 | 
				                } | 
			
		||||
 | 
				                w.Header().Set("Content-Type", "application/json") | 
			
		||||
 | 
				                authCookie := tapit.generateCookie(currUser) | 
			
		||||
 | 
				                http.SetCookie(w, &authCookie) | 
			
		||||
 | 
				                w.Write(jsonResults) | 
			
		||||
 | 
				                return | 
			
		||||
 | 
				            } else { | 
			
		||||
 | 
				                notifyPopup(w, r, "failure", "Username or password is incorrect", nil) | 
			
		||||
 | 
				                return | 
			
		||||
 | 
				            } | 
			
		||||
 | 
				        } else { | 
			
		||||
 | 
				            tapit.hashPassword("nothing-to-do-waste-time") | 
			
		||||
 | 
				            notifyPopup(w, r, "failure", "Username or password is incorrect", nil) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        http.Error(w, "HTTP method not implemented", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) register(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    if strings.ToUpper(r.Method) == "POST" { | 
			
		||||
 | 
				        // start doing work
 | 
			
		||||
 | 
				        requestBody, err:= ioutil.ReadAll(r.Body) | 
			
		||||
 | 
				        if err != nil { | 
			
		||||
 | 
				            http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        userJson := UserJson{} | 
			
		||||
 | 
				        err = json.Unmarshal(requestBody, &userJson) | 
			
		||||
 | 
				        if err != nil { | 
			
		||||
 | 
				            http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        // checks if secret code is correct
 | 
			
		||||
 | 
				        if userJson.SecretCode != tapit.globalSettings.secretRegistrationCode { | 
			
		||||
 | 
				            messageOutput := NotificationJson{ | 
			
		||||
 | 
				            Text: "Your secret code is incorrect. Please try again.", | 
			
		||||
 | 
				            ResultType:   "failure", | 
			
		||||
 | 
				            } | 
			
		||||
 | 
				            jsonResults, err := json.Marshal(messageOutput) | 
			
		||||
 | 
				            if err!=nil { | 
			
		||||
 | 
				                http.Error(w, "Internal server error", 500) | 
			
		||||
 | 
				                return | 
			
		||||
 | 
				            } | 
			
		||||
 | 
				            w.Header().Set("Content-Type", "application/json") | 
			
		||||
 | 
				            http.Error(w, string(jsonResults), 200) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        //check if user exists
 | 
			
		||||
 | 
				        currUser := User{} | 
			
		||||
 | 
				        tapit.db.Where(&User{Username: userJson.Username}).First(&currUser) | 
			
		||||
 | 
				        if currUser.Username != "" { | 
			
		||||
 | 
				            messageOutput := NotificationJson{ | 
			
		||||
 | 
				            Text: "Username exists. Please choose another one.", | 
			
		||||
 | 
				            ResultType:   "failure", | 
			
		||||
 | 
				            } | 
			
		||||
 | 
				            jsonResults, err := json.Marshal(messageOutput) | 
			
		||||
 | 
				            if err!=nil { | 
			
		||||
 | 
				                http.Error(w, "Internal server error", 500) | 
			
		||||
 | 
				                return | 
			
		||||
 | 
				            } | 
			
		||||
 | 
				            w.Header().Set("Content-Type", "application/json") | 
			
		||||
 | 
				            http.Error(w, string(jsonResults), 200) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        //input validation that all are filled
 | 
			
		||||
 | 
				        if userJson.Username == "" || userJson.Name == "" || userJson.Email == "" || userJson.Password == "" { | 
			
		||||
 | 
				            messageOutput := NotificationJson{ | 
			
		||||
 | 
				            Text: "Please fill up all the information", | 
			
		||||
 | 
				            ResultType:   "failure", | 
			
		||||
 | 
				            } | 
			
		||||
 | 
				            jsonResults, err := json.Marshal(messageOutput) | 
			
		||||
 | 
				            if err!=nil { | 
			
		||||
 | 
				                http.Error(w, "Internal server error", 500) | 
			
		||||
 | 
				                return | 
			
		||||
 | 
				            } | 
			
		||||
 | 
				            w.Header().Set("Content-Type", "application/json") | 
			
		||||
 | 
				            http.Error(w, string(jsonResults), 200) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        // creates user...
 | 
			
		||||
 | 
				        currUser.Username = userJson.Username | 
			
		||||
 | 
				        currUser.Name = userJson.Name | 
			
		||||
 | 
				        currUser.Email = userJson.Email | 
			
		||||
 | 
				        currUser.PasswordHash, _ = tapit.hashPassword(userJson.Password) | 
			
		||||
 | 
				        var jsonResults []byte | 
			
		||||
 | 
				        if (tapit.db.NewRecord(&currUser)) { | 
			
		||||
 | 
				            tapit.db.Create(&currUser) | 
			
		||||
 | 
				            userJson.Password = "" | 
			
		||||
 | 
				            messageOutput := NotificationJson{ | 
			
		||||
 | 
				                Text: "Successfully registered!", | 
			
		||||
 | 
				                ResultType:   "success", | 
			
		||||
 | 
				                Payload: userJson, | 
			
		||||
 | 
				                } | 
			
		||||
 | 
				            jsonResults, err = json.Marshal(messageOutput) | 
			
		||||
 | 
				            if err!=nil { | 
			
		||||
 | 
				                http.Error(w, "Internal server error", 500) | 
			
		||||
 | 
				                return | 
			
		||||
 | 
				            } | 
			
		||||
 | 
				        } else { | 
			
		||||
 | 
				            http.Error(w, "Internal server error", 500) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				        w.Header().Set("Content-Type", "application/json") | 
			
		||||
 | 
				        authCookie := tapit.generateCookie(currUser) | 
			
		||||
 | 
				        http.SetCookie(w, &authCookie) | 
			
		||||
 | 
				        w.Write(jsonResults) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        http.Error(w, "HTTP method not implemented", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) logout(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    if strings.ToUpper(r.Method) == "POST" { | 
			
		||||
 | 
				        // start doing work
 | 
			
		||||
 | 
				        var currSession Session | 
			
		||||
 | 
				        authCookie, err := r.Cookie("tapitsession") | 
			
		||||
 | 
				        if err!=nil { | 
			
		||||
 | 
				            http.Error(w, "Not authorised", 401) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				        authCookieStr := authCookie.String()[13:] | 
			
		||||
 | 
				        tapit.db.Where(&Session{SessionID: authCookieStr}).First(&currSession) | 
			
		||||
 | 
				        if currSession.SessionID != authCookieStr { | 
			
		||||
 | 
				            http.Error(w, "Not authorised", 401) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } else { | 
			
		||||
 | 
				            tapit.db.Delete(&currSession) | 
			
		||||
 | 
				            messageOutput := NotificationJson{ | 
			
		||||
 | 
				            Text: "Successfully logged out", | 
			
		||||
 | 
				            ResultType:   "success", | 
			
		||||
 | 
				            Payload: "", | 
			
		||||
 | 
				            } | 
			
		||||
 | 
				            jsonResults, err := json.Marshal(messageOutput) | 
			
		||||
 | 
				            if err!=nil { | 
			
		||||
 | 
				                http.Error(w, "Internal server error", 500) | 
			
		||||
 | 
				                return | 
			
		||||
 | 
				            } | 
			
		||||
 | 
				            delCookie := tapit.deleteCookie() | 
			
		||||
 | 
				            http.SetCookie(w, &delCookie) | 
			
		||||
 | 
				            w.Header().Set("Content-Type", "application/json") | 
			
		||||
 | 
				            w.Write(jsonResults) | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        http.Error(w, "HTTP method not implemented", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) authenticationHandler(next http.HandlerFunc) http.HandlerFunc { | 
			
		||||
 | 
				    return func(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				        var currSession Session | 
			
		||||
 | 
				        authCookie, err := r.Cookie("tapitsession") | 
			
		||||
 | 
				        if err!=nil { | 
			
		||||
 | 
				            http.Error(w, "Not authorised", 401) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				        authCookieStr := authCookie.String()[13:] | 
			
		||||
 | 
				        tapit.db.Where(&Session{SessionID: authCookieStr}).First(&currSession) | 
			
		||||
 | 
				        if currSession.SessionID != authCookieStr { | 
			
		||||
 | 
				            http.Error(w, "Not authorised", 401) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } else { | 
			
		||||
 | 
				            next.ServeHTTP(w, r) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) generateCookie(user User) http.Cookie { | 
			
		||||
 | 
				    newToken := generateToken() | 
			
		||||
 | 
				    newSession := Session{} | 
			
		||||
 | 
				    tapit.db.Where(&Session{SessionID: newToken}).First(&newSession) | 
			
		||||
 | 
				    for newToken == newSession.SessionID { | 
			
		||||
 | 
				        newToken = generateToken() | 
			
		||||
 | 
				        tapit.db.Where(&Session{SessionID: newToken}).First(&newSession) | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    newSession.UserID = user.ID | 
			
		||||
 | 
				    newSession.SessionID = newToken | 
			
		||||
 | 
				    tapit.db.NewRecord(&newSession) | 
			
		||||
 | 
				    tapit.db.Create(&newSession) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    newCookie := http.Cookie { | 
			
		||||
 | 
				        Name: "tapitsession", | 
			
		||||
 | 
				        Value: newToken, | 
			
		||||
 | 
				        Path: "/", | 
			
		||||
 | 
				        MaxAge: 60*60*24*365*10, | 
			
		||||
 | 
				        HttpOnly: true, | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    return newCookie | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) deleteCookie() http.Cookie { | 
			
		||||
 | 
				    newCookie := http.Cookie { | 
			
		||||
 | 
				        Name: "tapitsession", | 
			
		||||
 | 
				        Value: "", | 
			
		||||
 | 
				        Path: "/", | 
			
		||||
 | 
				        MaxAge: 0, | 
			
		||||
 | 
				        HttpOnly: true, | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    return newCookie | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func generateToken() string { | 
			
		||||
 | 
				    var tokenResult strings.Builder | 
			
		||||
 | 
				    rand.Seed(time.Now().UnixNano()) | 
			
		||||
 | 
				    var r int | 
			
		||||
 | 
				    tokenCharset := "abcdefghijklmnopqrstuvwxyz0123456789" | 
			
		||||
 | 
				    for i:=0; i<16; i++ { | 
			
		||||
 | 
				        r = rand.Int() % len(tokenCharset) | 
			
		||||
 | 
				        tokenResult.WriteRune(rune(tokenCharset[r])) | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    return tokenResult.String() | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) hashPassword(password string) (string, error) { | 
			
		||||
 | 
				    bytes, err := bcrypt.GenerateFromPassword([]byte(password), tapit.globalSettings.bcryptCost) | 
			
		||||
 | 
				    return string(bytes), err | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func checkPasswordHash(hash string, password string) bool { | 
			
		||||
 | 
				    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) | 
			
		||||
 | 
				    return err == nil | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) myselfHandler(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    if strings.ToUpper(r.Method) == "GET" { | 
			
		||||
 | 
				        tapit.checkUser(w, r) | 
			
		||||
 | 
				    } else if strings.ToUpper(r.Method) == "PUT" { | 
			
		||||
 | 
				        tapit.updateUser(w, r) | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        http.Error(w, "HTTP method not implemented", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) checkUser(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    var currSession Session | 
			
		||||
 | 
				    authCookie, err := r.Cookie("tapitsession") | 
			
		||||
 | 
				    if err!=nil { | 
			
		||||
 | 
				        http.Error(w, "Not authorised", 401) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    authCookieStr := authCookie.String()[13:] | 
			
		||||
 | 
				    tapit.db.Where(&Session{SessionID: authCookieStr}).First(&currSession) | 
			
		||||
 | 
				    if currSession.SessionID != authCookieStr { | 
			
		||||
 | 
				        http.Error(w, "Not authorised", 401) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        currUser := User{} | 
			
		||||
 | 
				        searchUser := User{} | 
			
		||||
 | 
				        searchUser.ID = currSession.UserID | 
			
		||||
 | 
				        tapit.db.Where(searchUser).First(&currUser) | 
			
		||||
 | 
				        currentUserJson := UserJson{} | 
			
		||||
 | 
				        currentUserJson.Username = currUser.Username | 
			
		||||
 | 
				        currentUserJson.Name = currUser.Name | 
			
		||||
 | 
				        currentUserJson.Email = currUser.Email | 
			
		||||
 | 
				        jsonResults, err := json.Marshal(currentUserJson) | 
			
		||||
 | 
				        if err != nil { | 
			
		||||
 | 
				            http.Error(w, err.Error(), 500) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } else { | 
			
		||||
 | 
				            w.Header().Set("Content-Type", "application/json") | 
			
		||||
 | 
				            w.Write(jsonResults) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) updateUser(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    var currSession Session | 
			
		||||
 | 
				    authCookie, err := r.Cookie("tapitsession") | 
			
		||||
 | 
				    if err!=nil { | 
			
		||||
 | 
				        http.Error(w, "Not authorised", 401) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    authCookieStr := authCookie.String()[13:] | 
			
		||||
 | 
				    tapit.db.Where(&Session{SessionID: authCookieStr}).First(&currSession) | 
			
		||||
 | 
				    if currSession.SessionID != authCookieStr { | 
			
		||||
 | 
				        http.Error(w, "Not authorised", 401) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        requestBody, err:= ioutil.ReadAll(r.Body) | 
			
		||||
 | 
				        if err != nil { | 
			
		||||
 | 
				            http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				        userJson := UserJson{} | 
			
		||||
 | 
				        err = json.Unmarshal(requestBody, &userJson) | 
			
		||||
 | 
				        if err != nil { | 
			
		||||
 | 
				            http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				        currUser := User{} | 
			
		||||
 | 
				        searchUser := User{} | 
			
		||||
 | 
				        searchUser.ID = currSession.UserID | 
			
		||||
 | 
				        tapit.db.Where(searchUser).First(&currUser) | 
			
		||||
 | 
				        if currUser.ID == currSession.UserID && currUser.Username == userJson.Username { | 
			
		||||
 | 
				            currUser.Name = userJson.Name | 
			
		||||
 | 
				            currUser.Email = userJson.Email | 
			
		||||
 | 
				            currUser.PasswordHash, _ = tapit.hashPassword(userJson.Password) | 
			
		||||
 | 
				            tapit.db.Save(&currUser) | 
			
		||||
 | 
				            userJson.Password = "" | 
			
		||||
 | 
				            // writing output
 | 
			
		||||
 | 
				            notifyPopup(w, r, "success", "Successfully changed profile!", userJson) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } else { | 
			
		||||
 | 
				            http.Error(w, "Not authorised", 401) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
@ -0,0 +1,540 @@ | 
			
		|||||
 | 
				package main | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				import ( | 
			
		||||
 | 
				    "github.com/jinzhu/gorm" | 
			
		||||
 | 
				    "github.com/gorilla/mux" | 
			
		||||
 | 
				    "sync" | 
			
		||||
 | 
				    "time" | 
			
		||||
 | 
				    "net/http" | 
			
		||||
 | 
				    "strings" | 
			
		||||
 | 
				    "encoding/json" | 
			
		||||
 | 
				    "io/ioutil" | 
			
		||||
 | 
				    "strconv" | 
			
		||||
 | 
				    "log" | 
			
		||||
 | 
				) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				type Campaign struct { | 
			
		||||
 | 
				    gorm.Model | 
			
		||||
 | 
				    Name string | 
			
		||||
 | 
				    FromNumber string | 
			
		||||
 | 
				    Size int | 
			
		||||
 | 
				    CurrentStatus string        // enum Running, Paused, Completed, Not Started
 | 
			
		||||
 | 
				    PhonebookId uint | 
			
		||||
 | 
				    TextTemplateId uint | 
			
		||||
 | 
				    WebTemplateId uint | 
			
		||||
 | 
				    ProviderTag string | 
			
		||||
 | 
				    Jobs []Job              `gorm:"foreignkey:CampaignId"` | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				type CampaignComms struct { | 
			
		||||
 | 
				    Campaign Campaign | 
			
		||||
 | 
				    Action string           // enum run, stop
 | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				type JobComms struct { | 
			
		||||
 | 
				    Job Job | 
			
		||||
 | 
				    Action string           // enum run, stop
 | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				type CampaignJson struct { | 
			
		||||
 | 
				    Id uint                     `json:"id"` | 
			
		||||
 | 
				    Name string                 `json:"name"` | 
			
		||||
 | 
				    FromNumber string           `json:"fromNumber"` | 
			
		||||
 | 
				    Size int                    `json:"size"` | 
			
		||||
 | 
				    CurrentStatus string        `json:"currentStatus"` | 
			
		||||
 | 
				    CreateDate time.Time        `json:"createDate"` | 
			
		||||
 | 
				    PhonebookId uint            `json:"phoneBookId"` | 
			
		||||
 | 
				    TextTemplateId uint         `json:"textTemplateId"` | 
			
		||||
 | 
				    WebTemplateId uint          `json:"webTemplateId"` | 
			
		||||
 | 
				    ProviderTag string          `json:"providerTag"` | 
			
		||||
 | 
				    Jobs []JobJson              `json:"jobs"` | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				type Job struct { | 
			
		||||
 | 
				    gorm.Model | 
			
		||||
 | 
				    CampaignId uint | 
			
		||||
 | 
				    CurrentStatus string        // enum Failed, Queued, Sent, Delivered, Not Started
 | 
			
		||||
 | 
				    TimeSent time.Time | 
			
		||||
 | 
				    ProviderTag string | 
			
		||||
 | 
				    AccSID string | 
			
		||||
 | 
				    AuthToken string | 
			
		||||
 | 
				    BodyText string | 
			
		||||
 | 
				    FromNum string | 
			
		||||
 | 
				    ToNum string | 
			
		||||
 | 
				    ResultStr string | 
			
		||||
 | 
				    MessageSid string | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				type JobJson struct { | 
			
		||||
 | 
				    Id uint                 `json:"id"` | 
			
		||||
 | 
				    CurrentStatus string    `json:"currentStatus"` | 
			
		||||
 | 
				    TimeSent time.Time      `json:"timeSent"` | 
			
		||||
 | 
				    FromNum string          `json:"fromNum"` | 
			
		||||
 | 
				    ToNum string            `json:"toNum"` | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				type TwilioMessageJson struct { | 
			
		||||
 | 
				    AccountSid string                   `json:"account_sid"` | 
			
		||||
 | 
				    ApiVersion string                   `json:"api_version"` | 
			
		||||
 | 
				    Body string                         `json:"body"` | 
			
		||||
 | 
				    DateCreated string                  `json:"date_created"` | 
			
		||||
 | 
				    DateSent string                     `json:"date_sent"` | 
			
		||||
 | 
				    DateUpdated string                  `json:"date_updated"` | 
			
		||||
 | 
				    Direction string                    `json:"direction"` | 
			
		||||
 | 
				    ErrorCode string                    `json:"error_code"` | 
			
		||||
 | 
				    ErrorMessage string                 `json:"error_message"` | 
			
		||||
 | 
				    From string                         `json:"from"` | 
			
		||||
 | 
				    MessagingServiceSid string          `json:"messaging_service_sid"` | 
			
		||||
 | 
				    NumMedia string                     `json:"num_media"` | 
			
		||||
 | 
				    NumSegments string                  `json:"num_segments"` | 
			
		||||
 | 
				    Price string                        `json:"price"` | 
			
		||||
 | 
				    PriceUnit string                    `json:"price_unit"` | 
			
		||||
 | 
				    Sid string                          `json:"sid"` | 
			
		||||
 | 
				    Status string                       `json:"status"` | 
			
		||||
 | 
				    SubResourceUri SubResourceUriJson   `json:"subresource_uris"` | 
			
		||||
 | 
				    To string                           `json:"to"` | 
			
		||||
 | 
				    Uri string                          `json:"uri"` | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				type SubResourceUriJson struct { | 
			
		||||
 | 
				    Media string                `json:"media"` | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) handleCampaign(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    if strings.ToUpper(r.Method) == "GET" { | 
			
		||||
 | 
				        tapit.getCampaigns(w, r) | 
			
		||||
 | 
				    } else if strings.ToUpper(r.Method) == "POST" { | 
			
		||||
 | 
				        tapit.createCampaign(w, r) | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        http.Error(w, "HTTP method not implemented", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) getCampaigns(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    var campaigns []Campaign | 
			
		||||
 | 
				    tapit.db.Find(&campaigns) | 
			
		||||
 | 
				    jsonResults, err := json.Marshal(campaignsToJson(campaigns)) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        http.Error(w, err.Error(), 500) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        w.Header().Set("Content-Type", "application/json") | 
			
		||||
 | 
				        w.Write(jsonResults) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func campaignsToJson(campaigns []Campaign) []CampaignJson { | 
			
		||||
 | 
				    var results []CampaignJson | 
			
		||||
 | 
				    for _, currCampaign := range campaigns { | 
			
		||||
 | 
				        var currJson CampaignJson | 
			
		||||
 | 
				        currJson.Id = currCampaign.ID | 
			
		||||
 | 
				        currJson.Name = currCampaign.Name | 
			
		||||
 | 
				        currJson.Size = currCampaign.Size | 
			
		||||
 | 
				        currJson.FromNumber = currCampaign.FromNumber | 
			
		||||
 | 
				        currJson.CurrentStatus = currCampaign.CurrentStatus | 
			
		||||
 | 
				        currJson.CreateDate = currCampaign.CreatedAt | 
			
		||||
 | 
				        currJson.PhonebookId = currCampaign.PhonebookId | 
			
		||||
 | 
				        currJson.TextTemplateId = currCampaign.TextTemplateId | 
			
		||||
 | 
				        currJson.WebTemplateId = currCampaign.WebTemplateId | 
			
		||||
 | 
				        currJson.ProviderTag = currCampaign.ProviderTag | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        results = append(results, currJson) | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    return results | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) createCampaign(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    requestBody, err:= ioutil.ReadAll(r.Body) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    var newCampaignJson CampaignJson | 
			
		||||
 | 
				    err = json.Unmarshal(requestBody, &newCampaignJson) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    if newCampaignJson.Name != "" { | 
			
		||||
 | 
				        var newCampaign Campaign | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        // populate details to be used later
 | 
			
		||||
 | 
				        var newRecords []PhoneRecord | 
			
		||||
 | 
				        var newTextTemplateBody string | 
			
		||||
 | 
				        var newAccSID string | 
			
		||||
 | 
				        var newAuthToken string | 
			
		||||
 | 
				        newRecords = tapit.getSpecificPhonebook(newCampaignJson.PhonebookId).Records | 
			
		||||
 | 
				        newTextTemplateBody = tapit.getSpecificTextBody(newCampaignJson.TextTemplateId) | 
			
		||||
 | 
				        if newCampaignJson.ProviderTag == "twilio" { | 
			
		||||
 | 
				            var twilioProvider TwilioProvider | 
			
		||||
 | 
				            tapit.db.Last(&twilioProvider) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				            newAccSID = twilioProvider.AccountSID | 
			
		||||
 | 
				            newAuthToken = twilioProvider.AuthToken | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        // update static details
 | 
			
		||||
 | 
				        newCampaign.Name = newCampaignJson.Name | 
			
		||||
 | 
				        newCampaign.Size = len(newRecords) | 
			
		||||
 | 
				        newCampaign.CurrentStatus = "Not Started" | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        newCampaign.FromNumber = newCampaignJson.FromNumber | 
			
		||||
 | 
				        newCampaign.PhonebookId = newCampaignJson.PhonebookId | 
			
		||||
 | 
				        newCampaign.TextTemplateId = newCampaignJson.TextTemplateId | 
			
		||||
 | 
				        newCampaign.WebTemplateId = newCampaignJson.WebTemplateId | 
			
		||||
 | 
				        newCampaign.ProviderTag = newCampaignJson.ProviderTag | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        // update records
 | 
			
		||||
 | 
				        for _, record := range newRecords { | 
			
		||||
 | 
				            var newJob Job | 
			
		||||
 | 
				            newJob.CurrentStatus = "Not Started" | 
			
		||||
 | 
				            newJob.ProviderTag = newCampaign.ProviderTag | 
			
		||||
 | 
				            newJob.AccSID = newAccSID | 
			
		||||
 | 
				            newJob.AuthToken = newAuthToken | 
			
		||||
 | 
				            newJob.FromNum = newCampaign.FromNumber | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				            // interpreting records
 | 
			
		||||
 | 
				            var newBodyText string | 
			
		||||
 | 
				            newJob.ToNum = record.PhoneNumber | 
			
		||||
 | 
				            newBodyText = newTextTemplateBody | 
			
		||||
 | 
				            newBodyText = strings.Replace(newBodyText, "{firstName}", record.FirstName, -1) | 
			
		||||
 | 
				            newBodyText = strings.Replace(newBodyText, "{lastName}", record.LastName, -1) | 
			
		||||
 | 
				            newBodyText = strings.Replace(newBodyText, "{alias}", record.Alias, -1) | 
			
		||||
 | 
				            newBodyText = strings.Replace(newBodyText, "{phoneNumber}", record.PhoneNumber, -1) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				            newJob.BodyText = newBodyText | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				            // saving it
 | 
			
		||||
 | 
				            newCampaign.Jobs = append(newCampaign.Jobs, newJob) | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        // update database
 | 
			
		||||
 | 
				        tapit.db.NewRecord(&newCampaign) | 
			
		||||
 | 
				        tapit.db.Create(&newCampaign) | 
			
		||||
 | 
				        if newCampaign.ID == 0 { | 
			
		||||
 | 
				            notifyPopup(w, r, "failure", "Failed to create campaign", nil) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				        newCampaignJson.Id = newCampaign.ID | 
			
		||||
 | 
				        newCampaignJson.CreateDate = newCampaign.CreatedAt | 
			
		||||
 | 
				        newCampaignJson.Size = newCampaign.Size | 
			
		||||
 | 
				        newCampaignJson.CurrentStatus = newCampaign.CurrentStatus | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        notifyPopup(w, r, "success", "Successfully added new campaign", newCampaignJson) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        notifyPopup(w, r, "failure", "Please enter the campaign name", nil) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) handleSpecificCampaign(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    if strings.ToUpper(r.Method) == "PUT" { | 
			
		||||
 | 
				        // not implmented -- complexity in changing campaign perimeters
 | 
			
		||||
 | 
				        // tapit.updateCampaign(w, r)
 | 
			
		||||
 | 
				        http.Error(w, "HTTP method not implemented", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } else if strings.ToUpper(r.Method) == "DELETE" { | 
			
		||||
 | 
				        tapit.deleteCampaign(w,r) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } else if strings.ToUpper(r.Method) == "GET" { | 
			
		||||
 | 
				        tapit.getCampaign(w,r) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        http.Error(w, "HTTP method not implemented", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) getSpecificCampaign(id uint) Campaign { | 
			
		||||
 | 
				    var campaign Campaign | 
			
		||||
 | 
				    var jobs []Job | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    var dbSearchCampaign Campaign | 
			
		||||
 | 
				    dbSearchCampaign.ID = id | 
			
		||||
 | 
				    tapit.db.Where(&dbSearchCampaign).First(&campaign) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    var dbSearchJob Job | 
			
		||||
 | 
				    dbSearchJob.CampaignId = id | 
			
		||||
 | 
				    tapit.db.Where(&dbSearchJob).Find(&jobs) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    campaign.Jobs = jobs | 
			
		||||
 | 
				    return campaign | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) getCampaign(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    vars := mux.Vars(r) | 
			
		||||
 | 
				    tempID, err := strconv.Atoi(vars["id"]) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    phonebook := tapit.getSpecificCampaign(uint(tempID)) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    jsonResults, err := json.Marshal(campaignToJson(phonebook)) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        http.Error(w, err.Error(), 500) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        w.Header().Set("Content-Type", "application/json") | 
			
		||||
 | 
				        w.Write(jsonResults) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func campaignToJson(campaign Campaign) CampaignJson { | 
			
		||||
 | 
				    var cJson CampaignJson | 
			
		||||
 | 
				    cJson.Id = campaign.ID | 
			
		||||
 | 
				    cJson.Name = campaign.Name | 
			
		||||
 | 
				    cJson.FromNumber = campaign.FromNumber | 
			
		||||
 | 
				    cJson.Size = campaign.Size | 
			
		||||
 | 
				    cJson.CurrentStatus = campaign.CurrentStatus | 
			
		||||
 | 
				    cJson.PhonebookId = campaign.PhonebookId | 
			
		||||
 | 
				    cJson.TextTemplateId = campaign.TextTemplateId | 
			
		||||
 | 
				    cJson.WebTemplateId = campaign.WebTemplateId | 
			
		||||
 | 
				    cJson.ProviderTag = campaign.ProviderTag | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // iterating jobs
 | 
			
		||||
 | 
				    for _, job := range campaign.Jobs { | 
			
		||||
 | 
				        var currJson JobJson | 
			
		||||
 | 
				        currJson.CurrentStatus = job.CurrentStatus | 
			
		||||
 | 
				        currJson.TimeSent = job.TimeSent | 
			
		||||
 | 
				        currJson.FromNum = job.FromNum | 
			
		||||
 | 
				        currJson.ToNum = job.ToNum | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        cJson.Jobs = append(cJson.Jobs, currJson) | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    return cJson | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) deleteCampaign(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    vars := mux.Vars(r) | 
			
		||||
 | 
				    tempID, err := strconv.Atoi(vars["id"]) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // start working
 | 
			
		||||
 | 
				    var campaign Campaign | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // get phonebook
 | 
			
		||||
 | 
				    var dbSearchCampaign Campaign | 
			
		||||
 | 
				    dbSearchCampaign.ID = uint(tempID) | 
			
		||||
 | 
				    tapit.db.Where(&dbSearchCampaign).First(&campaign) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    if campaign.ID == uint(tempID) { | 
			
		||||
 | 
				        // finally delete it
 | 
			
		||||
 | 
				        tapit.db.Delete(&campaign) | 
			
		||||
 | 
				        notifyPopup(w, r, "success", "Successfully deleted campaign", nil) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) handleStartCampaign(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    if strings.ToUpper(r.Method) == "GET" { | 
			
		||||
 | 
				        tapit.startCampaign(w,r) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        http.Error(w, "HTTP method not implemented", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) handleStopCampaign(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    if strings.ToUpper(r.Method) == "GET" { | 
			
		||||
 | 
				        tapit.stopCampaign(w,r) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        http.Error(w, "HTTP method not implemented", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) startCampaign(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    vars := mux.Vars(r) | 
			
		||||
 | 
				    tempID, err := strconv.Atoi(vars["id"]) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // start working
 | 
			
		||||
 | 
				    var campaign Campaign | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    campaign = tapit.getSpecificCampaign(uint(tempID)) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    if campaign.ID == uint(tempID) && campaign.CurrentStatus != "Running" && campaign.CurrentStatus != "Completed" { | 
			
		||||
 | 
				        // finally start new thread and start working
 | 
			
		||||
 | 
				        go tapit.workerCampaign(campaign) | 
			
		||||
 | 
				        campaign.CurrentStatus = "Running" | 
			
		||||
 | 
				        tapit.db.Save(&campaign) | 
			
		||||
 | 
				        jsonResults := campaignToJson(campaign) | 
			
		||||
 | 
				        if err != nil { | 
			
		||||
 | 
				            http.Error(w, err.Error(), 500) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } else { | 
			
		||||
 | 
				            notifyPopup(w, r, "success", "Started campaign", jsonResults) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) stopCampaign(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    vars := mux.Vars(r) | 
			
		||||
 | 
				    tempID, err := strconv.Atoi(vars["id"]) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // start working
 | 
			
		||||
 | 
				    var campaign Campaign | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    campaign = tapit.getSpecificCampaign(uint(tempID)) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    if campaign.ID == uint(tempID) && campaign.CurrentStatus == "Running" { | 
			
		||||
 | 
				        var campaignComms CampaignComms | 
			
		||||
 | 
				        campaignComms.Action = "stop" | 
			
		||||
 | 
				        campaignComms.Campaign = campaign | 
			
		||||
 | 
				        tapit.campaignChan <- campaignComms | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        // notify
 | 
			
		||||
 | 
				        notifyPopup(w, r, "success", "Paused campaign", nil) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) workerCampaign(campaign Campaign) { | 
			
		||||
 | 
				    var campaignComms CampaignComms | 
			
		||||
 | 
				    var jobChan chan JobComms | 
			
		||||
 | 
				    var wg sync.WaitGroup | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    jobChan = make(chan JobComms, 1) | 
			
		||||
 | 
				    for i:=0; i<tapit.globalSettings.threadsPerCampaign; i++ { | 
			
		||||
 | 
				        wg.Add(1) | 
			
		||||
 | 
				        go tapit.workerJob(jobChan, &wg) | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    for _, job := range campaign.Jobs { | 
			
		||||
 | 
				        select { | 
			
		||||
 | 
				            case campaignComms = <-tapit.campaignChan: | 
			
		||||
 | 
				                if campaignComms.Campaign.ID == campaign.ID { | 
			
		||||
 | 
				                    if campaignComms.Action == "stop" { | 
			
		||||
 | 
				                        // kill all
 | 
			
		||||
 | 
				                        for i:=0; i<tapit.globalSettings.threadsPerCampaign; i++ { | 
			
		||||
 | 
				                            var stopComms JobComms | 
			
		||||
 | 
				                            stopComms.Action = "stop" | 
			
		||||
 | 
				                            jobChan <- stopComms | 
			
		||||
 | 
				                        } | 
			
		||||
 | 
				                        // wait to end
 | 
			
		||||
 | 
				                        wg.Wait() | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				                        // get updated campaign
 | 
			
		||||
 | 
				                        var newCampaign Campaign | 
			
		||||
 | 
				                        var searchCampaign Campaign | 
			
		||||
 | 
				                        searchCampaign.ID = campaign.ID | 
			
		||||
 | 
				                        tapit.db.Where(&searchCampaign).First(&newCampaign) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				                        // update campaign
 | 
			
		||||
 | 
				                        newCampaign.CurrentStatus = "Paused" | 
			
		||||
 | 
				                        tapit.db.Save(&newCampaign) | 
			
		||||
 | 
				                        return | 
			
		||||
 | 
				                    } | 
			
		||||
 | 
				                } else { | 
			
		||||
 | 
				                    // not mine -- throw it back
 | 
			
		||||
 | 
				                    tapit.campaignChan<- campaignComms | 
			
		||||
 | 
				                } | 
			
		||||
 | 
				            default: | 
			
		||||
 | 
				                if job.CurrentStatus == "Not Started" { | 
			
		||||
 | 
				                    var workComms JobComms | 
			
		||||
 | 
				                    workComms.Action = "run" | 
			
		||||
 | 
				                    workComms.Job = job | 
			
		||||
 | 
				                    jobChan <- workComms | 
			
		||||
 | 
				                } | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    for i:=0; i<tapit.globalSettings.threadsPerCampaign; i++ { | 
			
		||||
 | 
				        var stopComms JobComms | 
			
		||||
 | 
				        stopComms.Action = "stop" | 
			
		||||
 | 
				        jobChan <- stopComms | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // wait to end
 | 
			
		||||
 | 
				    wg.Wait() | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // get updated campaign
 | 
			
		||||
 | 
				    var newCampaign Campaign | 
			
		||||
 | 
				    var searchCampaign Campaign | 
			
		||||
 | 
				    searchCampaign.ID = campaign.ID | 
			
		||||
 | 
				    tapit.db.Where(&searchCampaign).First(&newCampaign) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // update campaign
 | 
			
		||||
 | 
				    newCampaign.CurrentStatus = "Completed" | 
			
		||||
 | 
				    tapit.db.Save(&newCampaign) | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) workerJob(jobChan chan JobComms, wg *sync.WaitGroup) { | 
			
		||||
 | 
				    var currentJob JobComms | 
			
		||||
 | 
				    exitCode := false | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    for !exitCode { | 
			
		||||
 | 
				        currentJob = <-jobChan | 
			
		||||
 | 
				        if currentJob.Action != "stop" { | 
			
		||||
 | 
				            if currentJob.Job.ProviderTag == "twilio" { | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				                var resultJson []byte | 
			
		||||
 | 
				                resultJson = tapit.twilioSend(currentJob.Job.AccSID, currentJob.Job.AuthToken, currentJob.Job.BodyText, currentJob.Job.FromNum, currentJob.Job.ToNum) | 
			
		||||
 | 
				                currentJob.Job.ResultStr = string(resultJson) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				                var twilioResult TwilioMessageJson | 
			
		||||
 | 
				                err := json.Unmarshal(resultJson, &twilioResult) | 
			
		||||
 | 
				                if err != nil { | 
			
		||||
 | 
				                    log.Println(err) | 
			
		||||
 | 
				                    currentJob.Job.CurrentStatus = "Failed" | 
			
		||||
 | 
				                } else if twilioResult.Status == "queued" { | 
			
		||||
 | 
				                    currentJob.Job.MessageSid = twilioResult.Sid | 
			
		||||
 | 
				                    currentJob.Job.CurrentStatus = "Queued" | 
			
		||||
 | 
				                } else if twilioResult.Status == "delivered" { | 
			
		||||
 | 
				                    currentJob.Job.MessageSid = twilioResult.Sid | 
			
		||||
 | 
				                    currentJob.Job.CurrentStatus = "Delivered" | 
			
		||||
 | 
				                } else { | 
			
		||||
 | 
				                    currentJob.Job.CurrentStatus = "Failed" | 
			
		||||
 | 
				                } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				                // redo until done
 | 
			
		||||
 | 
				                tapit.db.Save(¤tJob.Job) | 
			
		||||
 | 
				            } | 
			
		||||
 | 
				        } else { | 
			
		||||
 | 
				            exitCode = true | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    wg.Done() | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) clearRunningCampaigns() { | 
			
		||||
 | 
				    var campaigns []Campaign | 
			
		||||
 | 
				    var searchCampaign Campaign | 
			
		||||
 | 
				    searchCampaign.CurrentStatus = "Running" | 
			
		||||
 | 
				    tapit.db.Where(&searchCampaign).Find(&campaigns) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    for _, campaign := range campaigns { | 
			
		||||
 | 
				        campaign.CurrentStatus = "Paused" | 
			
		||||
 | 
				        tapit.db.Save(&campaign) | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
@ -0,0 +1,143 @@ | 
			
		|||||
 | 
				package main | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				import ( | 
			
		||||
 | 
				    "github.com/jinzhu/gorm" | 
			
		||||
 | 
				    _ "github.com/jinzhu/gorm/dialects/postgres" | 
			
		||||
 | 
				    "log" | 
			
		||||
 | 
				    "github.com/gorilla/mux" | 
			
		||||
 | 
				    "io/ioutil" | 
			
		||||
 | 
				    "net/http" | 
			
		||||
 | 
				    "os" | 
			
		||||
 | 
				    "path/filepath" | 
			
		||||
 | 
				) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				type Tapit struct { | 
			
		||||
 | 
				    db *gorm.DB | 
			
		||||
 | 
				    globalSettings GlobalSettings | 
			
		||||
 | 
				    campaignChan chan CampaignComms | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				type GlobalSettings struct { | 
			
		||||
 | 
				    secretRegistrationCode string | 
			
		||||
 | 
				    threadsPerCampaign int | 
			
		||||
 | 
				    bcryptCost int | 
			
		||||
 | 
				    maxRequestRetries int | 
			
		||||
 | 
				    waitBeforeRetry int | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func generateFileHandler(path string) func(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    return func(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				        r.Header.Add("Cache-Control", "private, max-age=604800") // 7 days
 | 
			
		||||
 | 
				        //r.Header.Add("Cache-Control", "private, max-age=1") // 1 sec -- debug
 | 
			
		||||
 | 
				        http.ServeFile(w, r, path) | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func iterateStatic(r *mux.Router, path string, startWebPath string) { | 
			
		||||
 | 
				    files, err := ioutil.ReadDir(path) | 
			
		||||
 | 
				    if err!=nil { | 
			
		||||
 | 
				        log.Fatal(err) | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    for _, f := range files { | 
			
		||||
 | 
				        if !f.IsDir() && f.Name()[0] != '.' { | 
			
		||||
 | 
				            r.HandleFunc(startWebPath + f.Name(), generateFileHandler(path+"/"+f.Name())) | 
			
		||||
 | 
				            log.Println(startWebPath + f.Name()+" added to path") | 
			
		||||
 | 
				        } else if f.IsDir() && f.Name()[0] != '.' { | 
			
		||||
 | 
				            iterateStatic(r, path + "/" + string(f.Name()), startWebPath + string(f.Name() + "/")) | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func generateRoutes(r *mux.Router, indexPath string, routes []string) { | 
			
		||||
 | 
				    for _, route := range routes { | 
			
		||||
 | 
				        r.HandleFunc(route, generateFileHandler(indexPath)) | 
			
		||||
 | 
				        log.Println(route+" added as route") | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func main() { | 
			
		||||
 | 
				    // Setting up DB
 | 
			
		||||
 | 
				    host := "postgres-tapit" | 
			
		||||
 | 
				    db, err := gorm.Open("postgres", "sslmode=disable host=" + host + " port=5432 user=tapit dbname=tapit password=secret-tapit-password") | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        log.Fatal(err) | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    defer db.Close() | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // DB Migrations
 | 
			
		||||
 | 
				    db.AutoMigrate(&Session{}) | 
			
		||||
 | 
				    db.AutoMigrate(&User{}) | 
			
		||||
 | 
				    db.AutoMigrate(&TextTemplate{}) | 
			
		||||
 | 
				    db.AutoMigrate(&TwilioProvider{}) | 
			
		||||
 | 
				    db.AutoMigrate(&Phonebook{}) | 
			
		||||
 | 
				    db.AutoMigrate(&PhoneRecord{}) | 
			
		||||
 | 
				    db.AutoMigrate(&Campaign{}) | 
			
		||||
 | 
				    db.AutoMigrate(&Job{}) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // Setting up Tapit app
 | 
			
		||||
 | 
				    var tapit Tapit | 
			
		||||
 | 
				    tapit.db = db | 
			
		||||
 | 
				    tapit.globalSettings.secretRegistrationCode = "Super-Secret-Code" | 
			
		||||
 | 
				    tapit.globalSettings.threadsPerCampaign = 2 | 
			
		||||
 | 
				    tapit.globalSettings.bcryptCost = 12 | 
			
		||||
 | 
				    tapit.globalSettings.maxRequestRetries = 5 | 
			
		||||
 | 
				    tapit.globalSettings.waitBeforeRetry = 1000 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // Clear running campaigns & starting background jobs
 | 
			
		||||
 | 
				    tapit.clearRunningCampaigns() | 
			
		||||
 | 
				    go tapit.workerTwilioChecker() | 
			
		||||
 | 
				    tapit.campaignChan = make(chan CampaignComms, 10) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // Setting up mux
 | 
			
		||||
 | 
				    r := mux.NewRouter() | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // Get current dir
 | 
			
		||||
 | 
				    dir, err := filepath.Abs(filepath.Dir(os.Args[0])) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				            log.Fatal(err) | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // Setting up static routes (frontend)
 | 
			
		||||
 | 
				    iterateStatic(r, dir + "/static/", "/") | 
			
		||||
 | 
				    routes := []string{ | 
			
		||||
 | 
				        "/", | 
			
		||||
 | 
				        "/login", | 
			
		||||
 | 
				        "/register", | 
			
		||||
 | 
				        "/profile", | 
			
		||||
 | 
				        "/campaign", | 
			
		||||
 | 
				        "/campaign/new", | 
			
		||||
 | 
				        "/campaign/{id}/view", | 
			
		||||
 | 
				        "/phonebook", | 
			
		||||
 | 
				        "/phonebook/new", | 
			
		||||
 | 
				        "/phonebook/{id}/edit", | 
			
		||||
 | 
				        "/text-template", | 
			
		||||
 | 
				        "/text-template/new", | 
			
		||||
 | 
				        "/text-template/{id}/edit", | 
			
		||||
 | 
				        "/provider", | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    indexPath := dir + "/static/index.html" | 
			
		||||
 | 
				    generateRoutes(r, indexPath, routes) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // Setting up API routes
 | 
			
		||||
 | 
				    r.HandleFunc("/api/login", tapit.login) | 
			
		||||
 | 
				    r.HandleFunc("/api/logout", tapit.logout) | 
			
		||||
 | 
				    r.HandleFunc("/api/register", tapit.register) | 
			
		||||
 | 
				    r.HandleFunc("/api/myself", tapit.authenticationHandler(tapit.myselfHandler)) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    r.Handle("/api/text-template",tapit.authenticationHandler(tapit.handleTextTemplate)) | 
			
		||||
 | 
				    r.Handle("/api/text-template/{id}",tapit.authenticationHandler(tapit.handleSpecificTextTemplate)) | 
			
		||||
 | 
				    r.Handle("/api/provider/twilio",tapit.authenticationHandler(tapit.handleTwilioProvider)) | 
			
		||||
 | 
				    r.Handle("/api/phonebook",tapit.authenticationHandler(tapit.handlePhonebook)) | 
			
		||||
 | 
				    r.Handle("/api/phonebook/{id}",tapit.authenticationHandler(tapit.handleSpecificPhonebook)) | 
			
		||||
 | 
				    r.Handle("/api/import-phonebook",tapit.authenticationHandler(tapit.importPhonebook)) | 
			
		||||
 | 
				    r.Handle("/api/campaign",tapit.authenticationHandler(tapit.handleCampaign)) | 
			
		||||
 | 
				    r.Handle("/api/campaign/{id}",tapit.authenticationHandler(tapit.handleSpecificCampaign)) | 
			
		||||
 | 
				    r.Handle("/api/campaign/{id}/start",tapit.authenticationHandler(tapit.handleStartCampaign)) | 
			
		||||
 | 
				    r.Handle("/api/campaign/{id}/pause",tapit.authenticationHandler(tapit.handleStopCampaign)) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // Starting web server
 | 
			
		||||
 | 
				    http.Handle("/", r) | 
			
		||||
 | 
				    log.Println("Starting web server...") | 
			
		||||
 | 
				    http.ListenAndServe(":8000", nil) | 
			
		||||
 | 
				} | 
			
		||||
@ -0,0 +1,30 @@ | 
			
		|||||
 | 
				package main | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				import ( | 
			
		||||
 | 
				    "encoding/json" | 
			
		||||
 | 
				    "net/http" | 
			
		||||
 | 
				) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				type NotificationJson struct { | 
			
		||||
 | 
				    ResultType string           `json:"resultType"`           // success/failure/info
 | 
			
		||||
 | 
				    Text string                 `json:"text"` | 
			
		||||
 | 
				    Payload interface{}         `json:"payload"` | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				type Payload interface{} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func notifyPopup(w http.ResponseWriter, r *http.Request, resultType string, text string, payload Payload) { | 
			
		||||
 | 
				    messageOutput := NotificationJson{ | 
			
		||||
 | 
				        ResultType:   resultType, | 
			
		||||
 | 
				        Text: text, | 
			
		||||
 | 
				        Payload: payload, | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				    jsonResults, err := json.Marshal(messageOutput) | 
			
		||||
 | 
				    if err!=nil { | 
			
		||||
 | 
				        http.Error(w, "Internal server error", 500) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    w.Header().Set("Content-Type", "application/json") | 
			
		||||
 | 
				    http.Error(w, string(jsonResults), 200) | 
			
		||||
 | 
				    return | 
			
		||||
 | 
				} | 
			
		||||
@ -0,0 +1,342 @@ | 
			
		|||||
 | 
				package main | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				import ( | 
			
		||||
 | 
				    "github.com/jinzhu/gorm" | 
			
		||||
 | 
				    "github.com/gorilla/mux" | 
			
		||||
 | 
				    "github.com/tealeg/xlsx" | 
			
		||||
 | 
				    "time" | 
			
		||||
 | 
				    "net/http" | 
			
		||||
 | 
				    "strings" | 
			
		||||
 | 
				    "encoding/json" | 
			
		||||
 | 
				    "io/ioutil" | 
			
		||||
 | 
				    "strconv" | 
			
		||||
 | 
				    "log" | 
			
		||||
 | 
				    "io" | 
			
		||||
 | 
				    "bytes" | 
			
		||||
 | 
				) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				type Phonebook struct { | 
			
		||||
 | 
				    gorm.Model | 
			
		||||
 | 
				    Name string | 
			
		||||
 | 
				    Size int | 
			
		||||
 | 
				    Records []PhoneRecord           `gorm:"foreignkey:PhonebookID"` | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				type PhonebookJson struct { | 
			
		||||
 | 
				    Id uint                          `json:"id"` | 
			
		||||
 | 
				    Name string                     `json:"name"` | 
			
		||||
 | 
				    Size int                        `json:"size"` | 
			
		||||
 | 
				    CreateDate time.Time            `json:"createDate"` | 
			
		||||
 | 
				    Records []PhoneRecordJson       `json:"records"` | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				type PhoneRecord struct { | 
			
		||||
 | 
				    gorm.Model | 
			
		||||
 | 
				    PhonebookID uint | 
			
		||||
 | 
				    FirstName string | 
			
		||||
 | 
				    LastName string | 
			
		||||
 | 
				    Alias string | 
			
		||||
 | 
				    PhoneNumber string | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				type PhoneRecordJson struct { | 
			
		||||
 | 
				    Id uint                         `json:"id"` | 
			
		||||
 | 
				    FirstName string                `json:"firstName"` | 
			
		||||
 | 
				    LastName string                 `json:"lastName"` | 
			
		||||
 | 
				    Alias string                    `json:"alias"` | 
			
		||||
 | 
				    PhoneNumber string              `json:"phoneNumber"` | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) handlePhonebook(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    if strings.ToUpper(r.Method) == "GET" { | 
			
		||||
 | 
				        tapit.getPhonebooks(w, r) | 
			
		||||
 | 
				    } else if strings.ToUpper(r.Method) == "POST" { | 
			
		||||
 | 
				        tapit.createPhonebook(w, r) | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        http.Error(w, "HTTP method not implemented", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) getPhonebooks(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    var phonebooks []Phonebook | 
			
		||||
 | 
				    tapit.db.Find(&phonebooks) | 
			
		||||
 | 
				    jsonResults, err := json.Marshal(phonebooksToJson(phonebooks)) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        http.Error(w, err.Error(), 500) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        w.Header().Set("Content-Type", "application/json") | 
			
		||||
 | 
				        w.Write(jsonResults) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func phonebooksToJson(pb []Phonebook) []PhonebookJson { | 
			
		||||
 | 
				    var pbJson []PhonebookJson | 
			
		||||
 | 
				    for _, currObj := range pb { | 
			
		||||
 | 
				        var currPbJson PhonebookJson | 
			
		||||
 | 
				        currPbJson.Id = currObj.ID | 
			
		||||
 | 
				        currPbJson.Name = currObj.Name | 
			
		||||
 | 
				        currPbJson.CreateDate = currObj.CreatedAt | 
			
		||||
 | 
				        currPbJson.Size = currObj.Size | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        pbJson = append(pbJson, currPbJson) | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    return pbJson | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) createPhonebook(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    requestBody, err:= ioutil.ReadAll(r.Body) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    var newPhonebookJson PhonebookJson | 
			
		||||
 | 
				    err = json.Unmarshal(requestBody, &newPhonebookJson) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    if newPhonebookJson.Name != "" { | 
			
		||||
 | 
				        var newPhonebook Phonebook | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        // update name & size
 | 
			
		||||
 | 
				        newPhonebook.Name = newPhonebookJson.Name | 
			
		||||
 | 
				        newPhonebook.Size = len(newPhonebookJson.Records) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        // update records
 | 
			
		||||
 | 
				        for _, record := range newPhonebookJson.Records { | 
			
		||||
 | 
				            var newRecord PhoneRecord | 
			
		||||
 | 
				            newRecord.FirstName = record.FirstName | 
			
		||||
 | 
				            newRecord.LastName = record.LastName | 
			
		||||
 | 
				            newRecord.Alias = record.Alias | 
			
		||||
 | 
				            newRecord.PhoneNumber = record.PhoneNumber | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				            newPhonebook.Records = append(newPhonebook.Records, newRecord) | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        // update database
 | 
			
		||||
 | 
				        tapit.db.NewRecord(&newPhonebook) | 
			
		||||
 | 
				        tapit.db.Create(&newPhonebook) | 
			
		||||
 | 
				        if newPhonebook.ID == 0 { | 
			
		||||
 | 
				            notifyPopup(w, r, "failure", "Failed to create phonebook", nil) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				        newPhonebookJson.Id = newPhonebook.ID | 
			
		||||
 | 
				        newPhonebookJson.CreateDate = newPhonebook.CreatedAt | 
			
		||||
 | 
				        newPhonebookJson.Size = newPhonebook.Size | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        notifyPopup(w, r, "success", "Successfully added new phonebook", newPhonebookJson) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        notifyPopup(w, r, "failure", "Please enter the phonebook name", nil) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) getSpecificPhonebook(id uint) Phonebook { | 
			
		||||
 | 
				    var phonebook Phonebook | 
			
		||||
 | 
				    var records []PhoneRecord | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    var dbPhonebookSearch Phonebook | 
			
		||||
 | 
				    dbPhonebookSearch.ID = id | 
			
		||||
 | 
				    tapit.db.Where(&dbPhonebookSearch).First(&phonebook) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    var dbSearchPhoneRecord PhoneRecord | 
			
		||||
 | 
				    dbSearchPhoneRecord.PhonebookID = id | 
			
		||||
 | 
				    tapit.db.Where(&dbSearchPhoneRecord).Find(&records) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    phonebook.Records = records | 
			
		||||
 | 
				    return phonebook | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) getPhonebook(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    vars := mux.Vars(r) | 
			
		||||
 | 
				    tempID, err := strconv.Atoi(vars["id"]) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    phonebook := tapit.getSpecificPhonebook(uint(tempID)) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    jsonResults, err := json.Marshal(phonebookToJson(phonebook)) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        http.Error(w, err.Error(), 500) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        w.Header().Set("Content-Type", "application/json") | 
			
		||||
 | 
				        w.Write(jsonResults) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func phonebookToJson(pb Phonebook) PhonebookJson { | 
			
		||||
 | 
				    var pbJson PhonebookJson | 
			
		||||
 | 
				    pbJson.Id = pb.ID | 
			
		||||
 | 
				    pbJson.Name = pb.Name | 
			
		||||
 | 
				    pbJson.CreateDate = pb.CreatedAt | 
			
		||||
 | 
				    pbJson.Size = pb.Size | 
			
		||||
 | 
				    for _, record := range pb.Records { | 
			
		||||
 | 
				        var recordJson PhoneRecordJson | 
			
		||||
 | 
				        recordJson.Id = record.ID | 
			
		||||
 | 
				        recordJson.FirstName = record.FirstName | 
			
		||||
 | 
				        recordJson.LastName = record.LastName | 
			
		||||
 | 
				        recordJson.Alias = record.Alias | 
			
		||||
 | 
				        recordJson.PhoneNumber = record.PhoneNumber | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        pbJson.Records = append(pbJson.Records, recordJson) | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    return pbJson | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) handleSpecificPhonebook(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    if strings.ToUpper(r.Method) == "PUT" { | 
			
		||||
 | 
				        tapit.updatePhonebook(w, r) | 
			
		||||
 | 
				    } else if strings.ToUpper(r.Method) == "DELETE" { | 
			
		||||
 | 
				        tapit.deletePhonebook(w,r) | 
			
		||||
 | 
				    } else if strings.ToUpper(r.Method) == "GET" { | 
			
		||||
 | 
				        tapit.getPhonebook(w,r) | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        http.Error(w, "HTTP method not implemented", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) updatePhonebook(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    requestBody, err:= ioutil.ReadAll(r.Body) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    var newPhonebookJson PhonebookJson | 
			
		||||
 | 
				    err = json.Unmarshal(requestBody, &newPhonebookJson) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    if newPhonebookJson.Name != "" { | 
			
		||||
 | 
				        var newPhonebook Phonebook | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        // get current phonebook
 | 
			
		||||
 | 
				        var dbSearchPhonebook Phonebook | 
			
		||||
 | 
				        tapit.db.Where(&dbSearchPhonebook).First(&newPhonebook) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        // update name & size
 | 
			
		||||
 | 
				        newPhonebook.Name = newPhonebookJson.Name | 
			
		||||
 | 
				        newPhonebook.Size = len(newPhonebookJson.Records) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        // update records
 | 
			
		||||
 | 
				        for _, record := range newPhonebookJson.Records { | 
			
		||||
 | 
				            var newRecord PhoneRecord | 
			
		||||
 | 
				            newRecord.FirstName = record.FirstName | 
			
		||||
 | 
				            newRecord.LastName = record.LastName | 
			
		||||
 | 
				            newRecord.Alias = record.Alias | 
			
		||||
 | 
				            newRecord.PhoneNumber = record.PhoneNumber | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				            newPhonebook.Records = append(newPhonebook.Records, newRecord) | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        // update database
 | 
			
		||||
 | 
				        tapit.db.Save(&newPhonebook) | 
			
		||||
 | 
				        if newPhonebook.ID == 0 { | 
			
		||||
 | 
				            notifyPopup(w, r, "failure", "Failed to create phonebook", nil) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				        newPhonebookJson.Id = newPhonebook.ID | 
			
		||||
 | 
				        newPhonebookJson.CreateDate = newPhonebook.CreatedAt | 
			
		||||
 | 
				        newPhonebookJson.Size = newPhonebook.Size | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        notifyPopup(w, r, "success", "Successfully added new phonebook", newPhonebookJson) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        notifyPopup(w, r, "failure", "Please enter the phonebook name", nil) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) deletePhonebook(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    vars := mux.Vars(r) | 
			
		||||
 | 
				    tempID, err := strconv.Atoi(vars["id"]) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // start working
 | 
			
		||||
 | 
				    var phonebook Phonebook | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // get phonebook
 | 
			
		||||
 | 
				    var dbSearchPhonebook Phonebook | 
			
		||||
 | 
				    dbSearchPhonebook.ID = uint(tempID) | 
			
		||||
 | 
				    tapit.db.Where(&dbSearchPhonebook).First(&phonebook) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    if phonebook.ID == uint(tempID) { | 
			
		||||
 | 
				        // finally delete it
 | 
			
		||||
 | 
				        tapit.db.Delete(&phonebook) | 
			
		||||
 | 
				        notifyPopup(w, r, "success", "Successfully deleted phonebook", nil) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) importPhonebook(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    var records []PhoneRecordJson | 
			
		||||
 | 
				    err := r.ParseForm() | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        log.Println(err) | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // 100 M reserved
 | 
			
		||||
 | 
				    err = r.ParseMultipartForm(100000000) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        log.Println(err) | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    importFile, _, err := r.FormFile("phonebookFile") | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        log.Println(err) | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    var buff bytes.Buffer | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // use buffer to bytes
 | 
			
		||||
 | 
				    io.Copy(&buff, importFile) | 
			
		||||
 | 
				    fileBytes := buff.Bytes() | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    excelFile, err := xlsx.OpenBinary(fileBytes) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        log.Println(err) | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    for num, row := range excelFile.Sheet["import"].Rows { | 
			
		||||
 | 
				        if num != 0 { | 
			
		||||
 | 
				            var tempRecord PhoneRecordJson | 
			
		||||
 | 
				            tempRecord.FirstName = row.Cells[0].Value | 
			
		||||
 | 
				            tempRecord.LastName = row.Cells[1].Value | 
			
		||||
 | 
				            tempRecord.Alias = row.Cells[2].Value | 
			
		||||
 | 
				            tempRecord.PhoneNumber = row.Cells[3].Value | 
			
		||||
 | 
				            records = append(records, tempRecord) | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    jsonResults, err := json.Marshal(records) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        http.Error(w, err.Error(), 500) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        w.Header().Set("Content-Type", "application/json") | 
			
		||||
 | 
				        w.Write(jsonResults) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
@ -0,0 +1,240 @@ | 
			
		|||||
 | 
				package main | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				import ( | 
			
		||||
 | 
				    "github.com/jinzhu/gorm" | 
			
		||||
 | 
				    "github.com/gorilla/mux" | 
			
		||||
 | 
				    "time" | 
			
		||||
 | 
				    "net/http" | 
			
		||||
 | 
				    "strings" | 
			
		||||
 | 
				    "encoding/json" | 
			
		||||
 | 
				    "io/ioutil" | 
			
		||||
 | 
				    "strconv" | 
			
		||||
 | 
				) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				type TextTemplate struct { | 
			
		||||
 | 
				    gorm.Model | 
			
		||||
 | 
				    Name string | 
			
		||||
 | 
				    TemplateStr string | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				type TextTemplateJson struct { | 
			
		||||
 | 
				    Id int                          `json:"id"` | 
			
		||||
 | 
				    Name string                     `json:"name"` | 
			
		||||
 | 
				    TemplateStr string              `json:"templateStr"` | 
			
		||||
 | 
				    CreateDate time.Time            `json:"createDate"` | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) handleTextTemplate(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    if strings.ToUpper(r.Method) == "GET" { | 
			
		||||
 | 
				        tapit.getTextTemplates(w, r) | 
			
		||||
 | 
				    } else if strings.ToUpper(r.Method) == "POST" { | 
			
		||||
 | 
				        tapit.createTextTemplate(w, r) | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        http.Error(w, "HTTP method not implemented", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) getTextTemplates(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    textTemplates := []TextTemplate{} | 
			
		||||
 | 
				    tapit.db.Find(&textTemplates) | 
			
		||||
 | 
				    jsonResults, err := json.Marshal(textTemplatesToJson(textTemplates)) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        http.Error(w, err.Error(), 500) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        w.Header().Set("Content-Type", "application/json") | 
			
		||||
 | 
				        w.Write(jsonResults) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func textTemplatesToJson(textTemplates []TextTemplate) []TextTemplateJson { | 
			
		||||
 | 
				    textTemplateJson := make([]TextTemplateJson, 0) | 
			
		||||
 | 
				    for _, textTemplate := range textTemplates { | 
			
		||||
 | 
				        var currentTextTemplateJson TextTemplateJson | 
			
		||||
 | 
				        currentTextTemplateJson.Id = int(textTemplate.ID) | 
			
		||||
 | 
				        currentTextTemplateJson.Name = textTemplate.Name | 
			
		||||
 | 
				        currentTextTemplateJson.TemplateStr = textTemplate.TemplateStr | 
			
		||||
 | 
				        currentTextTemplateJson.CreateDate = textTemplate.CreatedAt | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        textTemplateJson = append(textTemplateJson, currentTextTemplateJson) | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    return textTemplateJson | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func jsonToTextTemplate(textTemplateJson TextTemplateJson) TextTemplate { | 
			
		||||
 | 
				    var resultTextTemplate TextTemplate | 
			
		||||
 | 
				    resultTextTemplate.Name = textTemplateJson.Name | 
			
		||||
 | 
				    resultTextTemplate.TemplateStr = textTemplateJson.TemplateStr | 
			
		||||
 | 
				    return resultTextTemplate | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) createTextTemplate(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    // start doing work
 | 
			
		||||
 | 
				    requestBody, err:= ioutil.ReadAll(r.Body) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    newTextTemplateJson := TextTemplateJson{} | 
			
		||||
 | 
				    err = json.Unmarshal(requestBody, &newTextTemplateJson) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    if newTextTemplateJson.Name != "" && newTextTemplateJson.TemplateStr != "" { | 
			
		||||
 | 
				        newTextTemplate := jsonToTextTemplate(newTextTemplateJson) | 
			
		||||
 | 
				        tapit.db.NewRecord(&newTextTemplate) | 
			
		||||
 | 
				        tapit.db.Create(&newTextTemplate) | 
			
		||||
 | 
				        if newTextTemplate.ID == 0 { | 
			
		||||
 | 
				            notifyPopup(w, r, "failure", "Failed to create text template", nil) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				        newTextTemplateJson.Id = int(newTextTemplate.ID) | 
			
		||||
 | 
				        newTextTemplateJson.CreateDate = newTextTemplate.CreatedAt | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        notifyPopup(w, r, "success", "Successfully added new text template", newTextTemplate) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        notifyPopup(w, r, "failure", "Please fill in all details", nil) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) getSpecificTextBody(id uint) string { | 
			
		||||
 | 
				    var textTemplate TextTemplate | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    var dbSearchTT TextTemplate | 
			
		||||
 | 
				    dbSearchTT.ID = id | 
			
		||||
 | 
				    tapit.db.Where(&dbSearchTT).First(&textTemplate) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    return textTemplate.TemplateStr | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) handleSpecificTextTemplate(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    if strings.ToUpper(r.Method) == "PUT" { | 
			
		||||
 | 
				        tapit.updateTextTemplate(w, r) | 
			
		||||
 | 
				    } else if strings.ToUpper(r.Method) == "DELETE" { | 
			
		||||
 | 
				        tapit.deleteTextTemplate(w,r) | 
			
		||||
 | 
				    } else if strings.ToUpper(r.Method) == "GET" { | 
			
		||||
 | 
				        tapit.getTextTemplate(w,r) | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        http.Error(w, "HTTP method not implemented", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) updateTextTemplate(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    requestBody, err:= ioutil.ReadAll(r.Body) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    var newTextTemplateJson TextTemplateJson | 
			
		||||
 | 
				    err = json.Unmarshal(requestBody, &newTextTemplateJson) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    if newTextTemplateJson.Name != "" { | 
			
		||||
 | 
				        var newTextTemplate TextTemplate | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        // get current phonebook
 | 
			
		||||
 | 
				        var dbSearchTT TextTemplate | 
			
		||||
 | 
				        dbSearchTT.ID = uint(newTextTemplateJson.Id) | 
			
		||||
 | 
				        tapit.db.Where(&dbSearchTT).First(&newTextTemplate) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        if newTextTemplate.ID == uint(newTextTemplateJson.Id) { | 
			
		||||
 | 
				            // update name & template
 | 
			
		||||
 | 
				            newTextTemplate.Name = newTextTemplateJson.Name | 
			
		||||
 | 
				            newTextTemplate.TemplateStr = newTextTemplateJson.TemplateStr | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				            // update database
 | 
			
		||||
 | 
				            tapit.db.Save(&newTextTemplate) | 
			
		||||
 | 
				            if newTextTemplate.ID == 0 { | 
			
		||||
 | 
				                notifyPopup(w, r, "failure", "Failed to update phonebook", nil) | 
			
		||||
 | 
				                return | 
			
		||||
 | 
				            } | 
			
		||||
 | 
				            newTextTemplateJson.Id = int(newTextTemplate.ID) | 
			
		||||
 | 
				            newTextTemplateJson.CreateDate = newTextTemplate.CreatedAt | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				            notifyPopup(w, r, "success", "Successfully updated text template", newTextTemplateJson) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } else { | 
			
		||||
 | 
				            notifyPopup(w, r, "failure", "Failed to update text template", nil) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        notifyPopup(w, r, "failure", "Please enter the phonebook name", nil) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) deleteTextTemplate(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    vars := mux.Vars(r) | 
			
		||||
 | 
				    tempID, err := strconv.Atoi(vars["id"]) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // start working
 | 
			
		||||
 | 
				    var textTemplate TextTemplate | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // get tt
 | 
			
		||||
 | 
				    var dbSearchTT TextTemplate | 
			
		||||
 | 
				    dbSearchTT.ID = uint(tempID) | 
			
		||||
 | 
				    tapit.db.Where(dbSearchTT).First(&textTemplate) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    if textTemplate.ID == uint(tempID) { | 
			
		||||
 | 
				        // finally delete it
 | 
			
		||||
 | 
				        tapit.db.Delete(&textTemplate) | 
			
		||||
 | 
				        notifyPopup(w, r, "success", "Successfully deleted phonebook", nil) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) getTextTemplate(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    vars := mux.Vars(r) | 
			
		||||
 | 
				    tempID, err := strconv.Atoi(vars["id"]) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // start working
 | 
			
		||||
 | 
				    var textTemplate TextTemplate | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // get tt
 | 
			
		||||
 | 
				    var dbSearchTT TextTemplate | 
			
		||||
 | 
				    dbSearchTT.ID = uint(tempID) | 
			
		||||
 | 
				    tapit.db.Where(dbSearchTT).First(&textTemplate) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    if textTemplate.ID == uint(tempID) { | 
			
		||||
 | 
				        jsonResults, err := json.Marshal(textTemplateToJson(textTemplate)) | 
			
		||||
 | 
				        if err != nil { | 
			
		||||
 | 
				            http.Error(w, err.Error(), 500) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } else { | 
			
		||||
 | 
				            w.Header().Set("Content-Type", "application/json") | 
			
		||||
 | 
				            w.Write(jsonResults) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func textTemplateToJson(textTemplate TextTemplate) TextTemplateJson { | 
			
		||||
 | 
				    var result TextTemplateJson | 
			
		||||
 | 
				    result.Id = int(textTemplate.ID) | 
			
		||||
 | 
				    result.Name = textTemplate.Name | 
			
		||||
 | 
				    result.TemplateStr = textTemplate.TemplateStr | 
			
		||||
 | 
				    result.CreateDate = textTemplate.CreatedAt | 
			
		||||
 | 
				    return result | 
			
		||||
 | 
				} | 
			
		||||
@ -0,0 +1,215 @@ | 
			
		|||||
 | 
				package main | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				import ( | 
			
		||||
 | 
				    "github.com/jinzhu/gorm" | 
			
		||||
 | 
				    "net/http" | 
			
		||||
 | 
				    "net/url" | 
			
		||||
 | 
				    "strings" | 
			
		||||
 | 
				    "log" | 
			
		||||
 | 
				    "encoding/json" | 
			
		||||
 | 
				    "io/ioutil" | 
			
		||||
 | 
				    "time" | 
			
		||||
 | 
				) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				type TwilioProvider struct { | 
			
		||||
 | 
				    gorm.Model | 
			
		||||
 | 
				    AccountSID string | 
			
		||||
 | 
				    AuthToken string | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				type TwilioProviderJson struct { | 
			
		||||
 | 
				    AccountSID string               `json:"accountSID"` | 
			
		||||
 | 
				    AuthToken string                `json:"authToken"` | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) handleTwilioProvider(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    if strings.ToUpper(r.Method) == "GET" { | 
			
		||||
 | 
				        tapit.getTwilioProvider(w, r) | 
			
		||||
 | 
				    } else if strings.ToUpper(r.Method) == "POST" { | 
			
		||||
 | 
				        tapit.updateTwilioProvider(w, r) | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        http.Error(w, "HTTP method not implemented", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) getTwilioProvider(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    var twilioProvider TwilioProvider | 
			
		||||
 | 
				    tapit.db.Last(&twilioProvider) | 
			
		||||
 | 
				    jsonResults, err := json.Marshal(twilioProviderToJson(twilioProvider)) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        http.Error(w, err.Error(), 500) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				        w.Header().Set("Content-Type", "application/json") | 
			
		||||
 | 
				        w.Write(jsonResults) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) updateTwilioProvider(w http.ResponseWriter, r *http.Request) { | 
			
		||||
 | 
				    requestBody, err:= ioutil.ReadAll(r.Body) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    var newTwilioProviderJson TwilioProviderJson | 
			
		||||
 | 
				    err = json.Unmarshal(requestBody, &newTwilioProviderJson) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        http.Error(w, "Bad request", 400) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // first check if already exist
 | 
			
		||||
 | 
				    var twilioProvider TwilioProvider | 
			
		||||
 | 
				    tapit.db.Last(&twilioProvider) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        http.Error(w, err.Error(), 500) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // update twilioProvider
 | 
			
		||||
 | 
				    twilioProvider.AccountSID = newTwilioProviderJson.AccountSID | 
			
		||||
 | 
				    twilioProvider.AuthToken = newTwilioProviderJson.AuthToken | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // does not exist
 | 
			
		||||
 | 
				    if twilioProvider.ID == 0 { | 
			
		||||
 | 
				        tapit.db.NewRecord(&twilioProvider) | 
			
		||||
 | 
				        tapit.db.Create(&twilioProvider) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        if twilioProvider.ID == 0 { | 
			
		||||
 | 
				            notifyPopup(w, r, "failure", "Failed to create Twilio Provider", nil) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } else { | 
			
		||||
 | 
				            notifyPopup(w, r, "success", "Twilio provider updated", newTwilioProviderJson) | 
			
		||||
 | 
				            return | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				    // exists
 | 
			
		||||
 | 
				        tapit.db.Save(&twilioProvider) | 
			
		||||
 | 
				        notifyPopup(w, r, "success", "Twilio provider updated", newTwilioProviderJson) | 
			
		||||
 | 
				        return | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func twilioProviderToJson(tProvider TwilioProvider) TwilioProviderJson { | 
			
		||||
 | 
				    var results TwilioProviderJson | 
			
		||||
 | 
				    results.AccountSID = tProvider.AccountSID | 
			
		||||
 | 
				    results.AuthToken = tProvider.AuthToken | 
			
		||||
 | 
				    return results | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) twilioSend(accSid string, accToken string, bodyText string, fromNum string, toNum string) []byte { | 
			
		||||
 | 
				    // if burp proxy is necessary
 | 
			
		||||
 | 
				    client := &http.Client{ | 
			
		||||
 | 
				        Timeout: 5 * time.Second, | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    method1 := "POST" | 
			
		||||
 | 
				    url1 := "https://api.twilio.com/2010-04-01/Accounts/"+accSid+"/Messages.json" | 
			
		||||
 | 
				    // making body
 | 
			
		||||
 | 
				    params := url.Values{} | 
			
		||||
 | 
				    params.Add("Body", bodyText) | 
			
		||||
 | 
				    params.Add("From", fromNum) | 
			
		||||
 | 
				    params.Add("To", toNum) | 
			
		||||
 | 
				    body1 := strings.NewReader(params.Encode()) | 
			
		||||
 | 
				    log.Println(params.Encode()) | 
			
		||||
 | 
				    // making request
 | 
			
		||||
 | 
				    newRequest1, err := http.NewRequest(method1, url1, body1) | 
			
		||||
 | 
				    if err != nil { | 
			
		||||
 | 
				        log.Fatal("Error in creating request") | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    //basic auth with token
 | 
			
		||||
 | 
				    newRequest1.SetBasicAuth(accSid, accToken) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    //set headers
 | 
			
		||||
 | 
				    newRequest1.Header.Add("Content-Type","application/x-www-form-urlencoded; charset=UTF-8") | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // sending request
 | 
			
		||||
 | 
				    res, err := client.Do(newRequest1) | 
			
		||||
 | 
				    retriesLeft := tapit.globalSettings.maxRequestRetries | 
			
		||||
 | 
				    for err != nil && retriesLeft > 0 { | 
			
		||||
 | 
				        log.Println("Error in sending request") | 
			
		||||
 | 
				        res, err = client.Do(newRequest1) | 
			
		||||
 | 
				        retriesLeft -= 1 | 
			
		||||
 | 
				        time.Sleep(time.Duration(tapit.globalSettings.waitBeforeRetry) * time.Millisecond) | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // exit gracefully if can't
 | 
			
		||||
 | 
				    if err!= nil { | 
			
		||||
 | 
				        var emptyBytes []byte | 
			
		||||
 | 
				        return emptyBytes | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    outputStr, _ := ioutil.ReadAll(res.Body) | 
			
		||||
 | 
				    log.Println(string(outputStr)) | 
			
		||||
 | 
				    return outputStr | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) twilioCheck(accSid string, accToken string, messageSid string) []byte { | 
			
		||||
 | 
				    client := &http.Client{ | 
			
		||||
 | 
				        Timeout: 5 * time.Second, | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    method1 := "GET" | 
			
		||||
 | 
				    url1 := "https://api.twilio.com/2010-04-01/Accounts/"+accSid+"/Messages/"+messageSid+".json" | 
			
		||||
 | 
				    body1 := strings.NewReader("") | 
			
		||||
 | 
				    newRequest1, err := http.NewRequest(method1, url1, body1) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // authenticate
 | 
			
		||||
 | 
				    newRequest1.SetBasicAuth(accSid, accToken) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // sending request
 | 
			
		||||
 | 
				    res, err := client.Do(newRequest1) | 
			
		||||
 | 
				    retriesLeft := tapit.globalSettings.maxRequestRetries | 
			
		||||
 | 
				    for err != nil && retriesLeft > 0 { | 
			
		||||
 | 
				        log.Println("Error in sending request") | 
			
		||||
 | 
				        res, err = client.Do(newRequest1) | 
			
		||||
 | 
				        retriesLeft -= 1 | 
			
		||||
 | 
				        time.Sleep(time.Duration(tapit.globalSettings.waitBeforeRetry) * time.Millisecond) | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    // exit gracefully if can't
 | 
			
		||||
 | 
				    if err!= nil { | 
			
		||||
 | 
				        var emptyBytes []byte | 
			
		||||
 | 
				        return emptyBytes | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    outputStr, _ := ioutil.ReadAll(res.Body) | 
			
		||||
 | 
				    log.Println(string(outputStr)) | 
			
		||||
 | 
				    return outputStr | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				func (tapit *Tapit) workerTwilioChecker() { | 
			
		||||
 | 
				    // infinite loop to keep checking for queued jobs to check delivery status
 | 
			
		||||
 | 
				    for true { | 
			
		||||
 | 
				        // sleep 5 second per cycle
 | 
			
		||||
 | 
				        time.Sleep(5000 * time.Millisecond) | 
			
		||||
 | 
				        var pendJobs []Job | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        tapit.db.Where("provider_tag = ? AND (current_status = ? OR current_status = ?)", "twilio", "Queued", "Sent").Find(&pendJobs) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        for _, job := range pendJobs { | 
			
		||||
 | 
				            // sleep 100ms per job
 | 
			
		||||
 | 
				            time.Sleep(100 * time.Millisecond) | 
			
		||||
 | 
				            resultJson := tapit.twilioCheck(job.AccSID, job.AuthToken, job.MessageSid) | 
			
		||||
 | 
				            job.ResultStr = string(resultJson) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				            var twilioResult TwilioMessageJson | 
			
		||||
 | 
				            err := json.Unmarshal(resultJson, &twilioResult) | 
			
		||||
 | 
				            if err != nil { | 
			
		||||
 | 
				                log.Println(err) | 
			
		||||
 | 
				                job.CurrentStatus = "Failed" | 
			
		||||
 | 
				            } else if twilioResult.Status == "queued" { | 
			
		||||
 | 
				                job.MessageSid = twilioResult.Sid | 
			
		||||
 | 
				                job.CurrentStatus = "Queued" | 
			
		||||
 | 
				            } else if twilioResult.Status == "sent" { | 
			
		||||
 | 
				                job.MessageSid = twilioResult.Sid | 
			
		||||
 | 
				                job.CurrentStatus = "Sent" | 
			
		||||
 | 
				            } else if twilioResult.Status == "delivered" { | 
			
		||||
 | 
				                job.MessageSid = twilioResult.Sid | 
			
		||||
 | 
				                job.CurrentStatus = "Delivered" | 
			
		||||
 | 
				            } | 
			
		||||
 | 
				            tapit.db.Save(&job) | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
@ -0,0 +1,18 @@ | 
			
		|||||
 | 
				FROM ubuntu | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				ENV PROVISION_CONTEXT "production" | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				# Deploy scripts/configurations | 
			
		||||
 | 
				COPY static/ /static | 
			
		||||
 | 
				COPY tapit /tapit | 
			
		||||
 | 
				COPY api-poc /api-poc | 
			
		||||
 | 
				COPY entrypoint.sh /entrypoint.sh | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				run apt-get update | 
			
		||||
 | 
				run apt-get install -y ca-certificates | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				# Harder to bypass | 
			
		||||
 | 
				ENTRYPOINT ["/entrypoint.sh"] | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				# Can be overwritten -- with run args | 
			
		||||
 | 
				CMD ["/entrypoint.sh"] | 
			
		||||
								
									
										File diff suppressed because it is too large
									
								
							
						
					
								
									
										File diff suppressed because one or more lines are too long
									
								
							
						
					
								
									
										File diff suppressed because one or more lines are too long
									
								
							
						
					
								
									
										File diff suppressed because one or more lines are too long
									
								
							
						
					@ -0,0 +1,331 @@ | 
			
		|||||
 | 
				/*! | 
			
		||||
 | 
				 * Bootstrap Reboot v4.3.1 (https://getbootstrap.com/) | 
			
		||||
 | 
				 * Copyright 2011-2019 The Bootstrap Authors | 
			
		||||
 | 
				 * Copyright 2011-2019 Twitter, Inc. | 
			
		||||
 | 
				 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) | 
			
		||||
 | 
				 * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) | 
			
		||||
 | 
				 */ | 
			
		||||
 | 
				*, | 
			
		||||
 | 
				*::before, | 
			
		||||
 | 
				*::after { | 
			
		||||
 | 
				  box-sizing: border-box; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				html { | 
			
		||||
 | 
				  font-family: sans-serif; | 
			
		||||
 | 
				  line-height: 1.15; | 
			
		||||
 | 
				  -webkit-text-size-adjust: 100%; | 
			
		||||
 | 
				  -webkit-tap-highlight-color: rgba(0, 0, 0, 0); | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { | 
			
		||||
 | 
				  display: block; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				body { | 
			
		||||
 | 
				  margin: 0; | 
			
		||||
 | 
				  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; | 
			
		||||
 | 
				  font-size: 1rem; | 
			
		||||
 | 
				  font-weight: 400; | 
			
		||||
 | 
				  line-height: 1.5; | 
			
		||||
 | 
				  color: #212529; | 
			
		||||
 | 
				  text-align: left; | 
			
		||||
 | 
				  background-color: #fff; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				[tabindex="-1"]:focus { | 
			
		||||
 | 
				  outline: 0 !important; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				hr { | 
			
		||||
 | 
				  box-sizing: content-box; | 
			
		||||
 | 
				  height: 0; | 
			
		||||
 | 
				  overflow: visible; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				h1, h2, h3, h4, h5, h6 { | 
			
		||||
 | 
				  margin-top: 0; | 
			
		||||
 | 
				  margin-bottom: 0.5rem; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				p { | 
			
		||||
 | 
				  margin-top: 0; | 
			
		||||
 | 
				  margin-bottom: 1rem; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				abbr[title], | 
			
		||||
 | 
				abbr[data-original-title] { | 
			
		||||
 | 
				  text-decoration: underline; | 
			
		||||
 | 
				  -webkit-text-decoration: underline dotted; | 
			
		||||
 | 
				  text-decoration: underline dotted; | 
			
		||||
 | 
				  cursor: help; | 
			
		||||
 | 
				  border-bottom: 0; | 
			
		||||
 | 
				  -webkit-text-decoration-skip-ink: none; | 
			
		||||
 | 
				  text-decoration-skip-ink: none; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				address { | 
			
		||||
 | 
				  margin-bottom: 1rem; | 
			
		||||
 | 
				  font-style: normal; | 
			
		||||
 | 
				  line-height: inherit; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				ol, | 
			
		||||
 | 
				ul, | 
			
		||||
 | 
				dl { | 
			
		||||
 | 
				  margin-top: 0; | 
			
		||||
 | 
				  margin-bottom: 1rem; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				ol ol, | 
			
		||||
 | 
				ul ul, | 
			
		||||
 | 
				ol ul, | 
			
		||||
 | 
				ul ol { | 
			
		||||
 | 
				  margin-bottom: 0; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				dt { | 
			
		||||
 | 
				  font-weight: 700; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				dd { | 
			
		||||
 | 
				  margin-bottom: .5rem; | 
			
		||||
 | 
				  margin-left: 0; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				blockquote { | 
			
		||||
 | 
				  margin: 0 0 1rem; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				b, | 
			
		||||
 | 
				strong { | 
			
		||||
 | 
				  font-weight: bolder; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				small { | 
			
		||||
 | 
				  font-size: 80%; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				sub, | 
			
		||||
 | 
				sup { | 
			
		||||
 | 
				  position: relative; | 
			
		||||
 | 
				  font-size: 75%; | 
			
		||||
 | 
				  line-height: 0; | 
			
		||||
 | 
				  vertical-align: baseline; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				sub { | 
			
		||||
 | 
				  bottom: -.25em; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				sup { | 
			
		||||
 | 
				  top: -.5em; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				a { | 
			
		||||
 | 
				  color: #007bff; | 
			
		||||
 | 
				  text-decoration: none; | 
			
		||||
 | 
				  background-color: transparent; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				a:hover { | 
			
		||||
 | 
				  color: #0056b3; | 
			
		||||
 | 
				  text-decoration: underline; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				a:not([href]):not([tabindex]) { | 
			
		||||
 | 
				  color: inherit; | 
			
		||||
 | 
				  text-decoration: none; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus { | 
			
		||||
 | 
				  color: inherit; | 
			
		||||
 | 
				  text-decoration: none; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				a:not([href]):not([tabindex]):focus { | 
			
		||||
 | 
				  outline: 0; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				pre, | 
			
		||||
 | 
				code, | 
			
		||||
 | 
				kbd, | 
			
		||||
 | 
				samp { | 
			
		||||
 | 
				  font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; | 
			
		||||
 | 
				  font-size: 1em; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				pre { | 
			
		||||
 | 
				  margin-top: 0; | 
			
		||||
 | 
				  margin-bottom: 1rem; | 
			
		||||
 | 
				  overflow: auto; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				figure { | 
			
		||||
 | 
				  margin: 0 0 1rem; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				img { | 
			
		||||
 | 
				  vertical-align: middle; | 
			
		||||
 | 
				  border-style: none; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				svg { | 
			
		||||
 | 
				  overflow: hidden; | 
			
		||||
 | 
				  vertical-align: middle; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				table { | 
			
		||||
 | 
				  border-collapse: collapse; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				caption { | 
			
		||||
 | 
				  padding-top: 0.75rem; | 
			
		||||
 | 
				  padding-bottom: 0.75rem; | 
			
		||||
 | 
				  color: #6c757d; | 
			
		||||
 | 
				  text-align: left; | 
			
		||||
 | 
				  caption-side: bottom; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				th { | 
			
		||||
 | 
				  text-align: inherit; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				label { | 
			
		||||
 | 
				  display: inline-block; | 
			
		||||
 | 
				  margin-bottom: 0.5rem; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				button { | 
			
		||||
 | 
				  border-radius: 0; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				button:focus { | 
			
		||||
 | 
				  outline: 1px dotted; | 
			
		||||
 | 
				  outline: 5px auto -webkit-focus-ring-color; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				input, | 
			
		||||
 | 
				button, | 
			
		||||
 | 
				select, | 
			
		||||
 | 
				optgroup, | 
			
		||||
 | 
				textarea { | 
			
		||||
 | 
				  margin: 0; | 
			
		||||
 | 
				  font-family: inherit; | 
			
		||||
 | 
				  font-size: inherit; | 
			
		||||
 | 
				  line-height: inherit; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				button, | 
			
		||||
 | 
				input { | 
			
		||||
 | 
				  overflow: visible; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				button, | 
			
		||||
 | 
				select { | 
			
		||||
 | 
				  text-transform: none; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				select { | 
			
		||||
 | 
				  word-wrap: normal; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				button, | 
			
		||||
 | 
				[type="button"], | 
			
		||||
 | 
				[type="reset"], | 
			
		||||
 | 
				[type="submit"] { | 
			
		||||
 | 
				  -webkit-appearance: button; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				button:not(:disabled), | 
			
		||||
 | 
				[type="button"]:not(:disabled), | 
			
		||||
 | 
				[type="reset"]:not(:disabled), | 
			
		||||
 | 
				[type="submit"]:not(:disabled) { | 
			
		||||
 | 
				  cursor: pointer; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				button::-moz-focus-inner, | 
			
		||||
 | 
				[type="button"]::-moz-focus-inner, | 
			
		||||
 | 
				[type="reset"]::-moz-focus-inner, | 
			
		||||
 | 
				[type="submit"]::-moz-focus-inner { | 
			
		||||
 | 
				  padding: 0; | 
			
		||||
 | 
				  border-style: none; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				input[type="radio"], | 
			
		||||
 | 
				input[type="checkbox"] { | 
			
		||||
 | 
				  box-sizing: border-box; | 
			
		||||
 | 
				  padding: 0; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				input[type="date"], | 
			
		||||
 | 
				input[type="time"], | 
			
		||||
 | 
				input[type="datetime-local"], | 
			
		||||
 | 
				input[type="month"] { | 
			
		||||
 | 
				  -webkit-appearance: listbox; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				textarea { | 
			
		||||
 | 
				  overflow: auto; | 
			
		||||
 | 
				  resize: vertical; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				fieldset { | 
			
		||||
 | 
				  min-width: 0; | 
			
		||||
 | 
				  padding: 0; | 
			
		||||
 | 
				  margin: 0; | 
			
		||||
 | 
				  border: 0; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				legend { | 
			
		||||
 | 
				  display: block; | 
			
		||||
 | 
				  width: 100%; | 
			
		||||
 | 
				  max-width: 100%; | 
			
		||||
 | 
				  padding: 0; | 
			
		||||
 | 
				  margin-bottom: .5rem; | 
			
		||||
 | 
				  font-size: 1.5rem; | 
			
		||||
 | 
				  line-height: inherit; | 
			
		||||
 | 
				  color: inherit; | 
			
		||||
 | 
				  white-space: normal; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				progress { | 
			
		||||
 | 
				  vertical-align: baseline; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				[type="number"]::-webkit-inner-spin-button, | 
			
		||||
 | 
				[type="number"]::-webkit-outer-spin-button { | 
			
		||||
 | 
				  height: auto; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				[type="search"] { | 
			
		||||
 | 
				  outline-offset: -2px; | 
			
		||||
 | 
				  -webkit-appearance: none; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				[type="search"]::-webkit-search-decoration { | 
			
		||||
 | 
				  -webkit-appearance: none; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				::-webkit-file-upload-button { | 
			
		||||
 | 
				  font: inherit; | 
			
		||||
 | 
				  -webkit-appearance: button; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				output { | 
			
		||||
 | 
				  display: inline-block; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				summary { | 
			
		||||
 | 
				  display: list-item; | 
			
		||||
 | 
				  cursor: pointer; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				template { | 
			
		||||
 | 
				  display: none; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				[hidden] { | 
			
		||||
 | 
				  display: none !important; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				/*# sourceMappingURL=bootstrap-reboot.css.map */ | 
			
		||||
								
									
										File diff suppressed because one or more lines are too long
									
								
							
						
					@ -0,0 +1,8 @@ | 
			
		|||||
 | 
				/*! | 
			
		||||
 | 
				 * Bootstrap Reboot v4.3.1 (https://getbootstrap.com/) | 
			
		||||
 | 
				 * Copyright 2011-2019 The Bootstrap Authors | 
			
		||||
 | 
				 * Copyright 2011-2019 Twitter, Inc. | 
			
		||||
 | 
				 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) | 
			
		||||
 | 
				 * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) | 
			
		||||
 | 
				 */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important} | 
			
		||||
 | 
				/*# sourceMappingURL=bootstrap-reboot.min.css.map */ | 
			
		||||
								
									
										File diff suppressed because one or more lines are too long
									
								
							
						
					
								
									
										File diff suppressed because it is too large
									
								
							
						
					
								
									
										File diff suppressed because one or more lines are too long
									
								
							
						
					
								
									
										File diff suppressed because one or more lines are too long
									
								
							
						
					
								
									
										File diff suppressed because one or more lines are too long
									
								
							
						
					
								
									
										File diff suppressed because it is too large
									
								
							
						
					
								
									
										File diff suppressed because one or more lines are too long
									
								
							
						
					
								
									
										File diff suppressed because one or more lines are too long
									
								
							
						
					
								
									
										File diff suppressed because one or more lines are too long
									
								
							
						
					
								
									
										File diff suppressed because it is too large
									
								
							
						
					
								
									
										File diff suppressed because one or more lines are too long
									
								
							
						
					
								
									
										File diff suppressed because one or more lines are too long
									
								
							
						
					
								
									
										File diff suppressed because one or more lines are too long
									
								
							
						
					
								
									
										File diff suppressed because one or more lines are too long
									
								
							
						
					
								
									
										File diff suppressed because one or more lines are too long
									
								
							
						
					
								
									
										File diff suppressed because one or more lines are too long
									
								
							
						
					
								
									
										File diff suppressed because one or more lines are too long
									
								
							
						
					@ -0,0 +1,21 @@ | 
			
		|||||
 | 
				<!doctype html> | 
			
		||||
 | 
				<html lang="en"> | 
			
		||||
 | 
				<head> | 
			
		||||
 | 
				  <!-- Start Bootstrap  - Possible to change to static links--> | 
			
		||||
 | 
				  <script src="/assets/jquery-3.4.1.min.js"></script> | 
			
		||||
 | 
				  <script src="/assets/popper.min.js"></script> | 
			
		||||
 | 
				  <link rel="stylesheet" href="/assets/bootstrap-4.3.1-dist/css/bootstrap.min.css"> | 
			
		||||
 | 
				  <script src="/assets/bootstrap-4.3.1-dist/js/bootstrap.min.js"></script> | 
			
		||||
 | 
				  <!-- End Bootstrap  --> | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  <meta charset="utf-8"> | 
			
		||||
 | 
				  <title>Tap It - Text Phishing Framework</title> | 
			
		||||
 | 
				  <base href="/"> | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  <meta name="viewport" content="width=device-width, initial-scale=1"> | 
			
		||||
 | 
				  <link rel="icon" type="image/x-icon" href="favicon.ico"> | 
			
		||||
 | 
				</head> | 
			
		||||
 | 
				<body> | 
			
		||||
 | 
				  <app-root></app-root> | 
			
		||||
 | 
				<script type="text/javascript" src="runtime.js"></script><script type="text/javascript" src="es2015-polyfills.js" nomodule></script><script type="text/javascript" src="polyfills.js"></script><script type="text/javascript" src="styles.js"></script><script type="text/javascript" src="vendor.js"></script><script type="text/javascript" src="main.js"></script></body> | 
			
		||||
 | 
				</html> | 
			
		||||
@ -0,0 +1,2 @@ | 
			
		|||||
 | 
				!function(e){function r(r){for(var n,f,i=r[0],l=r[1],a=r[2],c=0,s=[];c<i.length;c++)f=i[c],o[f]&&s.push(o[f][0]),o[f]=0;for(n in l)Object.prototype.hasOwnProperty.call(l,n)&&(e[n]=l[n]);for(p&&p(r);s.length;)s.shift()();return u.push.apply(u,a||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++){var l=t[i];0!==o[l]&&(n=!1)}n&&(u.splice(r--,1),e=f(f.s=t[0]))}return e}var n={},o={0:0},u=[];function f(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,f),t.l=!0,t.exports}f.m=e,f.c=n,f.d=function(e,r,t){f.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},f.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},f.t=function(e,r){if(1&r&&(e=f(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(f.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)f.d(t,n,(function(r){return e[r]}).bind(null,n));return t},f.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return f.d(r,"a",r),r},f.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},f.p="";var i=window.webpackJsonp=window.webpackJsonp||[],l=i.push.bind(i);i.push=r,i=i.slice();for(var a=0;a<i.length;a++)r(i[a]);var p=l;t()}([]); | 
			
		||||
 | 
				//# sourceMappingURL=runtime.js.map
 | 
			
		||||
@ -0,0 +1,13 @@ | 
			
		|||||
 | 
				# Editor configuration, see https://editorconfig.org | 
			
		||||
 | 
				root = true | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				[*] | 
			
		||||
 | 
				charset = utf-8 | 
			
		||||
 | 
				indent_style = space | 
			
		||||
 | 
				indent_size = 2 | 
			
		||||
 | 
				insert_final_newline = true | 
			
		||||
 | 
				trim_trailing_whitespace = true | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				[*.md] | 
			
		||||
 | 
				max_line_length = off | 
			
		||||
 | 
				trim_trailing_whitespace = false | 
			
		||||
@ -0,0 +1,27 @@ | 
			
		|||||
 | 
				# TapitFrontend | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 7.3.1. | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				## Development server | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				## Code scaffolding | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				## Build | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				## Running unit tests | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				## Running end-to-end tests | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				## Further help | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). | 
			
		||||
@ -0,0 +1,136 @@ | 
			
		|||||
 | 
				{ | 
			
		||||
 | 
				  "$schema": "./node_modules/@angular/cli/lib/config/schema.json", | 
			
		||||
 | 
				  "version": 1, | 
			
		||||
 | 
				  "newProjectRoot": "projects", | 
			
		||||
 | 
				  "projects": { | 
			
		||||
 | 
				    "tapit-frontend": { | 
			
		||||
 | 
				      "root": "", | 
			
		||||
 | 
				      "sourceRoot": "src", | 
			
		||||
 | 
				      "projectType": "application", | 
			
		||||
 | 
				      "prefix": "app", | 
			
		||||
 | 
				      "schematics": {}, | 
			
		||||
 | 
				      "architect": { | 
			
		||||
 | 
				        "build": { | 
			
		||||
 | 
				          "builder": "@angular-devkit/build-angular:browser", | 
			
		||||
 | 
				          "options": { | 
			
		||||
 | 
				            "outputPath": "dist/tapit-frontend", | 
			
		||||
 | 
				            "index": "src/index.html", | 
			
		||||
 | 
				            "main": "src/main.ts", | 
			
		||||
 | 
				            "polyfills": "src/polyfills.ts", | 
			
		||||
 | 
				            "tsConfig": "src/tsconfig.app.json", | 
			
		||||
 | 
				            "assets": [ | 
			
		||||
 | 
				              "src/favicon.ico", | 
			
		||||
 | 
				              "src/assets" | 
			
		||||
 | 
				            ], | 
			
		||||
 | 
				            "styles": [ | 
			
		||||
 | 
				              "src/styles.css" | 
			
		||||
 | 
				            ], | 
			
		||||
 | 
				            "scripts": [], | 
			
		||||
 | 
				            "es5BrowserSupport": true | 
			
		||||
 | 
				          }, | 
			
		||||
 | 
				          "configurations": { | 
			
		||||
 | 
				            "production": { | 
			
		||||
 | 
				              "fileReplacements": [ | 
			
		||||
 | 
				                { | 
			
		||||
 | 
				                  "replace": "src/environments/environment.ts", | 
			
		||||
 | 
				                  "with": "src/environments/environment.prod.ts" | 
			
		||||
 | 
				                } | 
			
		||||
 | 
				              ], | 
			
		||||
 | 
				              "optimization": true, | 
			
		||||
 | 
				              "outputHashing": "all", | 
			
		||||
 | 
				              "sourceMap": false, | 
			
		||||
 | 
				              "extractCss": true, | 
			
		||||
 | 
				              "namedChunks": false, | 
			
		||||
 | 
				              "aot": true, | 
			
		||||
 | 
				              "extractLicenses": true, | 
			
		||||
 | 
				              "vendorChunk": false, | 
			
		||||
 | 
				              "buildOptimizer": true, | 
			
		||||
 | 
				              "budgets": [ | 
			
		||||
 | 
				                { | 
			
		||||
 | 
				                  "type": "initial", | 
			
		||||
 | 
				                  "maximumWarning": "2mb", | 
			
		||||
 | 
				                  "maximumError": "5mb" | 
			
		||||
 | 
				                } | 
			
		||||
 | 
				              ] | 
			
		||||
 | 
				            } | 
			
		||||
 | 
				          } | 
			
		||||
 | 
				        }, | 
			
		||||
 | 
				        "serve": { | 
			
		||||
 | 
				          "builder": "@angular-devkit/build-angular:dev-server", | 
			
		||||
 | 
				          "options": { | 
			
		||||
 | 
				            "browserTarget": "tapit-frontend:build" | 
			
		||||
 | 
				          }, | 
			
		||||
 | 
				          "configurations": { | 
			
		||||
 | 
				            "production": { | 
			
		||||
 | 
				              "browserTarget": "tapit-frontend:build:production" | 
			
		||||
 | 
				            } | 
			
		||||
 | 
				          } | 
			
		||||
 | 
				        }, | 
			
		||||
 | 
				        "extract-i18n": { | 
			
		||||
 | 
				          "builder": "@angular-devkit/build-angular:extract-i18n", | 
			
		||||
 | 
				          "options": { | 
			
		||||
 | 
				            "browserTarget": "tapit-frontend:build" | 
			
		||||
 | 
				          } | 
			
		||||
 | 
				        }, | 
			
		||||
 | 
				        "test": { | 
			
		||||
 | 
				          "builder": "@angular-devkit/build-angular:karma", | 
			
		||||
 | 
				          "options": { | 
			
		||||
 | 
				            "main": "src/test.ts", | 
			
		||||
 | 
				            "polyfills": "src/polyfills.ts", | 
			
		||||
 | 
				            "tsConfig": "src/tsconfig.spec.json", | 
			
		||||
 | 
				            "karmaConfig": "src/karma.conf.js", | 
			
		||||
 | 
				            "styles": [ | 
			
		||||
 | 
				              "src/styles.css" | 
			
		||||
 | 
				            ], | 
			
		||||
 | 
				            "scripts": [], | 
			
		||||
 | 
				            "assets": [ | 
			
		||||
 | 
				              "src/favicon.ico", | 
			
		||||
 | 
				              "src/assets" | 
			
		||||
 | 
				            ] | 
			
		||||
 | 
				          } | 
			
		||||
 | 
				        }, | 
			
		||||
 | 
				        "lint": { | 
			
		||||
 | 
				          "builder": "@angular-devkit/build-angular:tslint", | 
			
		||||
 | 
				          "options": { | 
			
		||||
 | 
				            "tsConfig": [ | 
			
		||||
 | 
				              "src/tsconfig.app.json", | 
			
		||||
 | 
				              "src/tsconfig.spec.json" | 
			
		||||
 | 
				            ], | 
			
		||||
 | 
				            "exclude": [ | 
			
		||||
 | 
				              "**/node_modules/**" | 
			
		||||
 | 
				            ] | 
			
		||||
 | 
				          } | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				      } | 
			
		||||
 | 
				    }, | 
			
		||||
 | 
				    "tapit-frontend-e2e": { | 
			
		||||
 | 
				      "root": "e2e/", | 
			
		||||
 | 
				      "projectType": "application", | 
			
		||||
 | 
				      "prefix": "", | 
			
		||||
 | 
				      "architect": { | 
			
		||||
 | 
				        "e2e": { | 
			
		||||
 | 
				          "builder": "@angular-devkit/build-angular:protractor", | 
			
		||||
 | 
				          "options": { | 
			
		||||
 | 
				            "protractorConfig": "e2e/protractor.conf.js", | 
			
		||||
 | 
				            "devServerTarget": "tapit-frontend:serve" | 
			
		||||
 | 
				          }, | 
			
		||||
 | 
				          "configurations": { | 
			
		||||
 | 
				            "production": { | 
			
		||||
 | 
				              "devServerTarget": "tapit-frontend:serve:production" | 
			
		||||
 | 
				            } | 
			
		||||
 | 
				          } | 
			
		||||
 | 
				        }, | 
			
		||||
 | 
				        "lint": { | 
			
		||||
 | 
				          "builder": "@angular-devkit/build-angular:tslint", | 
			
		||||
 | 
				          "options": { | 
			
		||||
 | 
				            "tsConfig": "e2e/tsconfig.e2e.json", | 
			
		||||
 | 
				            "exclude": [ | 
			
		||||
 | 
				              "**/node_modules/**" | 
			
		||||
 | 
				            ] | 
			
		||||
 | 
				          } | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				      } | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				  }, | 
			
		||||
 | 
				  "defaultProject": "tapit-frontend" | 
			
		||||
 | 
				} | 
			
		||||
@ -0,0 +1,28 @@ | 
			
		|||||
 | 
				// Protractor configuration file, see link for more information
 | 
			
		||||
 | 
				// https://github.com/angular/protractor/blob/master/lib/config.ts
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				const { SpecReporter } = require('jasmine-spec-reporter'); | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				exports.config = { | 
			
		||||
 | 
				  allScriptsTimeout: 11000, | 
			
		||||
 | 
				  specs: [ | 
			
		||||
 | 
				    './src/**/*.e2e-spec.ts' | 
			
		||||
 | 
				  ], | 
			
		||||
 | 
				  capabilities: { | 
			
		||||
 | 
				    'browserName': 'chrome' | 
			
		||||
 | 
				  }, | 
			
		||||
 | 
				  directConnect: true, | 
			
		||||
 | 
				  baseUrl: 'http://localhost:4200/', | 
			
		||||
 | 
				  framework: 'jasmine', | 
			
		||||
 | 
				  jasmineNodeOpts: { | 
			
		||||
 | 
				    showColors: true, | 
			
		||||
 | 
				    defaultTimeoutInterval: 30000, | 
			
		||||
 | 
				    print: function() {} | 
			
		||||
 | 
				  }, | 
			
		||||
 | 
				  onPrepare() { | 
			
		||||
 | 
				    require('ts-node').register({ | 
			
		||||
 | 
				      project: require('path').join(__dirname, './tsconfig.e2e.json') | 
			
		||||
 | 
				    }); | 
			
		||||
 | 
				    jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				}; | 
			
		||||
@ -0,0 +1,23 @@ | 
			
		|||||
 | 
				import { AppPage } from './app.po'; | 
			
		||||
 | 
				import { browser, logging } from 'protractor'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				describe('workspace-project App', () => { | 
			
		||||
 | 
				  let page: AppPage; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  beforeEach(() => { | 
			
		||||
 | 
				    page = new AppPage(); | 
			
		||||
 | 
				  }); | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  it('should display welcome message', () => { | 
			
		||||
 | 
				    page.navigateTo(); | 
			
		||||
 | 
				    expect(page.getTitleText()).toEqual('Welcome to tapit-frontend!'); | 
			
		||||
 | 
				  }); | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  afterEach(async () => { | 
			
		||||
 | 
				    // Assert that there are no errors emitted from the browser
 | 
			
		||||
 | 
				    const logs = await browser.manage().logs().get(logging.Type.BROWSER); | 
			
		||||
 | 
				    expect(logs).not.toContain(jasmine.objectContaining({ | 
			
		||||
 | 
				      level: logging.Level.SEVERE, | 
			
		||||
 | 
				    })); | 
			
		||||
 | 
				  }); | 
			
		||||
 | 
				}); | 
			
		||||
@ -0,0 +1,11 @@ | 
			
		|||||
 | 
				import { browser, by, element } from 'protractor'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export class AppPage { | 
			
		||||
 | 
				  navigateTo() { | 
			
		||||
 | 
				    return browser.get(browser.baseUrl) as Promise<any>; | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  getTitleText() { | 
			
		||||
 | 
				    return element(by.css('app-root h1')).getText() as Promise<string>; | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				} | 
			
		||||
@ -0,0 +1,13 @@ | 
			
		|||||
 | 
				{ | 
			
		||||
 | 
				  "extends": "../tsconfig.json", | 
			
		||||
 | 
				  "compilerOptions": { | 
			
		||||
 | 
				    "outDir": "../out-tsc/app", | 
			
		||||
 | 
				    "module": "commonjs", | 
			
		||||
 | 
				    "target": "es5", | 
			
		||||
 | 
				    "types": [ | 
			
		||||
 | 
				      "jasmine", | 
			
		||||
 | 
				      "jasminewd2", | 
			
		||||
 | 
				      "node" | 
			
		||||
 | 
				    ] | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				} | 
			
		||||
@ -0,0 +1,48 @@ | 
			
		|||||
 | 
				{ | 
			
		||||
 | 
				  "name": "tapit-frontend", | 
			
		||||
 | 
				  "version": "0.0.0", | 
			
		||||
 | 
				  "scripts": { | 
			
		||||
 | 
				    "ng": "ng", | 
			
		||||
 | 
				    "start": "ng serve", | 
			
		||||
 | 
				    "build": "ng build", | 
			
		||||
 | 
				    "test": "ng test", | 
			
		||||
 | 
				    "lint": "ng lint", | 
			
		||||
 | 
				    "e2e": "ng e2e" | 
			
		||||
 | 
				  }, | 
			
		||||
 | 
				  "private": true, | 
			
		||||
 | 
				  "dependencies": { | 
			
		||||
 | 
				    "@angular/animations": "~7.2.0", | 
			
		||||
 | 
				    "@angular/common": "~7.2.0", | 
			
		||||
 | 
				    "@angular/compiler": "~7.2.0", | 
			
		||||
 | 
				    "@angular/core": "~7.2.0", | 
			
		||||
 | 
				    "@angular/forms": "~7.2.0", | 
			
		||||
 | 
				    "@angular/platform-browser": "~7.2.0", | 
			
		||||
 | 
				    "@angular/platform-browser-dynamic": "~7.2.0", | 
			
		||||
 | 
				    "@angular/router": "~7.2.0", | 
			
		||||
 | 
				    "core-js": "^2.5.4", | 
			
		||||
 | 
				    "rxjs": "~6.3.3", | 
			
		||||
 | 
				    "tslib": "^1.9.0", | 
			
		||||
 | 
				    "zone.js": "~0.8.26" | 
			
		||||
 | 
				  }, | 
			
		||||
 | 
				  "devDependencies": { | 
			
		||||
 | 
				    "@angular-devkit/build-angular": "~0.13.0", | 
			
		||||
 | 
				    "@angular/cli": "~7.3.1", | 
			
		||||
 | 
				    "@angular/compiler-cli": "~7.2.0", | 
			
		||||
 | 
				    "@angular/language-service": "~7.2.0", | 
			
		||||
 | 
				    "@types/node": "~8.9.4", | 
			
		||||
 | 
				    "@types/jasmine": "~2.8.8", | 
			
		||||
 | 
				    "@types/jasminewd2": "~2.0.3", | 
			
		||||
 | 
				    "codelyzer": "~4.5.0", | 
			
		||||
 | 
				    "jasmine-core": "~2.99.1", | 
			
		||||
 | 
				    "jasmine-spec-reporter": "~4.2.1", | 
			
		||||
 | 
				    "karma": "~3.1.1", | 
			
		||||
 | 
				    "karma-chrome-launcher": "~2.2.0", | 
			
		||||
 | 
				    "karma-coverage-istanbul-reporter": "~2.0.1", | 
			
		||||
 | 
				    "karma-jasmine": "~1.1.2", | 
			
		||||
 | 
				    "karma-jasmine-html-reporter": "^0.2.2", | 
			
		||||
 | 
				    "protractor": "~5.4.0", | 
			
		||||
 | 
				    "ts-node": "~7.0.0", | 
			
		||||
 | 
				    "tslint": "~5.11.0", | 
			
		||||
 | 
				    "typescript": "~3.2.2" | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				} | 
			
		||||
@ -0,0 +1,37 @@ | 
			
		|||||
 | 
				import { NgModule } from '@angular/core'; | 
			
		||||
 | 
				import { Routes, RouterModule } from '@angular/router'; | 
			
		||||
 | 
				import { MainComponent } from './main/main.component'; | 
			
		||||
 | 
				import { LoginComponent } from './login/login.component'; | 
			
		||||
 | 
				import { RegisterComponent } from './register/register.component'; | 
			
		||||
 | 
				import { CampaignComponent } from './campaign/campaign.component'; | 
			
		||||
 | 
				import { CampaignNewComponent } from './campaign-new/campaign-new.component'; | 
			
		||||
 | 
				import { CampaignViewComponent } from './campaign-view/campaign-view.component'; | 
			
		||||
 | 
				import { PhonebookComponent } from './phonebook/phonebook.component'; | 
			
		||||
 | 
				import { PhonebookNewComponent } from './phonebook-new/phonebook-new.component'; | 
			
		||||
 | 
				import { TextTemplateComponent } from './text-template/text-template.component'; | 
			
		||||
 | 
				import { TextTemplateNewComponent } from './text-template-new/text-template-new.component'; | 
			
		||||
 | 
				import { ProviderComponent } from './provider/provider.component'; | 
			
		||||
 | 
				import { ProfileComponent } from './profile/profile.component'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				const routes: Routes = [ | 
			
		||||
 | 
				  { path: '', component: MainComponent }, | 
			
		||||
 | 
				  { path: 'login', component: LoginComponent }, | 
			
		||||
 | 
				  { path: 'register', component: RegisterComponent }, | 
			
		||||
 | 
				  { path: 'profile', component: ProfileComponent }, | 
			
		||||
 | 
				  { path: 'campaign', component: CampaignComponent }, | 
			
		||||
 | 
				  { path: 'campaign/new', component: CampaignNewComponent }, | 
			
		||||
 | 
				  { path: 'campaign/:id/view', component: CampaignViewComponent }, | 
			
		||||
 | 
				  { path: 'phonebook', component: PhonebookComponent }, | 
			
		||||
 | 
				  { path: 'phonebook/new', component: PhonebookNewComponent }, | 
			
		||||
 | 
				  { path: 'phonebook/:id/edit', component: PhonebookNewComponent }, | 
			
		||||
 | 
				  { path: 'text-template', component: TextTemplateComponent }, | 
			
		||||
 | 
				  { path: 'text-template/new', component: TextTemplateNewComponent }, | 
			
		||||
 | 
				  { path: 'text-template/:id/edit', component: TextTemplateNewComponent }, | 
			
		||||
 | 
				  { path: 'provider', component: ProviderComponent }, | 
			
		||||
 | 
				  ]; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				@NgModule({ | 
			
		||||
 | 
				  imports: [RouterModule.forRoot(routes)], | 
			
		||||
 | 
				  exports: [RouterModule] | 
			
		||||
 | 
				}) | 
			
		||||
 | 
				export class AppRoutingModule { } | 
			
		||||
@ -0,0 +1,42 @@ | 
			
		|||||
 | 
				<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> | 
			
		||||
 | 
				  <a class="navbar-brand" routerLink="/">Tap It!</a> | 
			
		||||
 | 
				  <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> | 
			
		||||
 | 
				    <span class="navbar-toggler-icon"></span> | 
			
		||||
 | 
				  </button> | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  <div class="collapse navbar-collapse" id="navbarSupportedContent"> | 
			
		||||
 | 
				    <ul class="navbar-nav"> | 
			
		||||
 | 
				      <li *ngFor="let navlink of navlinks" data-toggle="collapse" data-target="#navbarNav" class="nav-item"> | 
			
		||||
 | 
				        <a class="nav-link" *ngIf="navlink.loginOnly === authService.loggedin" [ngClass]="{'active': router.url === navlink.link}" routerLink="/{{ navlink.link }}"> | 
			
		||||
 | 
				          {{ navlink.name }} | 
			
		||||
 | 
				          <span *ngIf="this.router.url === navlink.link" class="sr-only">(current)</span> | 
			
		||||
 | 
				        </a> | 
			
		||||
 | 
				      </li> | 
			
		||||
 | 
				      <li class="nav-item dropdown" *ngIf="authService.loggedin === true"> | 
			
		||||
 | 
				          <a class="nav-link dropdown-toggle" routerLink="{{ router.url }}" id="navbarDropdownMenuLink" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> | 
			
		||||
 | 
				            Settings | 
			
		||||
 | 
				          </a> | 
			
		||||
 | 
				          <div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink"> | 
			
		||||
 | 
				            <a class="dropdown-item" routerLink="/profile">Profile</a> | 
			
		||||
 | 
				            <a class="dropdown-item" routerLink="/provider">Providers</a> | 
			
		||||
 | 
				            <a class="dropdown-item" routerLink="/globalsettings">Global Settings</a> | 
			
		||||
 | 
				          </div> | 
			
		||||
 | 
				      </li> | 
			
		||||
 | 
				    </ul> | 
			
		||||
 | 
				    <ul class="navbar-nav ml-auto"> | 
			
		||||
 | 
				      <li *ngIf="authService.loggedin" data-toggle="collapse" data-target="#navbarNav" class="nav-item"> | 
			
		||||
 | 
				        <a class="nav-link" routerLink="/" (click)="authService.logout()">Log Out</a> | 
			
		||||
 | 
				      </li> | 
			
		||||
 | 
				      <li *ngIf="!authService.loggedin" data-toggle="collapse" data-target="#navbarNav" class="nav-item"> | 
			
		||||
 | 
				        <a class="nav-link" routerLink="/login">Login</a> | 
			
		||||
 | 
				      </li> | 
			
		||||
 | 
				    </ul> | 
			
		||||
 | 
				  </div> | 
			
		||||
 | 
				</nav> | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				<main class="container-fluid pt-2"> | 
			
		||||
 | 
				  <router-outlet></router-outlet> | 
			
		||||
 | 
				</main> | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				<div class="fixed-bottom"><app-notification></app-notification></div> | 
			
		||||
@ -0,0 +1,35 @@ | 
			
		|||||
 | 
				import { TestBed, async } from '@angular/core/testing'; | 
			
		||||
 | 
				import { RouterTestingModule } from '@angular/router/testing'; | 
			
		||||
 | 
				import { AppComponent } from './app.component'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				describe('AppComponent', () => { | 
			
		||||
 | 
				  beforeEach(async(() => { | 
			
		||||
 | 
				    TestBed.configureTestingModule({ | 
			
		||||
 | 
				      imports: [ | 
			
		||||
 | 
				        RouterTestingModule | 
			
		||||
 | 
				      ], | 
			
		||||
 | 
				      declarations: [ | 
			
		||||
 | 
				        AppComponent | 
			
		||||
 | 
				      ], | 
			
		||||
 | 
				    }).compileComponents(); | 
			
		||||
 | 
				  })); | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  it('should create the app', () => { | 
			
		||||
 | 
				    const fixture = TestBed.createComponent(AppComponent); | 
			
		||||
 | 
				    const app = fixture.debugElement.componentInstance; | 
			
		||||
 | 
				    expect(app).toBeTruthy(); | 
			
		||||
 | 
				  }); | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  it(`should have as title 'tapit-frontend'`, () => { | 
			
		||||
 | 
				    const fixture = TestBed.createComponent(AppComponent); | 
			
		||||
 | 
				    const app = fixture.debugElement.componentInstance; | 
			
		||||
 | 
				    expect(app.title).toEqual('tapit-frontend'); | 
			
		||||
 | 
				  }); | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  it('should render title in a h1 tag', () => { | 
			
		||||
 | 
				    const fixture = TestBed.createComponent(AppComponent); | 
			
		||||
 | 
				    fixture.detectChanges(); | 
			
		||||
 | 
				    const compiled = fixture.debugElement.nativeElement; | 
			
		||||
 | 
				    expect(compiled.querySelector('h1').textContent).toContain('Welcome to tapit-frontend!'); | 
			
		||||
 | 
				  }); | 
			
		||||
 | 
				}); | 
			
		||||
@ -0,0 +1,37 @@ | 
			
		|||||
 | 
				import { Component } from '@angular/core'; | 
			
		||||
 | 
				import { RouterModule, Routes, Router } from '@angular/router'; | 
			
		||||
 | 
				import { AuthService } from './auth.service'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				@Component({ | 
			
		||||
 | 
				  selector: 'app-root', | 
			
		||||
 | 
				  templateUrl: './app.component.html', | 
			
		||||
 | 
				  styleUrls: ['./app.component.css'] | 
			
		||||
 | 
				}) | 
			
		||||
 | 
				export class AppComponent { | 
			
		||||
 | 
				  title = 'tapit-frontend'; | 
			
		||||
 | 
				  navlinks = [ | 
			
		||||
 | 
				    { | 
			
		||||
 | 
				      link: '/campaign', | 
			
		||||
 | 
				      name: 'Campaigns', | 
			
		||||
 | 
				      loginOnly: true, | 
			
		||||
 | 
				    }, | 
			
		||||
 | 
				    { | 
			
		||||
 | 
				      link: '/phonebook', | 
			
		||||
 | 
				      name: 'Phonebook', | 
			
		||||
 | 
				      loginOnly: true, | 
			
		||||
 | 
				    }, | 
			
		||||
 | 
				    { | 
			
		||||
 | 
				      link: '/text-template', | 
			
		||||
 | 
				      name: 'Text Templates', | 
			
		||||
 | 
				      loginOnly: true, | 
			
		||||
 | 
				    }, | 
			
		||||
 | 
				    { | 
			
		||||
 | 
				      link: '/web-template', | 
			
		||||
 | 
				      name: 'Web Templates', | 
			
		||||
 | 
				      loginOnly: true, | 
			
		||||
 | 
				    }, | 
			
		||||
 | 
				  ]; | 
			
		||||
 | 
				  constructor( private router: Router, private authService: AuthService) { | 
			
		||||
 | 
				    authService.getUser(); | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				} | 
			
		||||
@ -0,0 +1,48 @@ | 
			
		|||||
 | 
				import { BrowserModule } from '@angular/platform-browser'; | 
			
		||||
 | 
				import { NgModule } from '@angular/core'; | 
			
		||||
 | 
				import { HttpClientModule } from '@angular/common/http'; | 
			
		||||
 | 
				import { FormsModule } from '@angular/forms'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				import { AppRoutingModule } from './app-routing.module'; | 
			
		||||
 | 
				import { AppComponent } from './app.component'; | 
			
		||||
 | 
				import { MainComponent } from './main/main.component'; | 
			
		||||
 | 
				import { CampaignComponent } from './campaign/campaign.component'; | 
			
		||||
 | 
				import { CampaignNewComponent } from './campaign-new/campaign-new.component'; | 
			
		||||
 | 
				import { NotificationComponent } from './notification/notification.component'; | 
			
		||||
 | 
				import { PhonebookComponent } from './phonebook/phonebook.component'; | 
			
		||||
 | 
				import { PhonebookNewComponent } from './phonebook-new/phonebook-new.component'; | 
			
		||||
 | 
				import { TextTemplateComponent } from './text-template/text-template.component'; | 
			
		||||
 | 
				import { TextTemplateNewComponent } from './text-template-new/text-template-new.component'; | 
			
		||||
 | 
				import { LoginComponent } from './login/login.component'; | 
			
		||||
 | 
				import { RegisterComponent } from './register/register.component'; | 
			
		||||
 | 
				import { ProviderComponent } from './provider/provider.component'; | 
			
		||||
 | 
				import { ProfileComponent } from './profile/profile.component'; | 
			
		||||
 | 
				import { CampaignViewComponent } from './campaign-view/campaign-view.component'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				@NgModule({ | 
			
		||||
 | 
				  declarations: [ | 
			
		||||
 | 
				    AppComponent, | 
			
		||||
 | 
				    MainComponent, | 
			
		||||
 | 
				    CampaignComponent, | 
			
		||||
 | 
				    CampaignNewComponent, | 
			
		||||
 | 
				    NotificationComponent, | 
			
		||||
 | 
				    PhonebookComponent, | 
			
		||||
 | 
				    PhonebookNewComponent, | 
			
		||||
 | 
				    TextTemplateComponent, | 
			
		||||
 | 
				    TextTemplateNewComponent, | 
			
		||||
 | 
				    LoginComponent, | 
			
		||||
 | 
				    RegisterComponent, | 
			
		||||
 | 
				    ProviderComponent, | 
			
		||||
 | 
				    ProfileComponent, | 
			
		||||
 | 
				    CampaignViewComponent | 
			
		||||
 | 
				  ], | 
			
		||||
 | 
				  imports: [ | 
			
		||||
 | 
				    BrowserModule, | 
			
		||||
 | 
				    AppRoutingModule, | 
			
		||||
 | 
				    FormsModule, | 
			
		||||
 | 
				    HttpClientModule, | 
			
		||||
 | 
				  ], | 
			
		||||
 | 
				  providers: [], | 
			
		||||
 | 
				  bootstrap: [AppComponent] | 
			
		||||
 | 
				}) | 
			
		||||
 | 
				export class AppModule { } | 
			
		||||
@ -0,0 +1,12 @@ | 
			
		|||||
 | 
				import { TestBed } from '@angular/core/testing'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				import { AuthService } from './auth.service'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				describe('AuthService', () => { | 
			
		||||
 | 
				  beforeEach(() => TestBed.configureTestingModule({})); | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  it('should be created', () => { | 
			
		||||
 | 
				    const service: AuthService = TestBed.get(AuthService); | 
			
		||||
 | 
				    expect(service).toBeTruthy(); | 
			
		||||
 | 
				  }); | 
			
		||||
 | 
				}); | 
			
		||||
@ -0,0 +1,131 @@ | 
			
		|||||
 | 
				import { Injectable } from '@angular/core'; | 
			
		||||
 | 
				import { Router } from '@angular/router'; | 
			
		||||
 | 
				import { HttpClient, HttpHeaders } from '@angular/common/http'; | 
			
		||||
 | 
				import { NotificationService } from './notification.service'; | 
			
		||||
 | 
				import { Observable, of } from 'rxjs'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export class User { | 
			
		||||
 | 
				  username: string; | 
			
		||||
 | 
				  password: string; | 
			
		||||
 | 
				  name: string; | 
			
		||||
 | 
				  email: string; | 
			
		||||
 | 
				  secretCode: string; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export class UserNotification { | 
			
		||||
 | 
				  resultType: string; | 
			
		||||
 | 
				  text: string; | 
			
		||||
 | 
				  payload: User; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				@Injectable({ | 
			
		||||
 | 
				  providedIn: 'root' | 
			
		||||
 | 
				}) | 
			
		||||
 | 
				export class AuthService { | 
			
		||||
 | 
				  currUser = new User(); | 
			
		||||
 | 
				  loggedin = false; | 
			
		||||
 | 
				  loginUrl = 'api/login'; | 
			
		||||
 | 
				  logoutUrl = 'api/logout'; | 
			
		||||
 | 
				  registerUrl = 'api/register'; | 
			
		||||
 | 
				  myselfUrl = 'api/myself'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  httpOptions = { | 
			
		||||
 | 
				    headers: new HttpHeaders({ | 
			
		||||
 | 
				      'Content-Type': 'application/json', | 
			
		||||
 | 
				    }), | 
			
		||||
 | 
				  }; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  login(username: string, password: string) { | 
			
		||||
 | 
				    this.currUser.username = username; | 
			
		||||
 | 
				    this.currUser.password = password; | 
			
		||||
 | 
				    this.http.post<UserNotification>(this.loginUrl, this.currUser, this.httpOptions).subscribe(usermessage => { | 
			
		||||
 | 
				      if (usermessage.payload !== null) { | 
			
		||||
 | 
				        this.loggedin = true; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        // update user
 | 
			
		||||
 | 
				        this.currUser.username = usermessage.payload.username; | 
			
		||||
 | 
				        this.currUser.email = usermessage.payload.email; | 
			
		||||
 | 
				        this.currUser.name = usermessage.payload.name; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        this.notificationService.addNotification(usermessage.resultType, usermessage.text); | 
			
		||||
 | 
				        this.router.navigate(['/campaign']); | 
			
		||||
 | 
				      } else { | 
			
		||||
 | 
				        this.notificationService.addNotification(usermessage.resultType, usermessage.text); | 
			
		||||
 | 
				      } | 
			
		||||
 | 
				    }, | 
			
		||||
 | 
				    err => { | 
			
		||||
 | 
				        this.notificationService.addNotification('failure', 'Error in logging in'); | 
			
		||||
 | 
				    }); | 
			
		||||
 | 
				    this.currUser.password = ''; | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  register(username: string, password: string, email: string, name: string, secretCode: string) { | 
			
		||||
 | 
				    this.currUser.username = username; | 
			
		||||
 | 
				    this.currUser.password = password; | 
			
		||||
 | 
				    this.currUser.email = email; | 
			
		||||
 | 
				    this.currUser.name = name; | 
			
		||||
 | 
				    this.currUser.secretCode = secretCode; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    this.http.post<UserNotification>(this.registerUrl, this.currUser, this.httpOptions).subscribe(usermessage => { | 
			
		||||
 | 
				      if (usermessage.payload !== null) { | 
			
		||||
 | 
				        this.loggedin = true; | 
			
		||||
 | 
				        this.notificationService.addNotification(usermessage.resultType, usermessage.text); | 
			
		||||
 | 
				        this.router.navigate(['/campaign']); | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        // update user
 | 
			
		||||
 | 
				        this.currUser.username = usermessage.payload.username; | 
			
		||||
 | 
				        this.currUser.email = usermessage.payload.email; | 
			
		||||
 | 
				        this.currUser.name = usermessage.payload.name; | 
			
		||||
 | 
				      } else { | 
			
		||||
 | 
				        this.notificationService.addNotification(usermessage.resultType, usermessage.text); | 
			
		||||
 | 
				      } | 
			
		||||
 | 
				    }); | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    this.currUser.secretCode = ''; | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  logout() { | 
			
		||||
 | 
				    this.http.post<UserNotification>(this.logoutUrl, '', this.httpOptions).subscribe(usermessage => { | 
			
		||||
 | 
				      this.notificationService.addNotification(usermessage.resultType, usermessage.text); | 
			
		||||
 | 
				      this.loggedin = false; | 
			
		||||
 | 
				      this.currUser = new User(); | 
			
		||||
 | 
				      this.router.navigate(['/']); | 
			
		||||
 | 
				    }); | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  getUser(): User { | 
			
		||||
 | 
				    this.http.get<User>(this.myselfUrl, this.httpOptions).subscribe(thisUser => { | 
			
		||||
 | 
				      this.currUser = thisUser; | 
			
		||||
 | 
				      if (this.currUser.username !== '') { | 
			
		||||
 | 
				        this.loggedin = true; | 
			
		||||
 | 
				      } else { | 
			
		||||
 | 
				        this.router.navigate(['/']); | 
			
		||||
 | 
				      } | 
			
		||||
 | 
				      // separate one to redirect main to campaign dashboard
 | 
			
		||||
 | 
				      if (this.router.url === '/' || this.router.url === '') { | 
			
		||||
 | 
				        this.router.navigate(['/campaign']); | 
			
		||||
 | 
				      } | 
			
		||||
 | 
				    }, | 
			
		||||
 | 
				    err => { | 
			
		||||
 | 
				      this.router.navigate(['/']); | 
			
		||||
 | 
				    }); | 
			
		||||
 | 
				    return this.currUser; | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  getUserObs(): Observable<User> { | 
			
		||||
 | 
				    return this.http.get<User>(this.myselfUrl, this.httpOptions); | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  updateUser(user: User) { | 
			
		||||
 | 
				    this.currUser = user; | 
			
		||||
 | 
				    this.http.put<UserNotification>(this.myselfUrl, this.currUser, this.httpOptions).subscribe(usermessage => { | 
			
		||||
 | 
				        this.notificationService.addNotification(usermessage.resultType, usermessage.text); | 
			
		||||
 | 
				    }, | 
			
		||||
 | 
				    err => { | 
			
		||||
 | 
				      this.notificationService.addNotification('failure', 'Error in updating profile'); | 
			
		||||
 | 
				    }); | 
			
		||||
 | 
				    this.currUser.password = ''; | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  constructor(private http: HttpClient, private router: Router, private notificationService: NotificationService) { } | 
			
		||||
 | 
				} | 
			
		||||
@ -0,0 +1,45 @@ | 
			
		|||||
 | 
				<div class="row p-2"> | 
			
		||||
 | 
				  <div class="col-12"> | 
			
		||||
 | 
				    <div class="row mt-3 mb-3"> | 
			
		||||
 | 
				      <div class="col-12 d-flex"> | 
			
		||||
 | 
				        <label for="campaignName" class="pr-2 mt-auto mb-auto">Campaign Name</label> | 
			
		||||
 | 
				        <input type="text" class="flex-grow-1" id="campaignName" [(ngModel)]="newCampaign.name" placeholder="Campaign Name"> | 
			
		||||
 | 
				      </div> | 
			
		||||
 | 
				    </div> | 
			
		||||
 | 
				    <div class="row mt-3 mb-3"> | 
			
		||||
 | 
				      <div class="col-12 d-flex"> | 
			
		||||
 | 
				        <label for="newFromNum" class="pr-2 mt-auto mb-auto">From Number</label> | 
			
		||||
 | 
				        <input type="text" class="flex-grow-1" id="newFromNum" [(ngModel)]="newCampaign.fromNumber" placeholder="From Number"> | 
			
		||||
 | 
				      </div> | 
			
		||||
 | 
				    </div> | 
			
		||||
 | 
				    <!-- Add phonebook & template via list -->   | 
			
		||||
 | 
				  <div class="form-group"> | 
			
		||||
 | 
				    <label for="provider-select">Provider</label> | 
			
		||||
 | 
				    <select class="form-control" [(ngModel)]="newCampaign.providerTag" id="provider-select"> | 
			
		||||
 | 
				      <option></option> | 
			
		||||
 | 
				      <option *ngFor="let providerEnum of providerService.providerEnums" [ngValue]="providerEnum.tag">{{providerEnum.name}}</option> | 
			
		||||
 | 
				    </select> | 
			
		||||
 | 
				  </div> | 
			
		||||
 | 
				  <div class="form-group"> | 
			
		||||
 | 
				    <label for="phonebook-select">Phonebook</label> | 
			
		||||
 | 
				    <select class="form-control" [(ngModel)]="newCampaign.phonebookId" id="phonebook-select"> | 
			
		||||
 | 
				      <option></option> | 
			
		||||
 | 
				      <option *ngFor="let phonebook of phonebookService.phonebooks" [ngValue]="phonebook.id">{{phonebook.name}}: Size {{phonebook.size}}</option> | 
			
		||||
 | 
				    </select> | 
			
		||||
 | 
				  </div> | 
			
		||||
 | 
				  <div class="form-group"> | 
			
		||||
 | 
				    <label for="text-template-select">Text Template</label> | 
			
		||||
 | 
				    <select class="form-control" [(ngModel)]="newCampaign.textTemplateId" id="text-template-select"> | 
			
		||||
 | 
				      <option></option> | 
			
		||||
 | 
				      <option *ngFor="let textTemplate of textTemplateService.textTemplates" [ngValue]="textTemplate.id">{{textTemplate.name}}</option> | 
			
		||||
 | 
				    </select> | 
			
		||||
 | 
				  </div> | 
			
		||||
 | 
				  | 
			
		||||
 | 
				    <div class="row mt-4"> | 
			
		||||
 | 
				      <div class="col-12 d-flex"> | 
			
		||||
 | 
				        <button type="button" (click)="submitNewCampaignRun()" class="btn btn-primary mr-2">Start</button> | 
			
		||||
 | 
				        <button type="button" (click)="submitNewCampaign()" class="btn btn-secondary ml-2">Save</button> | 
			
		||||
 | 
				        <button type="button" *ngIf="router.url !== '/campaign/new'" (click)="askDelete()" class="btn btn-danger ml-auto" data-toggle="modal" data-target="#completeModal">Delete</button> | 
			
		||||
 | 
				      </div> | 
			
		||||
 | 
				    </div> | 
			
		||||
 | 
				</div> | 
			
		||||
@ -0,0 +1,25 @@ | 
			
		|||||
 | 
				import { async, ComponentFixture, TestBed } from '@angular/core/testing'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				import { CampaignNewComponent } from './campaign-new.component'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				describe('CampaignNewComponent', () => { | 
			
		||||
 | 
				  let component: CampaignNewComponent; | 
			
		||||
 | 
				  let fixture: ComponentFixture<CampaignNewComponent>; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  beforeEach(async(() => { | 
			
		||||
 | 
				    TestBed.configureTestingModule({ | 
			
		||||
 | 
				      declarations: [ CampaignNewComponent ] | 
			
		||||
 | 
				    }) | 
			
		||||
 | 
				    .compileComponents(); | 
			
		||||
 | 
				  })); | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  beforeEach(() => { | 
			
		||||
 | 
				    fixture = TestBed.createComponent(CampaignNewComponent); | 
			
		||||
 | 
				    component = fixture.componentInstance; | 
			
		||||
 | 
				    fixture.detectChanges(); | 
			
		||||
 | 
				  }); | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  it('should create', () => { | 
			
		||||
 | 
				    expect(component).toBeTruthy(); | 
			
		||||
 | 
				  }); | 
			
		||||
 | 
				}); | 
			
		||||
@ -0,0 +1,35 @@ | 
			
		|||||
 | 
				import { Component, OnInit } from '@angular/core'; | 
			
		||||
 | 
				import { CampaignService, Campaign } from '../campaign.service'; | 
			
		||||
 | 
				import { Router, ActivatedRoute, ParamMap } from '@angular/router'; | 
			
		||||
 | 
				import { ProviderService } from '../provider.service'; | 
			
		||||
 | 
				import { PhonebookService } from '../phonebook.service'; | 
			
		||||
 | 
				import { TextTemplateService } from '../text-template.service'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				@Component({ | 
			
		||||
 | 
				  selector: 'app-campaign-new', | 
			
		||||
 | 
				  templateUrl: './campaign-new.component.html', | 
			
		||||
 | 
				  styleUrls: ['./campaign-new.component.css'] | 
			
		||||
 | 
				}) | 
			
		||||
 | 
				export class CampaignNewComponent implements OnInit { | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  constructor( | 
			
		||||
 | 
				          private campaignService: CampaignService, | 
			
		||||
 | 
				          private router: Router, | 
			
		||||
 | 
				          private providerService: ProviderService, | 
			
		||||
 | 
				          private phonebookService: PhonebookService, | 
			
		||||
 | 
				          private textTemplateService: TextTemplateService) { } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  newCampaign: Campaign = new Campaign(); | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  submitNewCampaign() { | 
			
		||||
 | 
				    this.campaignService.addCampaign(this.newCampaign); | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  submitNewCampaignRun() { | 
			
		||||
 | 
				    this.campaignService.addCampaignRun(this.newCampaign); | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  ngOnInit() { | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				} | 
			
		||||
@ -0,0 +1,3 @@ | 
			
		|||||
 | 
				.campaign-details:read-only { | 
			
		||||
 | 
				    background-color: white; | 
			
		||||
 | 
				} | 
			
		||||
@ -0,0 +1,77 @@ | 
			
		|||||
 | 
				<div class="row"> | 
			
		||||
 | 
				  <div class="col-12 mb-3 d-flux"> | 
			
		||||
 | 
				    <button type="button" *ngIf="currCampaign.currentStatus === 'Running'" (click)="pauseCampaign()" class="btn btn-warning mr-2">Pause Campaign</button> | 
			
		||||
 | 
				    <button type="button" *ngIf="currCampaign.currentStatus !== 'Running'" (click)="startCampaign()" class="btn btn-primary mr-2">Start Campaign</button> | 
			
		||||
 | 
				    <button type="button" class="btn btn-danger ml-auto" data-toggle="modal" data-target="#completeModal">Delete</button> | 
			
		||||
 | 
				  </div> | 
			
		||||
 | 
				</div> | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				<div class="row"> | 
			
		||||
 | 
				  <div class="col-12"> | 
			
		||||
 | 
				    <div class="input-group mb-3"> | 
			
		||||
 | 
				      <div class="input-group-prepend"> | 
			
		||||
 | 
				        <span class="input-group-text">Campaign Name</span> | 
			
		||||
 | 
				      </div> | 
			
		||||
 | 
				      <input type="text" class="form-control campaign-details" value="{{ currCampaign.name }}" readonly> | 
			
		||||
 | 
				    </div> | 
			
		||||
 | 
				    <div class="input-group mb-3"> | 
			
		||||
 | 
				      <div class="input-group-prepend"> | 
			
		||||
 | 
				        <span class="input-group-text">Campaign Size</span> | 
			
		||||
 | 
				      </div> | 
			
		||||
 | 
				      <input type="text" class="form-control campaign-details" value="{{ currCampaign.size }}" readonly> | 
			
		||||
 | 
				    </div> | 
			
		||||
 | 
				    <div class="input-group mb-3"> | 
			
		||||
 | 
				      <div class="input-group-prepend"> | 
			
		||||
 | 
				        <span class="input-group-text">Campaign Status</span> | 
			
		||||
 | 
				      </div> | 
			
		||||
 | 
				      <input type="text" class="form-control campaign-details" value="{{ currCampaign.currentStatus }}" readonly> | 
			
		||||
 | 
				    </div> | 
			
		||||
 | 
				  </div> | 
			
		||||
 | 
				</div> | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				<div class="row mt-2"> | 
			
		||||
 | 
				  <div class="col-12"> | 
			
		||||
 | 
				    <table class="table table-hover"> | 
			
		||||
 | 
				      <thead class="thead-dark"> | 
			
		||||
 | 
				        <tr> | 
			
		||||
 | 
				          <th scope="col">From</th> | 
			
		||||
 | 
				          <th scope="col">To</th> | 
			
		||||
 | 
				          <th scope="col">Currrent Status</th> | 
			
		||||
 | 
				          <th scope="col">Time Sent</th> | 
			
		||||
 | 
				        </tr> | 
			
		||||
 | 
				      </thead> | 
			
		||||
 | 
				      <tbody> | 
			
		||||
 | 
				        <ng-container *ngFor="let job of currCampaign.jobs"> | 
			
		||||
 | 
				          <tr> | 
			
		||||
 | 
				            <td>{{ job.fromNum }}</td> | 
			
		||||
 | 
				            <td>{{ job.toNum }}</td> | 
			
		||||
 | 
				            <td>{{ job.currentStatus }}</td> | 
			
		||||
 | 
				            <td>{{ job.timeSent | date:'dd-MMM-yyyy'}}</td> | 
			
		||||
 | 
				          </tr> | 
			
		||||
 | 
				        </ng-container> | 
			
		||||
 | 
				      </tbody> | 
			
		||||
 | 
				    </table> | 
			
		||||
 | 
				  </div> | 
			
		||||
 | 
				</div> | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				<div class="modal fade" id="completeModal" tabindex="-1" role="dialog" aria-labelledby="completeModal" aria-hidden="true"> | 
			
		||||
 | 
				  <div class="modal-dialog" role="document"> | 
			
		||||
 | 
				    <div class="modal-content"> | 
			
		||||
 | 
				      <div class="modal-header"> | 
			
		||||
 | 
				        <h5 class="modal-title" id="exampleModalLabel">{{ currCampaign.name }}</h5> | 
			
		||||
 | 
				        <button type="button" class="close" data-dismiss="modal" aria-label="Close"> | 
			
		||||
 | 
				          <span aria-hidden="true">×</span> | 
			
		||||
 | 
				        </button> | 
			
		||||
 | 
				      </div> | 
			
		||||
 | 
				      <div class="modal-body"> | 
			
		||||
 | 
				        <p>Are you sure you want to delete the campaign?</p> | 
			
		||||
 | 
				      </div> | 
			
		||||
 | 
				      <div class="modal-footer"> | 
			
		||||
 | 
				        <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> | 
			
		||||
 | 
				        <button type="button"  class="btn btn-danger" (click)="deleteCampaign()" data-dismiss="modal">Delete Campaign</button> | 
			
		||||
 | 
				      </div> | 
			
		||||
 | 
				    </div> | 
			
		||||
 | 
				  </div> | 
			
		||||
 | 
				</div> | 
			
		||||
@ -0,0 +1,25 @@ | 
			
		|||||
 | 
				import { async, ComponentFixture, TestBed } from '@angular/core/testing'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				import { CampaignViewComponent } from './campaign-view.component'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				describe('CampaignViewComponent', () => { | 
			
		||||
 | 
				  let component: CampaignViewComponent; | 
			
		||||
 | 
				  let fixture: ComponentFixture<CampaignViewComponent>; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  beforeEach(async(() => { | 
			
		||||
 | 
				    TestBed.configureTestingModule({ | 
			
		||||
 | 
				      declarations: [ CampaignViewComponent ] | 
			
		||||
 | 
				    }) | 
			
		||||
 | 
				    .compileComponents(); | 
			
		||||
 | 
				  })); | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  beforeEach(() => { | 
			
		||||
 | 
				    fixture = TestBed.createComponent(CampaignViewComponent); | 
			
		||||
 | 
				    component = fixture.componentInstance; | 
			
		||||
 | 
				    fixture.detectChanges(); | 
			
		||||
 | 
				  }); | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  it('should create', () => { | 
			
		||||
 | 
				    expect(component).toBeTruthy(); | 
			
		||||
 | 
				  }); | 
			
		||||
 | 
				}); | 
			
		||||
@ -0,0 +1,69 @@ | 
			
		|||||
 | 
				import { Component, OnInit } from '@angular/core'; | 
			
		||||
 | 
				import { CampaignService, Campaign, Job, CampaignNotification } from '../campaign.service'; | 
			
		||||
 | 
				import { Router, ActivatedRoute, ParamMap } from '@angular/router'; | 
			
		||||
 | 
				import { NotificationService } from '../notification.service'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				@Component({ | 
			
		||||
 | 
				  selector: 'app-campaign-view', | 
			
		||||
 | 
				  templateUrl: './campaign-view.component.html', | 
			
		||||
 | 
				  styleUrls: ['./campaign-view.component.css'] | 
			
		||||
 | 
				}) | 
			
		||||
 | 
				export class CampaignViewComponent implements OnInit { | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  currCampaign: Campaign = new Campaign(); | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  id = 0; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  constructor( | 
			
		||||
 | 
				        private campaignService: CampaignService, | 
			
		||||
 | 
				        private router: Router, | 
			
		||||
 | 
				        private route: ActivatedRoute, | 
			
		||||
 | 
				        private notificationService: NotificationService | 
			
		||||
 | 
				  ) { } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  startCampaign() { | 
			
		||||
 | 
				    this.campaignService.startCampaign(this.currCampaign).subscribe(campaignNotification => { | 
			
		||||
 | 
				      this.notificationService.addNotification(campaignNotification.resultType, campaignNotification.text); | 
			
		||||
 | 
				      this.campaignService.getCampaignObs(this.id).subscribe(campaign => { | 
			
		||||
 | 
				        this.currCampaign = campaign; | 
			
		||||
 | 
				      }); | 
			
		||||
 | 
				    }, | 
			
		||||
 | 
				    err => { | 
			
		||||
 | 
				      this.notificationService.addNotification('failure', 'Error in starting campaign'); | 
			
		||||
 | 
				    }); | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  pauseCampaign() { | 
			
		||||
 | 
				    this.campaignService.pauseCampaign(this.currCampaign).subscribe(campaignNotification => { | 
			
		||||
 | 
				      this.notificationService.addNotification(campaignNotification.resultType, campaignNotification.text); | 
			
		||||
 | 
				    }, | 
			
		||||
 | 
				    err => { | 
			
		||||
 | 
				      this.notificationService.addNotification('failure', 'Error in pausing campaign'); | 
			
		||||
 | 
				    }); | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  deleteCampaign() { | 
			
		||||
 | 
				    this.campaignService.deleteCampaign(this.currCampaign); | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  updateThisCampaign() { | 
			
		||||
 | 
				      this.campaignService.getCampaignObs(this.id).subscribe(campaign => { | 
			
		||||
 | 
				        this.currCampaign = campaign; | 
			
		||||
 | 
				      }); | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  ngOnInit() { | 
			
		||||
 | 
				      const idParam = 'id'; | 
			
		||||
 | 
				      this.route.params.subscribe( params => { | 
			
		||||
 | 
				        this.id = parseInt(params[idParam], 10); | 
			
		||||
 | 
				      }); | 
			
		||||
 | 
				      this.updateThisCampaign(); | 
			
		||||
 | 
				      const intervalId = setInterval(() => { | 
			
		||||
 | 
				        this.updateThisCampaign(); | 
			
		||||
 | 
				        if (!this.router.url.includes('/campaign')) { | 
			
		||||
 | 
				          clearInterval(intervalId); | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				      }, 2000); | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				} | 
			
		||||
@ -0,0 +1,12 @@ | 
			
		|||||
 | 
				import { TestBed } from '@angular/core/testing'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				import { CampaignService } from './campaign.service'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				describe('CampaignService', () => { | 
			
		||||
 | 
				  beforeEach(() => TestBed.configureTestingModule({})); | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  it('should be created', () => { | 
			
		||||
 | 
				    const service: CampaignService = TestBed.get(CampaignService); | 
			
		||||
 | 
				    expect(service).toBeTruthy(); | 
			
		||||
 | 
				  }); | 
			
		||||
 | 
				}); | 
			
		||||
@ -0,0 +1,115 @@ | 
			
		|||||
 | 
				import { Injectable } from '@angular/core'; | 
			
		||||
 | 
				import { Observable, of } from 'rxjs'; | 
			
		||||
 | 
				import { Router } from '@angular/router'; | 
			
		||||
 | 
				import { HttpClient, HttpHeaders } from '@angular/common/http'; | 
			
		||||
 | 
				import { NotificationService } from './notification.service'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export class Campaign { | 
			
		||||
 | 
				  id: number; | 
			
		||||
 | 
				  name: string; | 
			
		||||
 | 
				  fromNumber: string; | 
			
		||||
 | 
				  size: number; | 
			
		||||
 | 
				  currentStatus: string; | 
			
		||||
 | 
				  createDate: Date; | 
			
		||||
 | 
				  phonebookId: number; | 
			
		||||
 | 
				  textTemplateId: number; | 
			
		||||
 | 
				  webTemplateId: number; | 
			
		||||
 | 
				  providerTag: string; | 
			
		||||
 | 
				  jobs: Job[]; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export class Job { | 
			
		||||
 | 
				  id: number; | 
			
		||||
 | 
				  currentStatus: string; | 
			
		||||
 | 
				  timeSent: Date; | 
			
		||||
 | 
				  fromNum: string; | 
			
		||||
 | 
				  toNum: string; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export class CampaignNotification { | 
			
		||||
 | 
				  resultType: string; | 
			
		||||
 | 
				  text: string; | 
			
		||||
 | 
				  payload: Campaign; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				@Injectable({ | 
			
		||||
 | 
				  providedIn: 'root' | 
			
		||||
 | 
				}) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export class CampaignService { | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  campaigns: Campaign[] = []; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  campaignUrl = '/api/campaign'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  httpOptions = { | 
			
		||||
 | 
				    headers: new HttpHeaders({ | 
			
		||||
 | 
				      'Content-Type': 'application/json', | 
			
		||||
 | 
				    }), | 
			
		||||
 | 
				  }; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  getCampaigns() { | 
			
		||||
 | 
				    this.http.get<Campaign[]>(this.campaignUrl).subscribe(campaigns => { | 
			
		||||
 | 
				      if (campaigns === null) { | 
			
		||||
 | 
				        this.campaigns = []; | 
			
		||||
 | 
				      } else { | 
			
		||||
 | 
				        this.campaigns = campaigns; | 
			
		||||
 | 
				      } | 
			
		||||
 | 
				    }); | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  getCampaignObs(id: number): Observable<Campaign> { | 
			
		||||
 | 
				    return this.http.get<Campaign>(this.campaignUrl + '/' + id.toString()); | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  addCampaign(newCampaign: Campaign) { | 
			
		||||
 | 
				    this.http.post<CampaignNotification>(this.campaignUrl, newCampaign, this.httpOptions).subscribe(campaignNotification => { | 
			
		||||
 | 
				      this.notificationService.addNotification(campaignNotification.resultType, campaignNotification.text); | 
			
		||||
 | 
				      this.campaigns.push(campaignNotification.payload); | 
			
		||||
 | 
				      if (campaignNotification.payload !== null) { | 
			
		||||
 | 
				        this.router.navigate(['/campaign']); | 
			
		||||
 | 
				      } | 
			
		||||
 | 
				    }, | 
			
		||||
 | 
				    err => { | 
			
		||||
 | 
				      this.notificationService.addNotification('failure', 'Error in creating template'); | 
			
		||||
 | 
				    }); | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  addCampaignRun(newCampaign: Campaign) { | 
			
		||||
 | 
				    this.http.post<CampaignNotification>(this.campaignUrl, newCampaign, this.httpOptions).subscribe(campaignNotification => { | 
			
		||||
 | 
				      this.notificationService.addNotification(campaignNotification.resultType, campaignNotification.text); | 
			
		||||
 | 
				      this.campaigns.push(campaignNotification.payload); | 
			
		||||
 | 
				      if (campaignNotification.payload !== null) { | 
			
		||||
 | 
				        this.startCampaign(campaignNotification.payload).subscribe(); | 
			
		||||
 | 
				        this.router.navigate(['/campaign']); | 
			
		||||
 | 
				      } | 
			
		||||
 | 
				    }, | 
			
		||||
 | 
				    err => { | 
			
		||||
 | 
				      this.notificationService.addNotification('failure', 'Error in creating template'); | 
			
		||||
 | 
				    }); | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  deleteCampaign(campaign: Campaign) { | 
			
		||||
 | 
				    this.http.delete<CampaignNotification>(this.campaignUrl + '/' + campaign.id.toString(), this.httpOptions) | 
			
		||||
 | 
				      .subscribe(campaignNotification => { | 
			
		||||
 | 
				        this.notificationService.addNotification(campaignNotification.resultType, campaignNotification.text); | 
			
		||||
 | 
				        this.router.navigate(['/campaign']); | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      err => { | 
			
		||||
 | 
				        this.notificationService.addNotification('failure', 'Error in deleting campaign'); | 
			
		||||
 | 
				      }); | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  startCampaign(campaign: Campaign) { | 
			
		||||
 | 
				    return this.http.get<CampaignNotification>(this.campaignUrl + '/' + campaign.id.toString() + '/' + 'start'); | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  pauseCampaign(campaign: Campaign) { | 
			
		||||
 | 
				    return this.http.get<CampaignNotification>(this.campaignUrl + '/' + campaign.id.toString() + '/' + 'pause'); | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  constructor(private http: HttpClient, private router: Router, private notificationService: NotificationService) { | 
			
		||||
 | 
				    this.campaigns = []; | 
			
		||||
 | 
				    this.getCampaigns(); | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				} | 
			
		||||
@ -0,0 +1,31 @@ | 
			
		|||||
 | 
				<div class="row"> | 
			
		||||
 | 
				  <div class="col-12"> | 
			
		||||
 | 
				    <button class="btn btn-primary" routerLink="/campaign/new">New Campaign</button> | 
			
		||||
 | 
				  </div> | 
			
		||||
 | 
				</div> | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				<div class="row mt-2"> | 
			
		||||
 | 
				  <div class="col-12"> | 
			
		||||
 | 
				    <table class="table table-hover"> | 
			
		||||
 | 
				      <thead class="thead-dark"> | 
			
		||||
 | 
				        <tr> | 
			
		||||
 | 
				          <th scope="col">Name</th> | 
			
		||||
 | 
				          <th scope="col">Status</th> | 
			
		||||
 | 
				          <th scope="col">Target Size</th> | 
			
		||||
 | 
				          <th scope="col">Create Date</th> | 
			
		||||
 | 
				        </tr> | 
			
		||||
 | 
				      </thead> | 
			
		||||
 | 
				      <tbody> | 
			
		||||
 | 
				        <ng-container *ngFor="let campaign of campaignService.campaigns"> | 
			
		||||
 | 
				          <tr routerLink="/campaign/{{ campaign.id }}/view"> | 
			
		||||
 | 
				            <td>{{ campaign.name }}</td> | 
			
		||||
 | 
				            <td>{{ campaign.currentStatus }}</td> | 
			
		||||
 | 
				            <td>{{ campaign.size }}</td> | 
			
		||||
 | 
				            <td>{{ campaign.createDate | date:'dd-MMM-yyyy'}}</td> | 
			
		||||
 | 
				          </tr> | 
			
		||||
 | 
				        </ng-container> | 
			
		||||
 | 
				        <p *ngIf="campaignService.campaigns.length === 0">No campaigns created yet. Create compaigns by clicking <a routerLink="/campaign/new">here</a></p> | 
			
		||||
 | 
				      </tbody> | 
			
		||||
 | 
				    </table> | 
			
		||||
 | 
				  </div> | 
			
		||||
 | 
				</div> | 
			
		||||
@ -0,0 +1,25 @@ | 
			
		|||||
 | 
				import { async, ComponentFixture, TestBed } from '@angular/core/testing'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				import { CampaignComponent } from './campaign.component'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				describe('CampaignComponent', () => { | 
			
		||||
 | 
				  let component: CampaignComponent; | 
			
		||||
 | 
				  let fixture: ComponentFixture<CampaignComponent>; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  beforeEach(async(() => { | 
			
		||||
 | 
				    TestBed.configureTestingModule({ | 
			
		||||
 | 
				      declarations: [ CampaignComponent ] | 
			
		||||
 | 
				    }) | 
			
		||||
 | 
				    .compileComponents(); | 
			
		||||
 | 
				  })); | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  beforeEach(() => { | 
			
		||||
 | 
				    fixture = TestBed.createComponent(CampaignComponent); | 
			
		||||
 | 
				    component = fixture.componentInstance; | 
			
		||||
 | 
				    fixture.detectChanges(); | 
			
		||||
 | 
				  }); | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  it('should create', () => { | 
			
		||||
 | 
				    expect(component).toBeTruthy(); | 
			
		||||
 | 
				  }); | 
			
		||||
 | 
				}); | 
			
		||||
@ -0,0 +1,24 @@ | 
			
		|||||
 | 
				import { Component, OnInit } from '@angular/core'; | 
			
		||||
 | 
				import { Router, ActivatedRoute, ParamMap } from '@angular/router'; | 
			
		||||
 | 
				import { CampaignService } from '../campaign.service'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				@Component({ | 
			
		||||
 | 
				  selector: 'app-campaign', | 
			
		||||
 | 
				  templateUrl: './campaign.component.html', | 
			
		||||
 | 
				  styleUrls: ['./campaign.component.css'] | 
			
		||||
 | 
				}) | 
			
		||||
 | 
				export class CampaignComponent implements OnInit { | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  constructor(private campaignService: CampaignService, private router: Router) { } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  ngOnInit() { | 
			
		||||
 | 
				    this.campaignService.getCampaigns(); | 
			
		||||
 | 
				    const intervalId = setInterval(() => { | 
			
		||||
 | 
				      this.campaignService.getCampaigns(); | 
			
		||||
 | 
				      if (!this.router.url.includes('/campaign')) { | 
			
		||||
 | 
				        clearInterval(intervalId); | 
			
		||||
 | 
				      } | 
			
		||||
 | 
				    }, 2000); | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				} | 
			
		||||
@ -0,0 +1,27 @@ | 
			
		|||||
 | 
				<div class="row"> | 
			
		||||
 | 
				  <div class="col-12"> | 
			
		||||
 | 
				    <div class="row mt-3"> | 
			
		||||
 | 
				      <div class="col-12 d-flex"> | 
			
		||||
 | 
				        <h4>Login</h4> | 
			
		||||
 | 
				      </div> | 
			
		||||
 | 
				    </div> | 
			
		||||
 | 
				    <div class="row mt-3"> | 
			
		||||
 | 
				      <div class="col-12 d-flex"> | 
			
		||||
 | 
				        <label for="login-username" class="pr-2 mt-auto mb-auto">Username</label> | 
			
		||||
 | 
				        <input type="text" class="flex-grow-1" id="login-username" [(ngModel)]="username" > | 
			
		||||
 | 
				      </div> | 
			
		||||
 | 
				    </div> | 
			
		||||
 | 
				    <div class="row mt-3"> | 
			
		||||
 | 
				      <div class="col-12 d-flex"> | 
			
		||||
 | 
				        <label for="login-password" class="pr-2 mt-auto mb-auto">Password</label> | 
			
		||||
 | 
				        <input type="password" class="flex-grow-1" id="login-password" [(ngModel)]="password" > | 
			
		||||
 | 
				      </div> | 
			
		||||
 | 
				    </div> | 
			
		||||
 | 
				    <div class="row mt-3"> | 
			
		||||
 | 
				      <div class="col-12 d-flex"> | 
			
		||||
 | 
				        <button type="button" (click)="login()" class="btn btn-primary mr-3">Login</button> | 
			
		||||
 | 
				        <button type="button" (click)="routeRegister()" class="btn btn-primary">Register</button> | 
			
		||||
 | 
				      </div> | 
			
		||||
 | 
				    </div> | 
			
		||||
 | 
				  </div> | 
			
		||||
 | 
				<div> | 
			
		||||
@ -0,0 +1,25 @@ | 
			
		|||||
 | 
				import { async, ComponentFixture, TestBed } from '@angular/core/testing'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				import { LoginComponent } from './login.component'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				describe('LoginComponent', () => { | 
			
		||||
 | 
				  let component: LoginComponent; | 
			
		||||
 | 
				  let fixture: ComponentFixture<LoginComponent>; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  beforeEach(async(() => { | 
			
		||||
 | 
				    TestBed.configureTestingModule({ | 
			
		||||
 | 
				      declarations: [ LoginComponent ] | 
			
		||||
 | 
				    }) | 
			
		||||
 | 
				    .compileComponents(); | 
			
		||||
 | 
				  })); | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  beforeEach(() => { | 
			
		||||
 | 
				    fixture = TestBed.createComponent(LoginComponent); | 
			
		||||
 | 
				    component = fixture.componentInstance; | 
			
		||||
 | 
				    fixture.detectChanges(); | 
			
		||||
 | 
				  }); | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  it('should create', () => { | 
			
		||||
 | 
				    expect(component).toBeTruthy(); | 
			
		||||
 | 
				  }); | 
			
		||||
 | 
				}); | 
			
		||||
@ -0,0 +1,27 @@ | 
			
		|||||
 | 
				import { Component, OnInit } from '@angular/core'; | 
			
		||||
 | 
				import { Router } from '@angular/router'; | 
			
		||||
 | 
				import { AuthService } from '../auth.service'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				@Component({ | 
			
		||||
 | 
				  selector: 'app-login', | 
			
		||||
 | 
				  templateUrl: './login.component.html', | 
			
		||||
 | 
				  styleUrls: ['./login.component.css'] | 
			
		||||
 | 
				}) | 
			
		||||
 | 
				export class LoginComponent implements OnInit { | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  username: string; | 
			
		||||
 | 
				  password: string; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  login() { | 
			
		||||
 | 
				    this.authService.login(this.username, this.password); | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  routeRegister() { | 
			
		||||
 | 
				    this.router.navigate(['/register']); | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				  constructor(private authService: AuthService, private router: Router) { } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  ngOnInit() { | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				} | 
			
		||||
@ -0,0 +1,25 @@ | 
			
		|||||
 | 
				import { async, ComponentFixture, TestBed } from '@angular/core/testing'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				import { MainComponent } from './main.component'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				describe('MainComponent', () => { | 
			
		||||
 | 
				  let component: MainComponent; | 
			
		||||
 | 
				  let fixture: ComponentFixture<MainComponent>; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  beforeEach(async(() => { | 
			
		||||
 | 
				    TestBed.configureTestingModule({ | 
			
		||||
 | 
				      declarations: [ MainComponent ] | 
			
		||||
 | 
				    }) | 
			
		||||
 | 
				    .compileComponents(); | 
			
		||||
 | 
				  })); | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  beforeEach(() => { | 
			
		||||
 | 
				    fixture = TestBed.createComponent(MainComponent); | 
			
		||||
 | 
				    component = fixture.componentInstance; | 
			
		||||
 | 
				    fixture.detectChanges(); | 
			
		||||
 | 
				  }); | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  it('should create', () => { | 
			
		||||
 | 
				    expect(component).toBeTruthy(); | 
			
		||||
 | 
				  }); | 
			
		||||
 | 
				}); | 
			
		||||
@ -0,0 +1,15 @@ | 
			
		|||||
 | 
				import { Component, OnInit } from '@angular/core'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				@Component({ | 
			
		||||
 | 
				  selector: 'app-main', | 
			
		||||
 | 
				  templateUrl: './main.component.html', | 
			
		||||
 | 
				  styleUrls: ['./main.component.css'] | 
			
		||||
 | 
				}) | 
			
		||||
 | 
				export class MainComponent implements OnInit { | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  constructor() { } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  ngOnInit() { | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				} | 
			
		||||
@ -0,0 +1,12 @@ | 
			
		|||||
 | 
				import { TestBed } from '@angular/core/testing'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				import { NotificationService } from './notification.service'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				describe('NotificationService', () => { | 
			
		||||
 | 
				  beforeEach(() => TestBed.configureTestingModule({})); | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  it('should be created', () => { | 
			
		||||
 | 
				    const service: NotificationService = TestBed.get(NotificationService); | 
			
		||||
 | 
				    expect(service).toBeTruthy(); | 
			
		||||
 | 
				  }); | 
			
		||||
 | 
				}); | 
			
		||||
@ -0,0 +1,38 @@ | 
			
		|||||
 | 
				import { Injectable } from '@angular/core'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export class Notification { | 
			
		||||
 | 
				  id: number; | 
			
		||||
 | 
				  resultType: string; // enum success or failure or info
 | 
			
		||||
 | 
				  text: string; | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				@Injectable({ | 
			
		||||
 | 
				  providedIn: 'root' | 
			
		||||
 | 
				}) | 
			
		||||
 | 
				export class NotificationService { | 
			
		||||
 | 
				  notifications: Notification[] = []; | 
			
		||||
 | 
				  currentCount = 0; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  addNotification(resultType, text) { | 
			
		||||
 | 
				    const newNotification = new Notification(); | 
			
		||||
 | 
				    newNotification.id = this.currentCount; | 
			
		||||
 | 
				    this.currentCount++; | 
			
		||||
 | 
				    newNotification.resultType = resultType; | 
			
		||||
 | 
				    newNotification.text = text; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    this.notifications.push(newNotification); | 
			
		||||
 | 
				    setTimeout(() => this.closeNotification(newNotification), 3000); | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  closeNotification(notify: Notification) { | 
			
		||||
 | 
				    for (let i = 0; i < this.notifications.length; i++) { | 
			
		||||
 | 
				      if (this.notifications[i].id === notify.id) { | 
			
		||||
 | 
				        this.notifications.splice(i, 1); | 
			
		||||
 | 
				        break; | 
			
		||||
 | 
				      } | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  constructor() { | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				} | 
			
		||||
@ -0,0 +1,3 @@ | 
			
		|||||
 | 
				<div class="alert notification col-11 mx-auto" *ngFor="let notification of notificationService.notifications" [ngClass]="{'alert-success': notification.resultType === 'success', 'alert-danger': notification.resultType ==='failure'}" (click)=notificationService.closeNotification(notification)> | 
			
		||||
 | 
				{{ notification.text }} | 
			
		||||
 | 
				</div> | 
			
		||||
@ -0,0 +1,25 @@ | 
			
		|||||
 | 
				import { async, ComponentFixture, TestBed } from '@angular/core/testing'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				import { NotificationComponent } from './notification.component'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				describe('NotificationComponent', () => { | 
			
		||||
 | 
				  let component: NotificationComponent; | 
			
		||||
 | 
				  let fixture: ComponentFixture<NotificationComponent>; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  beforeEach(async(() => { | 
			
		||||
 | 
				    TestBed.configureTestingModule({ | 
			
		||||
 | 
				      declarations: [ NotificationComponent ] | 
			
		||||
 | 
				    }) | 
			
		||||
 | 
				    .compileComponents(); | 
			
		||||
 | 
				  })); | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  beforeEach(() => { | 
			
		||||
 | 
				    fixture = TestBed.createComponent(NotificationComponent); | 
			
		||||
 | 
				    component = fixture.componentInstance; | 
			
		||||
 | 
				    fixture.detectChanges(); | 
			
		||||
 | 
				  }); | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  it('should create', () => { | 
			
		||||
 | 
				    expect(component).toBeTruthy(); | 
			
		||||
 | 
				  }); | 
			
		||||
 | 
				}); | 
			
		||||
@ -0,0 +1,16 @@ | 
			
		|||||
 | 
				import { Component, OnInit } from '@angular/core'; | 
			
		||||
 | 
				import { NotificationService } from '../notification.service'; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				@Component({ | 
			
		||||
 | 
				  selector: 'app-notification', | 
			
		||||
 | 
				  templateUrl: './notification.component.html', | 
			
		||||
 | 
				  styleUrls: ['./notification.component.css'] | 
			
		||||
 | 
				}) | 
			
		||||
 | 
				export class NotificationComponent implements OnInit { | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  constructor(private notificationService: NotificationService) { } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  ngOnInit() { | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				} | 
			
		||||
@ -0,0 +1,3 @@ | 
			
		|||||
 | 
				.no-space-break { | 
			
		||||
 | 
				    white-space:nowrap; | 
			
		||||
 | 
				  } | 
			
		||||
@ -0,0 +1,84 @@ | 
			
		|||||
 | 
				<div class="row p-2"> | 
			
		||||
 | 
				  <div class="col-12"> | 
			
		||||
 | 
				    <div class="row mt-3"> | 
			
		||||
 | 
				      <div class="col-12 d-flex"> | 
			
		||||
 | 
				        <label for="campaignName" class="pr-2 mt-auto mb-auto">Phonebook Name</label> | 
			
		||||
 | 
				        <input type="text" class="flex-grow-1" id="campaignName" [(ngModel)]="newPhonebook.name" placeholder="Phonebook Name"> | 
			
		||||
 | 
				      </div> | 
			
		||||
 | 
				    </div> | 
			
		||||
 | 
				    <div class="row mt-3"> | 
			
		||||
 | 
				      <div class="col-12 d-flex"> | 
			
		||||
 | 
				        <label class="no-space-break mt-auto mb-auto pr-2" for="import-records">Import Records</label> | 
			
		||||
 | 
				        <div class="custom-file" id="import-records"> | 
			
		||||
 | 
				          <input type="file" (change)="importPhoneRecords($event.target.files)" class="custom-file-input" id="customFile"> | 
			
		||||
 | 
				          <label class="custom-file-label" for="customFile">Choose file</label> | 
			
		||||
 | 
				        </div> | 
			
		||||
 | 
				      </div> | 
			
		||||
 | 
				    </div> | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    <div class="row"> | 
			
		||||
 | 
				      <div class="col-12 d-flex"> | 
			
		||||
 | 
				        <p><small><em><a href="/assets/phonebook-template.xlsx">Download file template here.</a></em></small></p> | 
			
		||||
 | 
				      </div> | 
			
		||||
 | 
				    </div> | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    <div class="row mt-1"> | 
			
		||||
 | 
				      <div class="col-12 d-flex"> | 
			
		||||
 | 
				        <table class="table table-hover"> | 
			
		||||
 | 
				          <thead class="thead-dark"> | 
			
		||||
 | 
				            <tr> | 
			
		||||
 | 
				              <th scope="col">First Name</th> | 
			
		||||
 | 
				              <th scope="col">Last Name</th> | 
			
		||||
 | 
				              <th scope="col">Alias</th> | 
			
		||||
 | 
				              <th scope="col">Phone Number</th> | 
			
		||||
 | 
				            </tr> | 
			
		||||
 | 
				          </thead> | 
			
		||||
 | 
				          <tbody> | 
			
		||||
 | 
				            <ng-container *ngFor="let phoneRecord of newPhoneRecords"> | 
			
		||||
 | 
				              <tr> | 
			
		||||
 | 
				                <td>{{ phoneRecord.firstName }}</td> | 
			
		||||
 | 
				                <td>{{ phoneRecord.lastName }}</td> | 
			
		||||
 | 
				                <td>{{ phoneRecord.alias }}</td> | 
			
		||||
 | 
				                <td>{{ phoneRecord.phoneNumber }}</td> | 
			
		||||
 | 
				              </tr> | 
			
		||||
 | 
				            </ng-container> | 
			
		||||
 | 
				              <tr (keyup.enter)="insertAdditionalRecord()"> | 
			
		||||
 | 
				                <td><input type="text" [(ngModel)]="additionalRecord.firstName" class="form-control" placeholder="firstName"></td> | 
			
		||||
 | 
				                <td><input type="text" [(ngModel)]="additionalRecord.lastName" class="form-control" placeholder="lastName"></td> | 
			
		||||
 | 
				                <td><input type="text" [(ngModel)]="additionalRecord.alias" class="form-control" placeholder="alias"></td> | 
			
		||||
 | 
				                <td><input type="text" [(ngModel)]="additionalRecord.phoneNumber" class="form-control" placeholder="phoneNumber"></td> | 
			
		||||
 | 
				              </tr> | 
			
		||||
 | 
				              <tr> | 
			
		||||
 | 
				                <p><small><em>Press enter to insert additional record</em></small></p> | 
			
		||||
 | 
				              </tr> | 
			
		||||
 | 
				          </tbody> | 
			
		||||
 | 
				        </table> | 
			
		||||
 | 
				      </div> | 
			
		||||
 | 
				    </div> | 
			
		||||
 | 
				    <div class="row mt-3"> | 
			
		||||
 | 
				      <div class="col-12 d-flex"> | 
			
		||||
 | 
				        <button type="button" (click)="submitNewPhonebook()" class="btn btn-primary mr-2">Save Phonebook</button> | 
			
		||||
 | 
				        <button type="button" *ngIf="router.url !== '/phonebook/new'" class="btn btn-danger ml-auto" data-toggle="modal" data-target="#completeModal">Delete</button> | 
			
		||||
 | 
				      </div> | 
			
		||||
 | 
				    </div> | 
			
		||||
 | 
				</div> | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				<div class="modal fade" id="completeModal" tabindex="-1" role="dialog" aria-labelledby="completeModal" aria-hidden="true"> | 
			
		||||
 | 
				  <div class="modal-dialog" role="document"> | 
			
		||||
 | 
				    <div class="modal-content"> | 
			
		||||
 | 
				      <div class="modal-header"> | 
			
		||||
 | 
				        <h5 class="modal-title" id="exampleModalLabel">{{ newPhonebook.name }}</h5> | 
			
		||||
 | 
				        <button type="button" class="close" data-dismiss="modal" aria-label="Close"> | 
			
		||||
 | 
				          <span aria-hidden="true">×</span> | 
			
		||||
 | 
				        </button> | 
			
		||||
 | 
				      </div> | 
			
		||||
 | 
				      <div class="modal-body"> | 
			
		||||
 | 
				        <p>Are you sure you want to delete the phonebook?</p> | 
			
		||||
 | 
				      </div> | 
			
		||||
 | 
				      <div class="modal-footer"> | 
			
		||||
 | 
				        <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> | 
			
		||||
 | 
				        <button type="button"  class="btn btn-danger" (click)="deletePhonebook()" data-dismiss="modal">Delete Phonebook</button> | 
			
		||||
 | 
				      </div> | 
			
		||||
 | 
				    </div> | 
			
		||||
 | 
				  </div> | 
			
		||||
 | 
				</div> | 
			
		||||
Some files were not shown because too many files changed in this diff
					Loading…
					
					
				
		Reference in new issue