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