Browse Source

First commit

master
Samuel Pua 6 years ago
commit
d504997def
  1. 47
      .gitignore
  2. 14
      build.sh
  3. 26
      docker-compose.yml
  4. BIN
      phonebook-template.xlsx
  5. 4
      psql-start.sh
  6. 389
      tapit-backend/auth.go
  7. 540
      tapit-backend/campaign.go
  8. 143
      tapit-backend/main.go
  9. 30
      tapit-backend/notification.go
  10. 342
      tapit-backend/phonebook.go
  11. BIN
      tapit-backend/tapit-backend
  12. 240
      tapit-backend/text-template.go
  13. 215
      tapit-backend/twilio.go
  14. 18
      tapit-build/Dockerfile
  15. 3
      tapit-build/entrypoint.sh
  16. 3719
      tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap-grid.css
  17. 1
      tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap-grid.css.map
  18. 7
      tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap-grid.min.css
  19. 1
      tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap-grid.min.css.map
  20. 331
      tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap-reboot.css
  21. 1
      tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap-reboot.css.map
  22. 8
      tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap-reboot.min.css
  23. 1
      tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap-reboot.min.css.map
  24. 10038
      tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap.css
  25. 1
      tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap.css.map
  26. 7
      tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap.min.css
  27. 1
      tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap.min.css.map
  28. 7013
      tapit-build/static/assets/bootstrap-4.3.1-dist/js/bootstrap.bundle.js
  29. 1
      tapit-build/static/assets/bootstrap-4.3.1-dist/js/bootstrap.bundle.js.map
  30. 7
      tapit-build/static/assets/bootstrap-4.3.1-dist/js/bootstrap.bundle.min.js
  31. 1
      tapit-build/static/assets/bootstrap-4.3.1-dist/js/bootstrap.bundle.min.js.map
  32. 4435
      tapit-build/static/assets/bootstrap-4.3.1-dist/js/bootstrap.js
  33. 1
      tapit-build/static/assets/bootstrap-4.3.1-dist/js/bootstrap.js.map
  34. 7
      tapit-build/static/assets/bootstrap-4.3.1-dist/js/bootstrap.min.js
  35. 1
      tapit-build/static/assets/bootstrap-4.3.1-dist/js/bootstrap.min.js.map
  36. 2
      tapit-build/static/assets/jquery-3.4.1.min.js
  37. BIN
      tapit-build/static/assets/phonebook-template.xlsx
  38. 5
      tapit-build/static/assets/popper.min.js
  39. 2
      tapit-build/static/es2015-polyfills.js
  40. 1
      tapit-build/static/es2015-polyfills.js.map
  41. BIN
      tapit-build/static/favicon.ico
  42. 21
      tapit-build/static/index.html
  43. 2
      tapit-build/static/main.js
  44. 1
      tapit-build/static/main.js.map
  45. 2
      tapit-build/static/polyfills.js
  46. 1
      tapit-build/static/polyfills.js.map
  47. 2
      tapit-build/static/runtime.js
  48. 1
      tapit-build/static/runtime.js.map
  49. 2
      tapit-build/static/styles.js
  50. 1
      tapit-build/static/styles.js.map
  51. 2
      tapit-build/static/vendor.js
  52. 1
      tapit-build/static/vendor.js.map
  53. BIN
      tapit-build/tapit
  54. 13
      tapit-frontend/.editorconfig
  55. 27
      tapit-frontend/README.md
  56. 136
      tapit-frontend/angular.json
  57. 28
      tapit-frontend/e2e/protractor.conf.js
  58. 23
      tapit-frontend/e2e/src/app.e2e-spec.ts
  59. 11
      tapit-frontend/e2e/src/app.po.ts
  60. 13
      tapit-frontend/e2e/tsconfig.e2e.json
  61. 10514
      tapit-frontend/package-lock.json
  62. 48
      tapit-frontend/package.json
  63. 37
      tapit-frontend/src/app/app-routing.module.ts
  64. 0
      tapit-frontend/src/app/app.component.css
  65. 42
      tapit-frontend/src/app/app.component.html
  66. 35
      tapit-frontend/src/app/app.component.spec.ts
  67. 37
      tapit-frontend/src/app/app.component.ts
  68. 48
      tapit-frontend/src/app/app.module.ts
  69. 12
      tapit-frontend/src/app/auth.service.spec.ts
  70. 131
      tapit-frontend/src/app/auth.service.ts
  71. 0
      tapit-frontend/src/app/campaign-new/campaign-new.component.css
  72. 45
      tapit-frontend/src/app/campaign-new/campaign-new.component.html
  73. 25
      tapit-frontend/src/app/campaign-new/campaign-new.component.spec.ts
  74. 35
      tapit-frontend/src/app/campaign-new/campaign-new.component.ts
  75. 3
      tapit-frontend/src/app/campaign-view/campaign-view.component.css
  76. 77
      tapit-frontend/src/app/campaign-view/campaign-view.component.html
  77. 25
      tapit-frontend/src/app/campaign-view/campaign-view.component.spec.ts
  78. 69
      tapit-frontend/src/app/campaign-view/campaign-view.component.ts
  79. 12
      tapit-frontend/src/app/campaign.service.spec.ts
  80. 115
      tapit-frontend/src/app/campaign.service.ts
  81. 0
      tapit-frontend/src/app/campaign/campaign.component.css
  82. 31
      tapit-frontend/src/app/campaign/campaign.component.html
  83. 25
      tapit-frontend/src/app/campaign/campaign.component.spec.ts
  84. 24
      tapit-frontend/src/app/campaign/campaign.component.ts
  85. 0
      tapit-frontend/src/app/login/login.component.css
  86. 27
      tapit-frontend/src/app/login/login.component.html
  87. 25
      tapit-frontend/src/app/login/login.component.spec.ts
  88. 27
      tapit-frontend/src/app/login/login.component.ts
  89. 0
      tapit-frontend/src/app/main/main.component.css
  90. 3
      tapit-frontend/src/app/main/main.component.html
  91. 25
      tapit-frontend/src/app/main/main.component.spec.ts
  92. 15
      tapit-frontend/src/app/main/main.component.ts
  93. 12
      tapit-frontend/src/app/notification.service.spec.ts
  94. 38
      tapit-frontend/src/app/notification.service.ts
  95. 0
      tapit-frontend/src/app/notification/notification.component.css
  96. 3
      tapit-frontend/src/app/notification/notification.component.html
  97. 25
      tapit-frontend/src/app/notification/notification.component.spec.ts
  98. 16
      tapit-frontend/src/app/notification/notification.component.ts
  99. 3
      tapit-frontend/src/app/phonebook-new/phonebook-new.component.css
  100. 84
      tapit-frontend/src/app/phonebook-new/phonebook-new.component.html

