First commit

This commit is contained in:
2019-05-08 16:38:12 +08:00
commit d504997def
172 changed files with 66423 additions and 0 deletions

47
.gitignore vendored Normal file
View 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
View 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
View 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

Binary file not shown.

4
psql-start.sh Normal file
View 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
View 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
View 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(&currentJob.Job)
}
} else {
exitCode = true
}
}
wg.Done()
}
func (tapit *Tapit) clearRunningCampaigns() {
var campaigns []Campaign
var searchCampaign Campaign
searchCampaign.CurrentStatus = "Running"
tapit.db.Where(&searchCampaign).Find(&campaigns)
for _, campaign := range campaigns {
campaign.CurrentStatus = "Paused"
tapit.db.Save(&campaign)
}
}

143
tapit-backend/main.go Normal file
View 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)
}

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

Binary file not shown.

View 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
View 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
View 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
View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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

View 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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
tapit-build/tapit Executable file

Binary file not shown.

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

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

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

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

View 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

File diff suppressed because it is too large Load Diff

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

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

View File

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

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

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

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

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

View 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) { }
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,77 @@
<div class="row">
<div class="col-12 mb-3 d-flux">
<button type="button" *ngIf="currCampaign.currentStatus === 'Running'" (click)="pauseCampaign()" class="btn btn-warning mr-2">Pause Campaign</button>
<button type="button" *ngIf="currCampaign.currentStatus !== 'Running'" (click)="startCampaign()" class="btn btn-primary mr-2">Start Campaign</button>
<button type="button" class="btn btn-danger ml-auto" data-toggle="modal" data-target="#completeModal">Delete</button>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text">Campaign Name</span>
</div>
<input type="text" class="form-control campaign-details" value="{{ currCampaign.name }}" readonly>
</div>
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text">Campaign Size</span>
</div>
<input type="text" class="form-control campaign-details" value="{{ currCampaign.size }}" readonly>
</div>
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text">Campaign Status</span>
</div>
<input type="text" class="form-control campaign-details" value="{{ currCampaign.currentStatus }}" readonly>
</div>
</div>
</div>
<div class="row mt-2">
<div class="col-12">
<table class="table table-hover">
<thead class="thead-dark">
<tr>
<th scope="col">From</th>
<th scope="col">To</th>
<th scope="col">Currrent Status</th>
<th scope="col">Time Sent</th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let job of currCampaign.jobs">
<tr>
<td>{{ job.fromNum }}</td>
<td>{{ job.toNum }}</td>
<td>{{ job.currentStatus }}</td>
<td>{{ job.timeSent | date:'dd-MMM-yyyy'}}</td>
</tr>
</ng-container>
</tbody>
</table>
</div>
</div>
<div class="modal fade" id="completeModal" tabindex="-1" role="dialog" aria-labelledby="completeModal" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">{{ currCampaign.name }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete the campaign?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-danger" (click)="deleteCampaign()" data-dismiss="modal">Delete Campaign</button>
</div>
</div>
</div>
</div>

View File

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

View File

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

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

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

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

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

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

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

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

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,84 @@
<div class="row p-2">
<div class="col-12">
<div class="row mt-3">
<div class="col-12 d-flex">
<label for="campaignName" class="pr-2 mt-auto mb-auto">Phonebook Name</label>
<input type="text" class="flex-grow-1" id="campaignName" [(ngModel)]="newPhonebook.name" placeholder="Phonebook Name">
</div>
</div>
<div class="row mt-3">
<div class="col-12 d-flex">
<label class="no-space-break mt-auto mb-auto pr-2" for="import-records">Import Records</label>
<div class="custom-file" id="import-records">
<input type="file" (change)="importPhoneRecords($event.target.files)" class="custom-file-input" id="customFile">
<label class="custom-file-label" for="customFile">Choose file</label>
</div>
</div>
</div>
<div class="row">
<div class="col-12 d-flex">
<p><small><em><a href="/assets/phonebook-template.xlsx">Download file template here.</a></em></small></p>
</div>
</div>
<div class="row mt-1">
<div class="col-12 d-flex">
<table class="table table-hover">
<thead class="thead-dark">
<tr>
<th scope="col">First Name</th>
<th scope="col">Last Name</th>
<th scope="col">Alias</th>
<th scope="col">Phone Number</th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let phoneRecord of newPhoneRecords">
<tr>
<td>{{ phoneRecord.firstName }}</td>
<td>{{ phoneRecord.lastName }}</td>
<td>{{ phoneRecord.alias }}</td>
<td>{{ phoneRecord.phoneNumber }}</td>
</tr>
</ng-container>
<tr (keyup.enter)="insertAdditionalRecord()">
<td><input type="text" [(ngModel)]="additionalRecord.firstName" class="form-control" placeholder="firstName"></td>
<td><input type="text" [(ngModel)]="additionalRecord.lastName" class="form-control" placeholder="lastName"></td>
<td><input type="text" [(ngModel)]="additionalRecord.alias" class="form-control" placeholder="alias"></td>
<td><input type="text" [(ngModel)]="additionalRecord.phoneNumber" class="form-control" placeholder="phoneNumber"></td>
</tr>
<tr>
<p><small><em>Press enter to insert additional record</em></small></p>
</tr>
</tbody>
</table>
</div>
</div>
<div class="row mt-3">
<div class="col-12 d-flex">
<button type="button" (click)="submitNewPhonebook()" class="btn btn-primary mr-2">Save Phonebook</button>
<button type="button" *ngIf="router.url !== '/phonebook/new'" class="btn btn-danger ml-auto" data-toggle="modal" data-target="#completeModal">Delete</button>
</div>
</div>
</div>
<div class="modal fade" id="completeModal" tabindex="-1" role="dialog" aria-labelledby="completeModal" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">{{ newPhonebook.name }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete the phonebook?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-danger" (click)="deletePhonebook()" data-dismiss="modal">Delete Phonebook</button>
</div>
</div>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More