First commit
This commit is contained in:
47
.gitignore
vendored
Normal file
47
.gitignore
vendored
Normal file
@@ -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
Executable file
14
build.sh
Executable file
@@ -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
Normal file
26
docker-compose.yml
Normal file
@@ -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
Normal file
BIN
phonebook-template.xlsx
Normal file
Binary file not shown.
4
psql-start.sh
Normal file
4
psql-start.sh
Normal file
@@ -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
Normal file
389
tapit-backend/auth.go
Normal file
@@ -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
Normal file
540
tapit-backend/campaign.go
Normal file
@@ -0,0 +1,540 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jinzhu/gorm"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"strconv"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Campaign struct {
|
||||||
|
gorm.Model
|
||||||
|
Name string
|
||||||
|
FromNumber string
|
||||||
|
Size int
|
||||||
|
CurrentStatus string // enum Running, Paused, Completed, Not Started
|
||||||
|
PhonebookId uint
|
||||||
|
TextTemplateId uint
|
||||||
|
WebTemplateId uint
|
||||||
|
ProviderTag string
|
||||||
|
Jobs []Job `gorm:"foreignkey:CampaignId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CampaignComms struct {
|
||||||
|
Campaign Campaign
|
||||||
|
Action string // enum run, stop
|
||||||
|
}
|
||||||
|
|
||||||
|
type JobComms struct {
|
||||||
|
Job Job
|
||||||
|
Action string // enum run, stop
|
||||||
|
}
|
||||||
|
|
||||||
|
type CampaignJson struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
FromNumber string `json:"fromNumber"`
|
||||||
|
Size int `json:"size"`
|
||||||
|
CurrentStatus string `json:"currentStatus"`
|
||||||
|
CreateDate time.Time `json:"createDate"`
|
||||||
|
PhonebookId uint `json:"phoneBookId"`
|
||||||
|
TextTemplateId uint `json:"textTemplateId"`
|
||||||
|
WebTemplateId uint `json:"webTemplateId"`
|
||||||
|
ProviderTag string `json:"providerTag"`
|
||||||
|
Jobs []JobJson `json:"jobs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Job struct {
|
||||||
|
gorm.Model
|
||||||
|
CampaignId uint
|
||||||
|
CurrentStatus string // enum Failed, Queued, Sent, Delivered, Not Started
|
||||||
|
TimeSent time.Time
|
||||||
|
ProviderTag string
|
||||||
|
AccSID string
|
||||||
|
AuthToken string
|
||||||
|
BodyText string
|
||||||
|
FromNum string
|
||||||
|
ToNum string
|
||||||
|
ResultStr string
|
||||||
|
MessageSid string
|
||||||
|
}
|
||||||
|
|
||||||
|
type JobJson struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
CurrentStatus string `json:"currentStatus"`
|
||||||
|
TimeSent time.Time `json:"timeSent"`
|
||||||
|
FromNum string `json:"fromNum"`
|
||||||
|
ToNum string `json:"toNum"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TwilioMessageJson struct {
|
||||||
|
AccountSid string `json:"account_sid"`
|
||||||
|
ApiVersion string `json:"api_version"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
DateCreated string `json:"date_created"`
|
||||||
|
DateSent string `json:"date_sent"`
|
||||||
|
DateUpdated string `json:"date_updated"`
|
||||||
|
Direction string `json:"direction"`
|
||||||
|
ErrorCode string `json:"error_code"`
|
||||||
|
ErrorMessage string `json:"error_message"`
|
||||||
|
From string `json:"from"`
|
||||||
|
MessagingServiceSid string `json:"messaging_service_sid"`
|
||||||
|
NumMedia string `json:"num_media"`
|
||||||
|
NumSegments string `json:"num_segments"`
|
||||||
|
Price string `json:"price"`
|
||||||
|
PriceUnit string `json:"price_unit"`
|
||||||
|
Sid string `json:"sid"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
SubResourceUri SubResourceUriJson `json:"subresource_uris"`
|
||||||
|
To string `json:"to"`
|
||||||
|
Uri string `json:"uri"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubResourceUriJson struct {
|
||||||
|
Media string `json:"media"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tapit *Tapit) handleCampaign(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if strings.ToUpper(r.Method) == "GET" {
|
||||||
|
tapit.getCampaigns(w, r)
|
||||||
|
} else if strings.ToUpper(r.Method) == "POST" {
|
||||||
|
tapit.createCampaign(w, r)
|
||||||
|
} else {
|
||||||
|
http.Error(w, "HTTP method not implemented", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tapit *Tapit) getCampaigns(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var campaigns []Campaign
|
||||||
|
tapit.db.Find(&campaigns)
|
||||||
|
jsonResults, err := json.Marshal(campaignsToJson(campaigns))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(jsonResults)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func campaignsToJson(campaigns []Campaign) []CampaignJson {
|
||||||
|
var results []CampaignJson
|
||||||
|
for _, currCampaign := range campaigns {
|
||||||
|
var currJson CampaignJson
|
||||||
|
currJson.Id = currCampaign.ID
|
||||||
|
currJson.Name = currCampaign.Name
|
||||||
|
currJson.Size = currCampaign.Size
|
||||||
|
currJson.FromNumber = currCampaign.FromNumber
|
||||||
|
currJson.CurrentStatus = currCampaign.CurrentStatus
|
||||||
|
currJson.CreateDate = currCampaign.CreatedAt
|
||||||
|
currJson.PhonebookId = currCampaign.PhonebookId
|
||||||
|
currJson.TextTemplateId = currCampaign.TextTemplateId
|
||||||
|
currJson.WebTemplateId = currCampaign.WebTemplateId
|
||||||
|
currJson.ProviderTag = currCampaign.ProviderTag
|
||||||
|
|
||||||
|
results = append(results, currJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tapit *Tapit) createCampaign(w http.ResponseWriter, r *http.Request) {
|
||||||
|
requestBody, err:= ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Bad request", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var newCampaignJson CampaignJson
|
||||||
|
err = json.Unmarshal(requestBody, &newCampaignJson)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Bad request", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if newCampaignJson.Name != "" {
|
||||||
|
var newCampaign Campaign
|
||||||
|
|
||||||
|
// populate details to be used later
|
||||||
|
var newRecords []PhoneRecord
|
||||||
|
var newTextTemplateBody string
|
||||||
|
var newAccSID string
|
||||||
|
var newAuthToken string
|
||||||
|
newRecords = tapit.getSpecificPhonebook(newCampaignJson.PhonebookId).Records
|
||||||
|
newTextTemplateBody = tapit.getSpecificTextBody(newCampaignJson.TextTemplateId)
|
||||||
|
if newCampaignJson.ProviderTag == "twilio" {
|
||||||
|
var twilioProvider TwilioProvider
|
||||||
|
tapit.db.Last(&twilioProvider)
|
||||||
|
|
||||||
|
newAccSID = twilioProvider.AccountSID
|
||||||
|
newAuthToken = twilioProvider.AuthToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// update static details
|
||||||
|
newCampaign.Name = newCampaignJson.Name
|
||||||
|
newCampaign.Size = len(newRecords)
|
||||||
|
newCampaign.CurrentStatus = "Not Started"
|
||||||
|
|
||||||
|
newCampaign.FromNumber = newCampaignJson.FromNumber
|
||||||
|
newCampaign.PhonebookId = newCampaignJson.PhonebookId
|
||||||
|
newCampaign.TextTemplateId = newCampaignJson.TextTemplateId
|
||||||
|
newCampaign.WebTemplateId = newCampaignJson.WebTemplateId
|
||||||
|
newCampaign.ProviderTag = newCampaignJson.ProviderTag
|
||||||
|
|
||||||
|
// update records
|
||||||
|
for _, record := range newRecords {
|
||||||
|
var newJob Job
|
||||||
|
newJob.CurrentStatus = "Not Started"
|
||||||
|
newJob.ProviderTag = newCampaign.ProviderTag
|
||||||
|
newJob.AccSID = newAccSID
|
||||||
|
newJob.AuthToken = newAuthToken
|
||||||
|
newJob.FromNum = newCampaign.FromNumber
|
||||||
|
|
||||||
|
// interpreting records
|
||||||
|
var newBodyText string
|
||||||
|
newJob.ToNum = record.PhoneNumber
|
||||||
|
newBodyText = newTextTemplateBody
|
||||||
|
newBodyText = strings.Replace(newBodyText, "{firstName}", record.FirstName, -1)
|
||||||
|
newBodyText = strings.Replace(newBodyText, "{lastName}", record.LastName, -1)
|
||||||
|
newBodyText = strings.Replace(newBodyText, "{alias}", record.Alias, -1)
|
||||||
|
newBodyText = strings.Replace(newBodyText, "{phoneNumber}", record.PhoneNumber, -1)
|
||||||
|
|
||||||
|
newJob.BodyText = newBodyText
|
||||||
|
|
||||||
|
// saving it
|
||||||
|
newCampaign.Jobs = append(newCampaign.Jobs, newJob)
|
||||||
|
}
|
||||||
|
|
||||||
|
// update database
|
||||||
|
tapit.db.NewRecord(&newCampaign)
|
||||||
|
tapit.db.Create(&newCampaign)
|
||||||
|
if newCampaign.ID == 0 {
|
||||||
|
notifyPopup(w, r, "failure", "Failed to create campaign", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newCampaignJson.Id = newCampaign.ID
|
||||||
|
newCampaignJson.CreateDate = newCampaign.CreatedAt
|
||||||
|
newCampaignJson.Size = newCampaign.Size
|
||||||
|
newCampaignJson.CurrentStatus = newCampaign.CurrentStatus
|
||||||
|
|
||||||
|
notifyPopup(w, r, "success", "Successfully added new campaign", newCampaignJson)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
notifyPopup(w, r, "failure", "Please enter the campaign name", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tapit *Tapit) handleSpecificCampaign(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if strings.ToUpper(r.Method) == "PUT" {
|
||||||
|
// not implmented -- complexity in changing campaign perimeters
|
||||||
|
// tapit.updateCampaign(w, r)
|
||||||
|
http.Error(w, "HTTP method not implemented", 400)
|
||||||
|
return
|
||||||
|
} else if strings.ToUpper(r.Method) == "DELETE" {
|
||||||
|
tapit.deleteCampaign(w,r)
|
||||||
|
return
|
||||||
|
} else if strings.ToUpper(r.Method) == "GET" {
|
||||||
|
tapit.getCampaign(w,r)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
http.Error(w, "HTTP method not implemented", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tapit *Tapit) getSpecificCampaign(id uint) Campaign {
|
||||||
|
var campaign Campaign
|
||||||
|
var jobs []Job
|
||||||
|
|
||||||
|
var dbSearchCampaign Campaign
|
||||||
|
dbSearchCampaign.ID = id
|
||||||
|
tapit.db.Where(&dbSearchCampaign).First(&campaign)
|
||||||
|
|
||||||
|
var dbSearchJob Job
|
||||||
|
dbSearchJob.CampaignId = id
|
||||||
|
tapit.db.Where(&dbSearchJob).Find(&jobs)
|
||||||
|
|
||||||
|
campaign.Jobs = jobs
|
||||||
|
return campaign
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tapit *Tapit) getCampaign(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
tempID, err := strconv.Atoi(vars["id"])
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Bad request", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
phonebook := tapit.getSpecificCampaign(uint(tempID))
|
||||||
|
|
||||||
|
jsonResults, err := json.Marshal(campaignToJson(phonebook))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(jsonResults)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func campaignToJson(campaign Campaign) CampaignJson {
|
||||||
|
var cJson CampaignJson
|
||||||
|
cJson.Id = campaign.ID
|
||||||
|
cJson.Name = campaign.Name
|
||||||
|
cJson.FromNumber = campaign.FromNumber
|
||||||
|
cJson.Size = campaign.Size
|
||||||
|
cJson.CurrentStatus = campaign.CurrentStatus
|
||||||
|
cJson.PhonebookId = campaign.PhonebookId
|
||||||
|
cJson.TextTemplateId = campaign.TextTemplateId
|
||||||
|
cJson.WebTemplateId = campaign.WebTemplateId
|
||||||
|
cJson.ProviderTag = campaign.ProviderTag
|
||||||
|
|
||||||
|
// iterating jobs
|
||||||
|
for _, job := range campaign.Jobs {
|
||||||
|
var currJson JobJson
|
||||||
|
currJson.CurrentStatus = job.CurrentStatus
|
||||||
|
currJson.TimeSent = job.TimeSent
|
||||||
|
currJson.FromNum = job.FromNum
|
||||||
|
currJson.ToNum = job.ToNum
|
||||||
|
|
||||||
|
cJson.Jobs = append(cJson.Jobs, currJson)
|
||||||
|
}
|
||||||
|
return cJson
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tapit *Tapit) deleteCampaign(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
tempID, err := strconv.Atoi(vars["id"])
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Bad request", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// start working
|
||||||
|
var campaign Campaign
|
||||||
|
|
||||||
|
// get phonebook
|
||||||
|
var dbSearchCampaign Campaign
|
||||||
|
dbSearchCampaign.ID = uint(tempID)
|
||||||
|
tapit.db.Where(&dbSearchCampaign).First(&campaign)
|
||||||
|
|
||||||
|
if campaign.ID == uint(tempID) {
|
||||||
|
// finally delete it
|
||||||
|
tapit.db.Delete(&campaign)
|
||||||
|
notifyPopup(w, r, "success", "Successfully deleted campaign", nil)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
http.Error(w, "Bad request", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tapit *Tapit) handleStartCampaign(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if strings.ToUpper(r.Method) == "GET" {
|
||||||
|
tapit.startCampaign(w,r)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
http.Error(w, "HTTP method not implemented", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tapit *Tapit) handleStopCampaign(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if strings.ToUpper(r.Method) == "GET" {
|
||||||
|
tapit.stopCampaign(w,r)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
http.Error(w, "HTTP method not implemented", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (tapit *Tapit) startCampaign(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
tempID, err := strconv.Atoi(vars["id"])
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Bad request", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// start working
|
||||||
|
var campaign Campaign
|
||||||
|
|
||||||
|
campaign = tapit.getSpecificCampaign(uint(tempID))
|
||||||
|
|
||||||
|
if campaign.ID == uint(tempID) && campaign.CurrentStatus != "Running" && campaign.CurrentStatus != "Completed" {
|
||||||
|
// finally start new thread and start working
|
||||||
|
go tapit.workerCampaign(campaign)
|
||||||
|
campaign.CurrentStatus = "Running"
|
||||||
|
tapit.db.Save(&campaign)
|
||||||
|
jsonResults := campaignToJson(campaign)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
notifyPopup(w, r, "success", "Started campaign", jsonResults)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
http.Error(w, "Bad request", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tapit *Tapit) stopCampaign(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
tempID, err := strconv.Atoi(vars["id"])
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Bad request", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// start working
|
||||||
|
var campaign Campaign
|
||||||
|
|
||||||
|
campaign = tapit.getSpecificCampaign(uint(tempID))
|
||||||
|
|
||||||
|
if campaign.ID == uint(tempID) && campaign.CurrentStatus == "Running" {
|
||||||
|
var campaignComms CampaignComms
|
||||||
|
campaignComms.Action = "stop"
|
||||||
|
campaignComms.Campaign = campaign
|
||||||
|
tapit.campaignChan <- campaignComms
|
||||||
|
|
||||||
|
// notify
|
||||||
|
notifyPopup(w, r, "success", "Paused campaign", nil)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
http.Error(w, "Bad request", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tapit *Tapit) workerCampaign(campaign Campaign) {
|
||||||
|
var campaignComms CampaignComms
|
||||||
|
var jobChan chan JobComms
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
jobChan = make(chan JobComms, 1)
|
||||||
|
for i:=0; i<tapit.globalSettings.threadsPerCampaign; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go tapit.workerJob(jobChan, &wg)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, job := range campaign.Jobs {
|
||||||
|
select {
|
||||||
|
case campaignComms = <-tapit.campaignChan:
|
||||||
|
if campaignComms.Campaign.ID == campaign.ID {
|
||||||
|
if campaignComms.Action == "stop" {
|
||||||
|
// kill all
|
||||||
|
for i:=0; i<tapit.globalSettings.threadsPerCampaign; i++ {
|
||||||
|
var stopComms JobComms
|
||||||
|
stopComms.Action = "stop"
|
||||||
|
jobChan <- stopComms
|
||||||
|
}
|
||||||
|
// wait to end
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// get updated campaign
|
||||||
|
var newCampaign Campaign
|
||||||
|
var searchCampaign Campaign
|
||||||
|
searchCampaign.ID = campaign.ID
|
||||||
|
tapit.db.Where(&searchCampaign).First(&newCampaign)
|
||||||
|
|
||||||
|
// update campaign
|
||||||
|
newCampaign.CurrentStatus = "Paused"
|
||||||
|
tapit.db.Save(&newCampaign)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// not mine -- throw it back
|
||||||
|
tapit.campaignChan<- campaignComms
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if job.CurrentStatus == "Not Started" {
|
||||||
|
var workComms JobComms
|
||||||
|
workComms.Action = "run"
|
||||||
|
workComms.Job = job
|
||||||
|
jobChan <- workComms
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i:=0; i<tapit.globalSettings.threadsPerCampaign; i++ {
|
||||||
|
var stopComms JobComms
|
||||||
|
stopComms.Action = "stop"
|
||||||
|
jobChan <- stopComms
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait to end
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// get updated campaign
|
||||||
|
var newCampaign Campaign
|
||||||
|
var searchCampaign Campaign
|
||||||
|
searchCampaign.ID = campaign.ID
|
||||||
|
tapit.db.Where(&searchCampaign).First(&newCampaign)
|
||||||
|
|
||||||
|
// update campaign
|
||||||
|
newCampaign.CurrentStatus = "Completed"
|
||||||
|
tapit.db.Save(&newCampaign)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tapit *Tapit) workerJob(jobChan chan JobComms, wg *sync.WaitGroup) {
|
||||||
|
var currentJob JobComms
|
||||||
|
exitCode := false
|
||||||
|
|
||||||
|
for !exitCode {
|
||||||
|
currentJob = <-jobChan
|
||||||
|
if currentJob.Action != "stop" {
|
||||||
|
if currentJob.Job.ProviderTag == "twilio" {
|
||||||
|
|
||||||
|
var resultJson []byte
|
||||||
|
resultJson = tapit.twilioSend(currentJob.Job.AccSID, currentJob.Job.AuthToken, currentJob.Job.BodyText, currentJob.Job.FromNum, currentJob.Job.ToNum)
|
||||||
|
currentJob.Job.ResultStr = string(resultJson)
|
||||||
|
|
||||||
|
var twilioResult TwilioMessageJson
|
||||||
|
err := json.Unmarshal(resultJson, &twilioResult)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
currentJob.Job.CurrentStatus = "Failed"
|
||||||
|
} else if twilioResult.Status == "queued" {
|
||||||
|
currentJob.Job.MessageSid = twilioResult.Sid
|
||||||
|
currentJob.Job.CurrentStatus = "Queued"
|
||||||
|
} else if twilioResult.Status == "delivered" {
|
||||||
|
currentJob.Job.MessageSid = twilioResult.Sid
|
||||||
|
currentJob.Job.CurrentStatus = "Delivered"
|
||||||
|
} else {
|
||||||
|
currentJob.Job.CurrentStatus = "Failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
// redo until done
|
||||||
|
tapit.db.Save(¤tJob.Job)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
exitCode = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tapit *Tapit) clearRunningCampaigns() {
|
||||||
|
var campaigns []Campaign
|
||||||
|
var searchCampaign Campaign
|
||||||
|
searchCampaign.CurrentStatus = "Running"
|
||||||
|
tapit.db.Where(&searchCampaign).Find(&campaigns)
|
||||||
|
|
||||||
|
for _, campaign := range campaigns {
|
||||||
|
campaign.CurrentStatus = "Paused"
|
||||||
|
tapit.db.Save(&campaign)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
143
tapit-backend/main.go
Normal file
143
tapit-backend/main.go
Normal file
@@ -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
Normal file
30
tapit-backend/notification.go
Normal file
@@ -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
Normal file
342
tapit-backend/phonebook.go
Normal file
@@ -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
Executable file
BIN
tapit-backend/tapit-backend
Executable file
Binary file not shown.
240
tapit-backend/text-template.go
Normal file
240
tapit-backend/text-template.go
Normal file
@@ -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
Normal file
215
tapit-backend/twilio.go
Normal file
@@ -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
Normal file
18
tapit-build/Dockerfile
Normal file
@@ -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
Executable file
3
tapit-build/entrypoint.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#! /bin/bash
|
||||||
|
sleep 5
|
||||||
|
/tapit
|
||||||
3719
tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap-grid.css
vendored
Normal file
3719
tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap-grid.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
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
vendored
Normal file
7
tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap-grid.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
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
vendored
Normal file
331
tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap-reboot.css
vendored
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
/*!
|
||||||
|
* Bootstrap Reboot v4.3.1 (https://getbootstrap.com/)
|
||||||
|
* Copyright 2011-2019 The Bootstrap Authors
|
||||||
|
* Copyright 2011-2019 Twitter, Inc.
|
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||||
|
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||||
|
*/
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-family: sans-serif;
|
||||||
|
line-height: 1.15;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #212529;
|
||||||
|
text-align: left;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
[tabindex="-1"]:focus {
|
||||||
|
outline: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
box-sizing: content-box;
|
||||||
|
height: 0;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
abbr[title],
|
||||||
|
abbr[data-original-title] {
|
||||||
|
text-decoration: underline;
|
||||||
|
-webkit-text-decoration: underline dotted;
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
cursor: help;
|
||||||
|
border-bottom: 0;
|
||||||
|
-webkit-text-decoration-skip-ink: none;
|
||||||
|
text-decoration-skip-ink: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
address {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul,
|
||||||
|
dl {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol ol,
|
||||||
|
ul ul,
|
||||||
|
ol ul,
|
||||||
|
ul ol {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
position: relative;
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #0056b3;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:not([href]):not([tabindex]) {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:not([href]):not([tabindex]):focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre,
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp {
|
||||||
|
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
vertical-align: middle;
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
overflow: hidden;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
caption {
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
|
color: #6c757d;
|
||||||
|
text-align: left;
|
||||||
|
caption-side: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus {
|
||||||
|
outline: 1px dotted;
|
||||||
|
outline: 5px auto -webkit-focus-ring-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
button,
|
||||||
|
select,
|
||||||
|
optgroup,
|
||||||
|
textarea {
|
||||||
|
margin: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
word-wrap: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
[type="button"],
|
||||||
|
[type="reset"],
|
||||||
|
[type="submit"] {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:not(:disabled),
|
||||||
|
[type="button"]:not(:disabled),
|
||||||
|
[type="reset"]:not(:disabled),
|
||||||
|
[type="submit"]:not(:disabled) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button::-moz-focus-inner,
|
||||||
|
[type="button"]::-moz-focus-inner,
|
||||||
|
[type="reset"]::-moz-focus-inner,
|
||||||
|
[type="submit"]::-moz-focus-inner {
|
||||||
|
padding: 0;
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"],
|
||||||
|
input[type="checkbox"] {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="date"],
|
||||||
|
input[type="time"],
|
||||||
|
input[type="datetime-local"],
|
||||||
|
input[type="month"] {
|
||||||
|
-webkit-appearance: listbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
overflow: auto;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
legend {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: inherit;
|
||||||
|
color: inherit;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="number"]::-webkit-inner-spin-button,
|
||||||
|
[type="number"]::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="search"] {
|
||||||
|
outline-offset: -2px;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="search"]::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
font: inherit;
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
|
||||||
|
output {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
template {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
||||||
File diff suppressed because one or more lines are too long
8
tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap-reboot.min.css
vendored
Normal file
8
tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap-reboot.min.css
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/*!
|
||||||
|
* Bootstrap Reboot v4.3.1 (https://getbootstrap.com/)
|
||||||
|
* Copyright 2011-2019 The Bootstrap Authors
|
||||||
|
* Copyright 2011-2019 Twitter, Inc.
|
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||||
|
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||||
|
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
|
||||||
|
/*# sourceMappingURL=bootstrap-reboot.min.css.map */
|
||||||
File diff suppressed because one or more lines are too long
10038
tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap.css
vendored
Normal file
10038
tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
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
vendored
Normal file
7
tapit-build/static/assets/bootstrap-4.3.1-dist/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
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
vendored
Normal file
7013
tapit-build/static/assets/bootstrap-4.3.1-dist/js/bootstrap.bundle.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
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
vendored
Normal file
7
tapit-build/static/assets/bootstrap-4.3.1-dist/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4435
tapit-build/static/assets/bootstrap-4.3.1-dist/js/bootstrap.js
vendored
Normal file
4435
tapit-build/static/assets/bootstrap-4.3.1-dist/js/bootstrap.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
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
vendored
Normal file
7
tapit-build/static/assets/bootstrap-4.3.1-dist/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
tapit-build/static/assets/jquery-3.4.1.min.js
vendored
Normal file
2
tapit-build/static/assets/jquery-3.4.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
tapit-build/static/assets/phonebook-template.xlsx
Normal file
BIN
tapit-build/static/assets/phonebook-template.xlsx
Normal file
Binary file not shown.
5
tapit-build/static/assets/popper.min.js
vendored
Normal file
5
tapit-build/static/assets/popper.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
tapit-build/static/es2015-polyfills.js
Normal file
2
tapit-build/static/es2015-polyfills.js
Normal file
File diff suppressed because one or more lines are too long
1
tapit-build/static/es2015-polyfills.js.map
Normal file
1
tapit-build/static/es2015-polyfills.js.map
Normal file
File diff suppressed because one or more lines are too long
BIN
tapit-build/static/favicon.ico
Normal file
BIN
tapit-build/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
21
tapit-build/static/index.html
Normal file
21
tapit-build/static/index.html
Normal file
@@ -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
Normal file
2
tapit-build/static/main.js
Normal file
File diff suppressed because one or more lines are too long
1
tapit-build/static/main.js.map
Normal file
1
tapit-build/static/main.js.map
Normal file
File diff suppressed because one or more lines are too long
2
tapit-build/static/polyfills.js
Normal file
2
tapit-build/static/polyfills.js
Normal file
File diff suppressed because one or more lines are too long
1
tapit-build/static/polyfills.js.map
Normal file
1
tapit-build/static/polyfills.js.map
Normal file
File diff suppressed because one or more lines are too long
2
tapit-build/static/runtime.js
Normal file
2
tapit-build/static/runtime.js
Normal file
@@ -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
Normal file
1
tapit-build/static/runtime.js.map
Normal file
File diff suppressed because one or more lines are too long
2
tapit-build/static/styles.js
Normal file
2
tapit-build/static/styles.js
Normal file
File diff suppressed because one or more lines are too long
1
tapit-build/static/styles.js.map
Normal file
1
tapit-build/static/styles.js.map
Normal file
File diff suppressed because one or more lines are too long
2
tapit-build/static/vendor.js
Normal file
2
tapit-build/static/vendor.js
Normal file
File diff suppressed because one or more lines are too long
1
tapit-build/static/vendor.js.map
Normal file
1
tapit-build/static/vendor.js.map
Normal file
File diff suppressed because one or more lines are too long
BIN
tapit-build/tapit
Executable file
BIN
tapit-build/tapit
Executable file
Binary file not shown.
13
tapit-frontend/.editorconfig
Normal file
13
tapit-frontend/.editorconfig
Normal file
@@ -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
Normal file
27
tapit-frontend/README.md
Normal file
@@ -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
Normal file
136
tapit-frontend/angular.json
Normal file
@@ -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
Normal file
28
tapit-frontend/e2e/protractor.conf.js
Normal file
@@ -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
Normal file
23
tapit-frontend/e2e/src/app.e2e-spec.ts
Normal file
@@ -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
Normal file
11
tapit-frontend/e2e/src/app.po.ts
Normal file
@@ -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
Normal file
13
tapit-frontend/e2e/tsconfig.e2e.json
Normal file
@@ -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
generated
Normal file
10514
tapit-frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
48
tapit-frontend/package.json
Normal file
48
tapit-frontend/package.json
Normal file
@@ -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
Normal file
37
tapit-frontend/src/app/app-routing.module.ts
Normal file
@@ -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
Normal file
0
tapit-frontend/src/app/app.component.css
Normal file
42
tapit-frontend/src/app/app.component.html
Normal file
42
tapit-frontend/src/app/app.component.html
Normal file
@@ -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
Normal file
35
tapit-frontend/src/app/app.component.spec.ts
Normal file
@@ -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
Normal file
37
tapit-frontend/src/app/app.component.ts
Normal file
@@ -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
Normal file
48
tapit-frontend/src/app/app.module.ts
Normal file
@@ -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
Normal file
12
tapit-frontend/src/app/auth.service.spec.ts
Normal file
@@ -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
Normal file
131
tapit-frontend/src/app/auth.service.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
|
import { NotificationService } from './notification.service';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
|
||||||
|
export class User {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
secretCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserNotification {
|
||||||
|
resultType: string;
|
||||||
|
text: string;
|
||||||
|
payload: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AuthService {
|
||||||
|
currUser = new User();
|
||||||
|
loggedin = false;
|
||||||
|
loginUrl = 'api/login';
|
||||||
|
logoutUrl = 'api/logout';
|
||||||
|
registerUrl = 'api/register';
|
||||||
|
myselfUrl = 'api/myself';
|
||||||
|
|
||||||
|
httpOptions = {
|
||||||
|
headers: new HttpHeaders({
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
login(username: string, password: string) {
|
||||||
|
this.currUser.username = username;
|
||||||
|
this.currUser.password = password;
|
||||||
|
this.http.post<UserNotification>(this.loginUrl, this.currUser, this.httpOptions).subscribe(usermessage => {
|
||||||
|
if (usermessage.payload !== null) {
|
||||||
|
this.loggedin = true;
|
||||||
|
|
||||||
|
// update user
|
||||||
|
this.currUser.username = usermessage.payload.username;
|
||||||
|
this.currUser.email = usermessage.payload.email;
|
||||||
|
this.currUser.name = usermessage.payload.name;
|
||||||
|
|
||||||
|
this.notificationService.addNotification(usermessage.resultType, usermessage.text);
|
||||||
|
this.router.navigate(['/campaign']);
|
||||||
|
} else {
|
||||||
|
this.notificationService.addNotification(usermessage.resultType, usermessage.text);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
this.notificationService.addNotification('failure', 'Error in logging in');
|
||||||
|
});
|
||||||
|
this.currUser.password = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
register(username: string, password: string, email: string, name: string, secretCode: string) {
|
||||||
|
this.currUser.username = username;
|
||||||
|
this.currUser.password = password;
|
||||||
|
this.currUser.email = email;
|
||||||
|
this.currUser.name = name;
|
||||||
|
this.currUser.secretCode = secretCode;
|
||||||
|
|
||||||
|
this.http.post<UserNotification>(this.registerUrl, this.currUser, this.httpOptions).subscribe(usermessage => {
|
||||||
|
if (usermessage.payload !== null) {
|
||||||
|
this.loggedin = true;
|
||||||
|
this.notificationService.addNotification(usermessage.resultType, usermessage.text);
|
||||||
|
this.router.navigate(['/campaign']);
|
||||||
|
|
||||||
|
// update user
|
||||||
|
this.currUser.username = usermessage.payload.username;
|
||||||
|
this.currUser.email = usermessage.payload.email;
|
||||||
|
this.currUser.name = usermessage.payload.name;
|
||||||
|
} else {
|
||||||
|
this.notificationService.addNotification(usermessage.resultType, usermessage.text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.currUser.secretCode = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
logout() {
|
||||||
|
this.http.post<UserNotification>(this.logoutUrl, '', this.httpOptions).subscribe(usermessage => {
|
||||||
|
this.notificationService.addNotification(usermessage.resultType, usermessage.text);
|
||||||
|
this.loggedin = false;
|
||||||
|
this.currUser = new User();
|
||||||
|
this.router.navigate(['/']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getUser(): User {
|
||||||
|
this.http.get<User>(this.myselfUrl, this.httpOptions).subscribe(thisUser => {
|
||||||
|
this.currUser = thisUser;
|
||||||
|
if (this.currUser.username !== '') {
|
||||||
|
this.loggedin = true;
|
||||||
|
} else {
|
||||||
|
this.router.navigate(['/']);
|
||||||
|
}
|
||||||
|
// separate one to redirect main to campaign dashboard
|
||||||
|
if (this.router.url === '/' || this.router.url === '') {
|
||||||
|
this.router.navigate(['/campaign']);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
this.router.navigate(['/']);
|
||||||
|
});
|
||||||
|
return this.currUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserObs(): Observable<User> {
|
||||||
|
return this.http.get<User>(this.myselfUrl, this.httpOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUser(user: User) {
|
||||||
|
this.currUser = user;
|
||||||
|
this.http.put<UserNotification>(this.myselfUrl, this.currUser, this.httpOptions).subscribe(usermessage => {
|
||||||
|
this.notificationService.addNotification(usermessage.resultType, usermessage.text);
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
this.notificationService.addNotification('failure', 'Error in updating profile');
|
||||||
|
});
|
||||||
|
this.currUser.password = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private http: HttpClient, private router: Router, private notificationService: NotificationService) { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<div class="row p-2">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="row mt-3 mb-3">
|
||||||
|
<div class="col-12 d-flex">
|
||||||
|
<label for="campaignName" class="pr-2 mt-auto mb-auto">Campaign Name</label>
|
||||||
|
<input type="text" class="flex-grow-1" id="campaignName" [(ngModel)]="newCampaign.name" placeholder="Campaign Name">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-3 mb-3">
|
||||||
|
<div class="col-12 d-flex">
|
||||||
|
<label for="newFromNum" class="pr-2 mt-auto mb-auto">From Number</label>
|
||||||
|
<input type="text" class="flex-grow-1" id="newFromNum" [(ngModel)]="newCampaign.fromNumber" placeholder="From Number">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Add phonebook & template via list -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="provider-select">Provider</label>
|
||||||
|
<select class="form-control" [(ngModel)]="newCampaign.providerTag" id="provider-select">
|
||||||
|
<option></option>
|
||||||
|
<option *ngFor="let providerEnum of providerService.providerEnums" [ngValue]="providerEnum.tag">{{providerEnum.name}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="phonebook-select">Phonebook</label>
|
||||||
|
<select class="form-control" [(ngModel)]="newCampaign.phonebookId" id="phonebook-select">
|
||||||
|
<option></option>
|
||||||
|
<option *ngFor="let phonebook of phonebookService.phonebooks" [ngValue]="phonebook.id">{{phonebook.name}}: Size {{phonebook.size}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="text-template-select">Text Template</label>
|
||||||
|
<select class="form-control" [(ngModel)]="newCampaign.textTemplateId" id="text-template-select">
|
||||||
|
<option></option>
|
||||||
|
<option *ngFor="let textTemplate of textTemplateService.textTemplates" [ngValue]="textTemplate.id">{{textTemplate.name}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-12 d-flex">
|
||||||
|
<button type="button" (click)="submitNewCampaignRun()" class="btn btn-primary mr-2">Start</button>
|
||||||
|
<button type="button" (click)="submitNewCampaign()" class="btn btn-secondary ml-2">Save</button>
|
||||||
|
<button type="button" *ngIf="router.url !== '/campaign/new'" (click)="askDelete()" class="btn btn-danger ml-auto" data-toggle="modal" data-target="#completeModal">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CampaignNewComponent } from './campaign-new.component';
|
||||||
|
|
||||||
|
describe('CampaignNewComponent', () => {
|
||||||
|
let component: CampaignNewComponent;
|
||||||
|
let fixture: ComponentFixture<CampaignNewComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ CampaignNewComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CampaignNewComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { CampaignService, Campaign } from '../campaign.service';
|
||||||
|
import { Router, ActivatedRoute, ParamMap } from '@angular/router';
|
||||||
|
import { ProviderService } from '../provider.service';
|
||||||
|
import { PhonebookService } from '../phonebook.service';
|
||||||
|
import { TextTemplateService } from '../text-template.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-campaign-new',
|
||||||
|
templateUrl: './campaign-new.component.html',
|
||||||
|
styleUrls: ['./campaign-new.component.css']
|
||||||
|
})
|
||||||
|
export class CampaignNewComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private campaignService: CampaignService,
|
||||||
|
private router: Router,
|
||||||
|
private providerService: ProviderService,
|
||||||
|
private phonebookService: PhonebookService,
|
||||||
|
private textTemplateService: TextTemplateService) { }
|
||||||
|
|
||||||
|
newCampaign: Campaign = new Campaign();
|
||||||
|
|
||||||
|
submitNewCampaign() {
|
||||||
|
this.campaignService.addCampaign(this.newCampaign);
|
||||||
|
}
|
||||||
|
|
||||||
|
submitNewCampaignRun() {
|
||||||
|
this.campaignService.addCampaignRun(this.newCampaign);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.campaign-details:read-only {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<div class="row">
|
||||||
|
<div class="col-12 mb-3 d-flux">
|
||||||
|
<button type="button" *ngIf="currCampaign.currentStatus === 'Running'" (click)="pauseCampaign()" class="btn btn-warning mr-2">Pause Campaign</button>
|
||||||
|
<button type="button" *ngIf="currCampaign.currentStatus !== 'Running'" (click)="startCampaign()" class="btn btn-primary mr-2">Start Campaign</button>
|
||||||
|
<button type="button" class="btn btn-danger ml-auto" data-toggle="modal" data-target="#completeModal">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">Campaign Name</span>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="form-control campaign-details" value="{{ currCampaign.name }}" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">Campaign Size</span>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="form-control campaign-details" value="{{ currCampaign.size }}" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">Campaign Status</span>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="form-control campaign-details" value="{{ currCampaign.currentStatus }}" readonly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-2">
|
||||||
|
<div class="col-12">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead class="thead-dark">
|
||||||
|
<tr>
|
||||||
|
<th scope="col">From</th>
|
||||||
|
<th scope="col">To</th>
|
||||||
|
<th scope="col">Currrent Status</th>
|
||||||
|
<th scope="col">Time Sent</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<ng-container *ngFor="let job of currCampaign.jobs">
|
||||||
|
<tr>
|
||||||
|
<td>{{ job.fromNum }}</td>
|
||||||
|
<td>{{ job.toNum }}</td>
|
||||||
|
<td>{{ job.currentStatus }}</td>
|
||||||
|
<td>{{ job.timeSent | date:'dd-MMM-yyyy'}}</td>
|
||||||
|
</tr>
|
||||||
|
</ng-container>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="modal fade" id="completeModal" tabindex="-1" role="dialog" aria-labelledby="completeModal" aria-hidden="true">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="exampleModalLabel">{{ currCampaign.name }}</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Are you sure you want to delete the campaign?</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||||
|
<button type="button" class="btn btn-danger" (click)="deleteCampaign()" data-dismiss="modal">Delete Campaign</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CampaignViewComponent } from './campaign-view.component';
|
||||||
|
|
||||||
|
describe('CampaignViewComponent', () => {
|
||||||
|
let component: CampaignViewComponent;
|
||||||
|
let fixture: ComponentFixture<CampaignViewComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ CampaignViewComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CampaignViewComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { CampaignService, Campaign, Job, CampaignNotification } from '../campaign.service';
|
||||||
|
import { Router, ActivatedRoute, ParamMap } from '@angular/router';
|
||||||
|
import { NotificationService } from '../notification.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-campaign-view',
|
||||||
|
templateUrl: './campaign-view.component.html',
|
||||||
|
styleUrls: ['./campaign-view.component.css']
|
||||||
|
})
|
||||||
|
export class CampaignViewComponent implements OnInit {
|
||||||
|
|
||||||
|
currCampaign: Campaign = new Campaign();
|
||||||
|
|
||||||
|
id = 0;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private campaignService: CampaignService,
|
||||||
|
private router: Router,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private notificationService: NotificationService
|
||||||
|
) { }
|
||||||
|
|
||||||
|
startCampaign() {
|
||||||
|
this.campaignService.startCampaign(this.currCampaign).subscribe(campaignNotification => {
|
||||||
|
this.notificationService.addNotification(campaignNotification.resultType, campaignNotification.text);
|
||||||
|
this.campaignService.getCampaignObs(this.id).subscribe(campaign => {
|
||||||
|
this.currCampaign = campaign;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
this.notificationService.addNotification('failure', 'Error in starting campaign');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pauseCampaign() {
|
||||||
|
this.campaignService.pauseCampaign(this.currCampaign).subscribe(campaignNotification => {
|
||||||
|
this.notificationService.addNotification(campaignNotification.resultType, campaignNotification.text);
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
this.notificationService.addNotification('failure', 'Error in pausing campaign');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteCampaign() {
|
||||||
|
this.campaignService.deleteCampaign(this.currCampaign);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateThisCampaign() {
|
||||||
|
this.campaignService.getCampaignObs(this.id).subscribe(campaign => {
|
||||||
|
this.currCampaign = campaign;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
const idParam = 'id';
|
||||||
|
this.route.params.subscribe( params => {
|
||||||
|
this.id = parseInt(params[idParam], 10);
|
||||||
|
});
|
||||||
|
this.updateThisCampaign();
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
this.updateThisCampaign();
|
||||||
|
if (!this.router.url.includes('/campaign')) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
12
tapit-frontend/src/app/campaign.service.spec.ts
Normal file
12
tapit-frontend/src/app/campaign.service.spec.ts
Normal file
@@ -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
Normal file
115
tapit-frontend/src/app/campaign.service.ts
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
31
tapit-frontend/src/app/campaign/campaign.component.html
Normal file
31
tapit-frontend/src/app/campaign/campaign.component.html
Normal file
@@ -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
Normal file
25
tapit-frontend/src/app/campaign/campaign.component.spec.ts
Normal file
@@ -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
Normal file
24
tapit-frontend/src/app/campaign/campaign.component.ts
Normal file
@@ -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
Normal file
0
tapit-frontend/src/app/login/login.component.css
Normal file
27
tapit-frontend/src/app/login/login.component.html
Normal file
27
tapit-frontend/src/app/login/login.component.html
Normal file
@@ -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
Normal file
25
tapit-frontend/src/app/login/login.component.spec.ts
Normal file
@@ -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
Normal file
27
tapit-frontend/src/app/login/login.component.ts
Normal file
@@ -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
Normal file
0
tapit-frontend/src/app/main/main.component.css
Normal file
3
tapit-frontend/src/app/main/main.component.html
Normal file
3
tapit-frontend/src/app/main/main.component.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<p>
|
||||||
|
main works!
|
||||||
|
</p>
|
||||||
25
tapit-frontend/src/app/main/main.component.spec.ts
Normal file
25
tapit-frontend/src/app/main/main.component.spec.ts
Normal file
@@ -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
Normal file
15
tapit-frontend/src/app/main/main.component.ts
Normal file
@@ -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
Normal file
12
tapit-frontend/src/app/notification.service.spec.ts
Normal file
@@ -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
Normal file
38
tapit-frontend/src/app/notification.service.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
export class Notification {
|
||||||
|
id: number;
|
||||||
|
resultType: string; // enum success or failure or info
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class NotificationService {
|
||||||
|
notifications: Notification[] = [];
|
||||||
|
currentCount = 0;
|
||||||
|
|
||||||
|
addNotification(resultType, text) {
|
||||||
|
const newNotification = new Notification();
|
||||||
|
newNotification.id = this.currentCount;
|
||||||
|
this.currentCount++;
|
||||||
|
newNotification.resultType = resultType;
|
||||||
|
newNotification.text = text;
|
||||||
|
|
||||||
|
this.notifications.push(newNotification);
|
||||||
|
setTimeout(() => this.closeNotification(newNotification), 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeNotification(notify: Notification) {
|
||||||
|
for (let i = 0; i < this.notifications.length; i++) {
|
||||||
|
if (this.notifications[i].id === notify.id) {
|
||||||
|
this.notifications.splice(i, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<div class="alert notification col-11 mx-auto" *ngFor="let notification of notificationService.notifications" [ngClass]="{'alert-success': notification.resultType === 'success', 'alert-danger': notification.resultType ==='failure'}" (click)=notificationService.closeNotification(notification)>
|
||||||
|
{{ notification.text }}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { NotificationComponent } from './notification.component';
|
||||||
|
|
||||||
|
describe('NotificationComponent', () => {
|
||||||
|
let component: NotificationComponent;
|
||||||
|
let fixture: ComponentFixture<NotificationComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ NotificationComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(NotificationComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { NotificationService } from '../notification.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-notification',
|
||||||
|
templateUrl: './notification.component.html',
|
||||||
|
styleUrls: ['./notification.component.css']
|
||||||
|
})
|
||||||
|
export class NotificationComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor(private notificationService: NotificationService) { }
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.no-space-break {
|
||||||
|
white-space:nowrap;
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<div class="row p-2">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12 d-flex">
|
||||||
|
<label for="campaignName" class="pr-2 mt-auto mb-auto">Phonebook Name</label>
|
||||||
|
<input type="text" class="flex-grow-1" id="campaignName" [(ngModel)]="newPhonebook.name" placeholder="Phonebook Name">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12 d-flex">
|
||||||
|
<label class="no-space-break mt-auto mb-auto pr-2" for="import-records">Import Records</label>
|
||||||
|
<div class="custom-file" id="import-records">
|
||||||
|
<input type="file" (change)="importPhoneRecords($event.target.files)" class="custom-file-input" id="customFile">
|
||||||
|
<label class="custom-file-label" for="customFile">Choose file</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 d-flex">
|
||||||
|
<p><small><em><a href="/assets/phonebook-template.xlsx">Download file template here.</a></em></small></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-1">
|
||||||
|
<div class="col-12 d-flex">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead class="thead-dark">
|
||||||
|
<tr>
|
||||||
|
<th scope="col">First Name</th>
|
||||||
|
<th scope="col">Last Name</th>
|
||||||
|
<th scope="col">Alias</th>
|
||||||
|
<th scope="col">Phone Number</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<ng-container *ngFor="let phoneRecord of newPhoneRecords">
|
||||||
|
<tr>
|
||||||
|
<td>{{ phoneRecord.firstName }}</td>
|
||||||
|
<td>{{ phoneRecord.lastName }}</td>
|
||||||
|
<td>{{ phoneRecord.alias }}</td>
|
||||||
|
<td>{{ phoneRecord.phoneNumber }}</td>
|
||||||
|
</tr>
|
||||||
|
</ng-container>
|
||||||
|
<tr (keyup.enter)="insertAdditionalRecord()">
|
||||||
|
<td><input type="text" [(ngModel)]="additionalRecord.firstName" class="form-control" placeholder="firstName"></td>
|
||||||
|
<td><input type="text" [(ngModel)]="additionalRecord.lastName" class="form-control" placeholder="lastName"></td>
|
||||||
|
<td><input type="text" [(ngModel)]="additionalRecord.alias" class="form-control" placeholder="alias"></td>
|
||||||
|
<td><input type="text" [(ngModel)]="additionalRecord.phoneNumber" class="form-control" placeholder="phoneNumber"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<p><small><em>Press enter to insert additional record</em></small></p>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12 d-flex">
|
||||||
|
<button type="button" (click)="submitNewPhonebook()" class="btn btn-primary mr-2">Save Phonebook</button>
|
||||||
|
<button type="button" *ngIf="router.url !== '/phonebook/new'" class="btn btn-danger ml-auto" data-toggle="modal" data-target="#completeModal">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="completeModal" tabindex="-1" role="dialog" aria-labelledby="completeModal" aria-hidden="true">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="exampleModalLabel">{{ newPhonebook.name }}</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Are you sure you want to delete the phonebook?</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||||
|
<button type="button" class="btn btn-danger" (click)="deletePhonebook()" data-dismiss="modal">Delete Phonebook</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user