47
.gitignore

@ -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

14
build.sh

@ -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

26
docker-compose.yml

@ -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

BIN
phonebook-template.xlsx

Binary file not shown.

4
psql-start.sh

@ -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

389
tapit-backend/auth.go

@ -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
}
}
}

540
tapit-backend/campaign.go

@ -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(&currentJob.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)
}
}

143
tapit-backend/main.go

@ -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)
}

30
tapit-backend/notification.go

@ -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
}

342
tapit-backend/phonebook.go

@ -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
}
}

BIN
tapit-backend/tapit-backend

Binary file not shown.

240
tapit-backend/text-template.go

@ -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
}

215
tapit-backend/twilio.go

@ -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)
}
}
}

18
tapit-build/Dockerfile

@ -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"]

3
tapit-build/entrypoint.sh

@ -0,0 +1,3 @@
#! /bin/bash
sleep 5
/tapit

3719
tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap-grid.css

File diff suppressed because it is too large

1
tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap-grid.css.map

File diff suppressed because one or more lines are too long

7
tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap-grid.min.css

File diff suppressed because one or more lines are too long

1
tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap-grid.min.css.map

File diff suppressed because one or more lines are too long

331
tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap-reboot.css

@ -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 */

1
tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap-reboot.css.map

File diff suppressed because one or more lines are too long

8
tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap-reboot.min.css

@ -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 */

1
tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap-reboot.min.css.map

File diff suppressed because one or more lines are too long

10038
tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap.css

File diff suppressed because it is too large

1
tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap.css.map

File diff suppressed because one or more lines are too long

7
tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap.min.css

File diff suppressed because one or more lines are too long

1
tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap.min.css.map

File diff suppressed because one or more lines are too long

7013
tapit-build/static/assets/bootstrap-4.3.1-dist/js/bootstrap.bundle.js

File diff suppressed because it is too large

1
tapit-build/static/assets/bootstrap-4.3.1-dist/js/bootstrap.bundle.js.map

File diff suppressed because one or more lines are too long

7
tapit-build/static/assets/bootstrap-4.3.1-dist/js/bootstrap.bundle.min.js

File diff suppressed because one or more lines are too long

1
tapit-build/static/assets/bootstrap-4.3.1-dist/js/bootstrap.bundle.min.js.map

File diff suppressed because one or more lines are too long

4435
tapit-build/static/assets/bootstrap-4.3.1-dist/js/bootstrap.js

File diff suppressed because it is too large

1
tapit-build/static/assets/bootstrap-4.3.1-dist/js/bootstrap.js.map

File diff suppressed because one or more lines are too long

7
tapit-build/static/assets/bootstrap-4.3.1-dist/js/bootstrap.min.js

File diff suppressed because one or more lines are too long

1
tapit-build/static/assets/bootstrap-4.3.1-dist/js/bootstrap.min.js.map

File diff suppressed because one or more lines are too long

2
tapit-build/static/assets/jquery-3.4.1.min.js

File diff suppressed because one or more lines are too long

BIN
tapit-build/static/assets/phonebook-template.xlsx

Binary file not shown.

5
tapit-build/static/assets/popper.min.js

File diff suppressed because one or more lines are too long

2
tapit-build/static/es2015-polyfills.js

File diff suppressed because one or more lines are too long

1
tapit-build/static/es2015-polyfills.js.map

File diff suppressed because one or more lines are too long

BIN
tapit-build/static/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

21
tapit-build/static/index.html

@ -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>

2
tapit-build/static/main.js

File diff suppressed because one or more lines are too long

1
tapit-build/static/main.js.map

File diff suppressed because one or more lines are too long

2
tapit-build/static/polyfills.js

File diff suppressed because one or more lines are too long

1
tapit-build/static/polyfills.js.map

File diff suppressed because one or more lines are too long

2
tapit-build/static/runtime.js

@ -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

1
tapit-build/static/runtime.js.map

File diff suppressed because one or more lines are too long

2
tapit-build/static/styles.js

File diff suppressed because one or more lines are too long

1
tapit-build/static/styles.js.map

File diff suppressed because one or more lines are too long

2
tapit-build/static/vendor.js

File diff suppressed because one or more lines are too long

1
tapit-build/static/vendor.js.map

File diff suppressed because one or more lines are too long

BIN
tapit-build/tapit

Binary file not shown.

13
tapit-frontend/.editorconfig

@ -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

27
tapit-frontend/README.md

@ -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).

136
tapit-frontend/angular.json

@ -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"
}

28
tapit-frontend/e2e/protractor.conf.js

@ -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 } }));
}
};

23
tapit-frontend/e2e/src/app.e2e-spec.ts

@ -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,
}));
});
});

11
tapit-frontend/e2e/src/app.po.ts

@ -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>;
}
}

13
tapit-frontend/e2e/tsconfig.e2e.json

@ -0,0 +1,13 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

10514
tapit-frontend/package-lock.json

File diff suppressed because it is too large

48
tapit-frontend/package.json

@ -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"
}
}

37
tapit-frontend/src/app/app-routing.module.ts

@ -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
tapit-frontend/src/app/app.component.css

42
tapit-frontend/src/app/app.component.html

@ -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>

35
tapit-frontend/src/app/app.component.spec.ts

@ -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!');
});
});

37
tapit-frontend/src/app/app.component.ts

@ -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();
}
}

48
tapit-frontend/src/app/app.module.ts

@ -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 { }

12
tapit-frontend/src/app/auth.service.spec.ts

@ -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();
});
});

131
tapit-frontend/src/app/auth.service.ts

@ -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
tapit-frontend/src/app/campaign-new/campaign-new.component.css

45
tapit-frontend/src/app/campaign-new/campaign-new.component.html

@ -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>

25
tapit-frontend/src/app/campaign-new/campaign-new.component.spec.ts

@ -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();
});
});

35
tapit-frontend/src/app/campaign-new/campaign-new.component.ts

@ -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() {
}
}

3
tapit-frontend/src/app/campaign-view/campaign-view.component.css

@ -0,0 +1,3 @@
.campaign-details:read-only {
background-color: white;
}

77
tapit-frontend/src/app/campaign-view/campaign-view.component.html

@ -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">&times;</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>

25
tapit-frontend/src/app/campaign-view/campaign-view.component.spec.ts

@ -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();
});
});

69
tapit-frontend/src/app/campaign-view/campaign-view.component.ts

@ -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);
}
}

12
tapit-frontend/src/app/campaign.service.spec.ts

@ -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();
});
});

115
tapit-frontend/src/app/campaign.service.ts

@ -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
tapit-frontend/src/app/campaign/campaign.component.css

31
tapit-frontend/src/app/campaign/campaign.component.html

@ -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>

25
tapit-frontend/src/app/campaign/campaign.component.spec.ts

@ -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();
});
});

24
tapit-frontend/src/app/campaign/campaign.component.ts

@ -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
tapit-frontend/src/app/login/login.component.css

27
tapit-frontend/src/app/login/login.component.html

@ -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>

25
tapit-frontend/src/app/login/login.component.spec.ts

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LoginComponent } from './login.component';
describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LoginComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

27
tapit-frontend/src/app/login/login.component.ts

@ -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
tapit-frontend/src/app/main/main.component.css

3
tapit-frontend/src/app/main/main.component.html

@ -0,0 +1,3 @@
<p>
main works!
</p>

25
tapit-frontend/src/app/main/main.component.spec.ts

@ -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();
});
});

15
tapit-frontend/src/app/main/main.component.ts

@ -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() {
}
}

12
tapit-frontend/src/app/notification.service.spec.ts

@ -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();
});
});

38
tapit-frontend/src/app/notification.service.ts

@ -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
tapit-frontend/src/app/notification/notification.component.css

3
tapit-frontend/src/app/notification/notification.component.html

@ -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>

25
tapit-frontend/src/app/notification/notification.component.spec.ts

@ -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();
});
});

16
tapit-frontend/src/app/notification/notification.component.ts

@ -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() {
}
}

3
tapit-frontend/src/app/phonebook-new/phonebook-new.component.css

@ -0,0 +1,3 @@
.no-space-break {
white-space:nowrap;
}

84
tapit-frontend/src/app/phonebook-new/phonebook-new.component.html

@ -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">&times;</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…
Cancel
Save