Skip to content

Commit

Permalink
Merge pull request #145 from Avanade/sprint-35
Browse files Browse the repository at this point in the history
Sprint 35
iibuan authored Oct 18, 2024
2 parents 6345cc4 + fa5985b commit 188f833
Showing 41 changed files with 985 additions and 787 deletions.
4 changes: 3 additions & 1 deletion .bicep/webapp/parameters.json
Original file line number Diff line number Diff line change
@@ -41,7 +41,9 @@
"EMAIL_CLIENT_SECRET" : "",
"EMAIL_USER_ID" : "",
"LINK_FOOTERS": "",
"ORGANIZATION_NAME": ""
"ORGANIZATION_NAME": "",
"COMMUNITY_PORTAL_APP_ID": "",
"CALLBACK_RETRY_FREQ": ""
}
}
}
2 changes: 2 additions & 0 deletions .github/workflows/setup-appservice-resource.yml
Original file line number Diff line number Diff line change
@@ -45,6 +45,8 @@ jobs:
parameters.appServiceSettings.value.DOCKER_REGISTRY_SERVER_USERNAME : ${{ secrets.CONTAINER_REGISTRY_SERVER_USERNAME }}
parameters.appServiceSettings.value.DOCKER_REGISTRY_SERVER_PASSWORD : ${{ secrets.CONTAINER_REGISTRY_SERVER_PASSWORD }}
parameters.appServiceSettings.value.APPROVALSYSTEMDB_CONNECTION_STRING : ${{ secrets.DATABASE_CONNECTION_STRING }}
parameters.appServiceSettings.value.COMMUNITY_PORTAL_APP_ID : ${{ vars.COMMUNITY_PORTAL_APP_ID }}
parameters.appServiceSettings.value.CALLBACK_RETRY_FREQ: ${{ vars.CALLBACK_RETRY_FREQ }}

- name: Deploy App Service Plan and Web App
uses: azure/arm-deploy@v1
3 changes: 2 additions & 1 deletion src/goapp/.env.template
Original file line number Diff line number Diff line change
@@ -16,4 +16,5 @@ EMAIL_CLIENT_SECRET=<Client Secret>
EMAIL_ENABLED=<Email enabled. default: false>
EMAIL_USER_ID=<Email user id>
LINK_FOOTERS=""
ORGANIZATION_NAME=""
ORGANIZATION_NAME=""
COMMUNITY_PORTAL_APP_ID=""
4 changes: 4 additions & 0 deletions src/goapp/config/config.go
Original file line number Diff line number Diff line change
@@ -17,4 +17,8 @@ type ConfigManager interface {
GetTenantID() string
GetClientID() string
GetClientSecret() string
GetLinkFooters() string
GetOrganizationName() string
GetCommunityPortalAppId() string
GetCallbackRetryFreq() string
}
16 changes: 16 additions & 0 deletions src/goapp/config/env-config.go
Original file line number Diff line number Diff line change
@@ -66,3 +66,19 @@ func (ecm *envConfigManager) GetClientID() string {
func (ecm *envConfigManager) GetClientSecret() string {
return os.Getenv("CLIENT_SECRET")
}

func (ecm *envConfigManager) GetLinkFooters() string {
return os.Getenv("LINK_FOOTERS")
}

func (ecm *envConfigManager) GetOrganizationName() string {
return os.Getenv("ORGANIZATION_NAME")
}

func (ecm *envConfigManager) GetCommunityPortalAppId() string {
return os.Getenv("COMMUNITY_PORTAL_APP_ID")
}

func (ecm *envConfigManager) GetCallbackRetryFreq() string {
return os.Getenv("CALLBACK_RETRY_FREQ")
}
10 changes: 10 additions & 0 deletions src/goapp/controller/controller.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package controller

import (
"main/config"
cApplicationModule "main/controller/app-module"
cItem "main/controller/item"
cUser "main/controller/user"
@@ -9,6 +10,7 @@ import (

type Controller struct {
Item cItem.ItemController
ItemPage cItem.ItemPageController
ApplicationModule cApplicationModule.ApplicationModuleController
User cUser.UserController
}
@@ -42,3 +44,11 @@ func NewUserController(svc *service.Service) ControllerOptionFunc {
c.User = cUser.NewUserController(svc)
}
}

// PAGES

func NewItemPageController(svc *service.Service, conf config.ConfigManager) ControllerOptionFunc {
return func(c *Controller) {
c.ItemPage = cItem.NewItemPageController(svc, conf)
}
}
15 changes: 15 additions & 0 deletions src/goapp/controller/item/item-controller-dto.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package item

import "main/model"

type ReassignItemCallback struct {
Id string `json:"Id"`
ApproverEmail string `json:"ApproverEmail"`
@@ -9,3 +11,16 @@ type ReassignItemCallback struct {
ApproveText string `json:"ApproveText"`
RejectText string `json:"RejectText"`
}

type RespondePageData struct {
ApplicationId string
ApplicationModuleId string
ItemId string
ApproverEmail string
IsApproved string
Data model.Item
RequireRemarks bool
Response string
ApproveText string
RejectText string
}
7 changes: 7 additions & 0 deletions src/goapp/controller/item/item-controller-interface.go
Original file line number Diff line number Diff line change
@@ -8,3 +8,10 @@ type ItemController interface {
ProcessResponse(w http.ResponseWriter, r *http.Request)
ReassignItem(w http.ResponseWriter, r *http.Request)
}

type ItemPageController interface {
MyRequests(w http.ResponseWriter, r *http.Request)
MyApprovals(w http.ResponseWriter, r *http.Request)
RespondToItem(w http.ResponseWriter, r *http.Request)
ReassignApproval(w http.ResponseWriter, r *http.Request)
}
258 changes: 258 additions & 0 deletions src/goapp/controller/item/item-page-controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
package item

import (
"encoding/json"
"fmt"
"main/config"
"main/model"
"main/pkg/session"
"main/service"
"net/http"

"github.com/gorilla/mux"
)

type itemPageController struct {
*service.Service
CommunityPortalAppId string
}

func NewItemPageController(s *service.Service, conf config.ConfigManager) ItemPageController {
return &itemPageController{
Service: s,
CommunityPortalAppId: conf.GetCommunityPortalAppId(),
}
}

func (c *itemPageController) MyRequests(w http.ResponseWriter, r *http.Request) {
session, err := session.Store.Get(r, "auth-session")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

var profile map[string]interface{}
u := session.Values["profile"]
profile, ok := u.(map[string]interface{})
if !ok {
http.Error(w, "Error getting user data", http.StatusInternalServerError)
return
}
user := model.AzureUser{
Name: profile["name"].(string),
Email: profile["preferred_username"].(string),
}

application, err := c.Service.Application.GetApplicationById(c.CommunityPortalAppId)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

b, err := json.Marshal(application)
if err != nil {
fmt.Println(err)
return
}

t, d := c.Service.Template.UseTemplate("myrequests", r.URL.Path, user, string(b))

err = t.Execute(w, d)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}

func (c *itemPageController) MyApprovals(w http.ResponseWriter, r *http.Request) {
session, err := session.Store.Get(r, "auth-session")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

var profile map[string]interface{}
u := session.Values["profile"]
profile, ok := u.(map[string]interface{})
if !ok {
http.Error(w, "Error getting user data", http.StatusInternalServerError)
return
}
user := model.AzureUser{
Name: profile["name"].(string),
Email: profile["preferred_username"].(string),
}

application, err := c.Service.Application.GetApplicationById(c.CommunityPortalAppId)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

b, err := json.Marshal(application)
if err != nil {
fmt.Println(err)
return
}

t, d := c.Service.Template.UseTemplate("myapprovals", r.URL.Path, user, string(b))

err = t.Execute(w, d)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}

func (c *itemPageController) RespondToItem(w http.ResponseWriter, r *http.Request) {
session, _ := session.Store.Get(r, "auth-session")

var profile map[string]interface{}
u := session.Values["profile"]
profile, ok := u.(map[string]interface{})
if !ok {
http.Error(w, "Error getting user data", http.StatusInternalServerError)
return
}
user := model.AzureUser{
Name: profile["name"].(string),
Email: profile["preferred_username"].(string),
}

params := mux.Vars(r)

itemIsAuthorized, err := c.Service.Item.ItemIsAuthorized(
params["appGuid"],
params["appModuleGuid"],
params["itemGuid"],
user.Email,
)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

if !itemIsAuthorized.IsAuthorized {
t, d := c.Service.Template.UseTemplate("Unauthorized", r.URL.Path, user, nil)
err = t.Execute(w, d)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
} else {
if itemIsAuthorized.IsApproved != nil {
var text string
if itemIsAuthorized.IsApproved.Value {
text = "approved"
} else {
text = "rejected"
}

data := RespondePageData{
Response: text,
}

t, d := c.Service.Template.UseTemplate("already-processed", r.URL.Path, user, data)
err = t.Execute(w, d)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
} else {
item, err := c.Service.Item.GetItemById(params["itemGuid"])
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

data := RespondePageData{
ApplicationId: params["appGuid"],
ApplicationModuleId: params["appModuleGuid"],
ItemId: params["itemGuid"],
ApproverEmail: user.Email,
IsApproved: params["isApproved"],
Data: *item,
RequireRemarks: itemIsAuthorized.RequireRemarks,
}

t, d := c.Service.Template.UseTemplate("response", r.URL.Path, user, data)
err = t.Execute(w, d)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}
}

func (c *itemPageController) ReassignApproval(w http.ResponseWriter, r *http.Request) {
session, _ := session.Store.Get(r, "auth-session")

var profile map[string]interface{}
u := session.Values["profile"]
profile, ok := u.(map[string]interface{})
if !ok {
http.Error(w, "Error getting user data", http.StatusInternalServerError)
return
}
user := model.AzureUser{
Name: profile["name"].(string),
Email: profile["preferred_username"].(string),
}

params := mux.Vars(r)

itemIsAuthorized, err := c.Service.Item.ItemIsAuthorized(
params["appGuid"],
params["appModuleGuid"],
params["itemGuid"],
user.Email,
)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

if !itemIsAuthorized.IsAuthorized {
t, d := c.Service.Template.UseTemplate("Unauthorized", r.URL.Path, user, nil)
err = t.Execute(w, d)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
} else {
if itemIsAuthorized.IsApproved != nil {
var text string
if itemIsAuthorized.IsApproved.Value {
text = "approved"
} else {
text = "rejected"
}

data := RespondePageData{
Response: text,
}

t, d := c.Service.Template.UseTemplate("already-processed", r.URL.Path, user, data)
err = t.Execute(w, d)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
} else {
item, err := c.Service.Item.GetItemById(params["itemGuid"])
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

data := RespondePageData{
ApplicationId: params["appGuid"],
ApplicationModuleId: params["appModuleGuid"],
ItemId: params["itemGuid"],
Data: *item,
ApproveText: params["ApproveText"],
RejectText: params["RejectText"],
}

t, d := c.Service.Template.UseTemplate("reassign", r.URL.Path, user, data)
err = t.Execute(w, d)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}
}
7 changes: 7 additions & 0 deletions src/goapp/init.go
Original file line number Diff line number Diff line change
@@ -8,31 +8,38 @@ import (
c "main/controller"
r "main/repository"
s "main/service"
t "main/timed-jobs"
)

var (
conf config.ConfigManager = config.NewEnvConfigManager()
db database.Database = database.NewDatabase(conf)

repo = r.NewRepository(
r.NewApplication(&db),
r.NewApplicationModule(&db),
r.NewItem(&db),
r.NewApprovalRequestApprover(&db),
)

svc = s.NewService(
s.NewApplicationService(repo),
s.NewApplicationModuleService(repo),
s.NewItemService(repo, conf),
s.NewEmailService(conf),
s.NewApprovalRequestApproverService(repo),
s.NewMsGraphService(conf),
s.NewTemplateService(conf),
)

ctrl = c.NewController(
c.NewItemController(svc),
c.NewApplicationModuleController(svc),
c.NewUserController(svc),
c.NewItemPageController(svc, conf),
)

timedJobs = t.NewTimedJobs(svc, conf)

httpRouter router.Router = router.NewMuxRouter()
)
18 changes: 1 addition & 17 deletions src/goapp/main.go
Original file line number Diff line number Diff line change
@@ -3,11 +3,6 @@ package main
import (
"log"
session "main/pkg/session"
rtApprovals "main/routes/pages/approvals"
"strconv"
"time"

ev "main/pkg/envvar"

"github.com/joho/godotenv"
)
@@ -22,21 +17,10 @@ func main() {
// Create session and GitHubClient
session.InitializeSession()

go checkFailedCallbacks()
go timedJobs.ReprocessFailedCallbacks()

setPageRoutes()
setApiRoutes()

serve()
}

func checkFailedCallbacks() {
// TIMER SERVICE
freq := ev.GetEnvVar("CALLBACK_RETRY_FREQ", "15")
freqInt, _ := strconv.ParseInt(freq, 0, 64)
if freq > "0" {
for range time.NewTicker(time.Duration(freqInt) * time.Minute).C {
rtApprovals.ProcessFailedCallbacks()
}
}
}
8 changes: 8 additions & 0 deletions src/goapp/model/application.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package model

type Application struct {
Id string
Name string
ExportUrl string
OrganizationTypeUrl string
}
10 changes: 10 additions & 0 deletions src/goapp/model/item.go
Original file line number Diff line number Diff line change
@@ -89,3 +89,13 @@ type ResponseCallback struct {
ResponseDate string `json:"responseDate"`
RespondedBy string `json:"respondedBy"`
}

type ItemIsAuthorized struct {
IsAuthorized bool `json:"isAuthorized"`
IsApproved *NullBool `json:"isApproved"`
RequireRemarks bool `json:"requireRemarks"`
}

type NullBool struct {
Value bool
}
18 changes: 9 additions & 9 deletions src/goapp/model/data.go → src/goapp/model/template.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package model

type TypPageData struct {
Header interface{}
Profile interface{}
Content interface{}
type MasterPageData struct {
Header Headers
Profile AzureUser
Content interface{} `json:"content"`
Footers []Footer
OrganizationName string
}
@@ -13,20 +13,20 @@ type Footer struct {
Url string
}

type TypHeaders struct {
Menu []TypMenu
ExternalLinks []TypMenu
type Headers struct {
Menu []Menu
ExternalLinks []Menu
Page string
}

type TypMenu struct {
type Menu struct {
Name string
Url string
IconPath string
UrlPath string
}

type TypAzureUser struct {
type AzureUser struct {
Name string `json:"name"`
Email string `json:"preferred_username"`
}
23 changes: 11 additions & 12 deletions src/goapp/pkg/template/template.go
Original file line number Diff line number Diff line change
@@ -20,18 +20,17 @@ func UseTemplate(w *http.ResponseWriter, r *http.Request, page string, pageData
}
profile := sessionaz.Values["profile"]

if profile == nil {
profile = map[string]interface{}{
"name": "",
"preferred_username": "",
}
user := model.AzureUser{}
if profile != nil {
user.Name = profile.(map[string]interface{})["name"].(string)
user.Email = profile.(map[string]interface{})["preferred_username"].(string)
}

// Data on master page
var menu []model.TypMenu
menu = append(menu, model.TypMenu{Name: "My Requests", Url: "/", IconPath: "/public/icons/projects.svg"})
menu = append(menu, model.TypMenu{Name: "My Approvals", Url: "/myapprovals", IconPath: "/public/icons/approvals.svg"})
masterPageData := model.TypHeaders{Menu: menu, Page: getUrlPath(r.URL.Path)}
var menu []model.Menu
menu = append(menu, model.Menu{Name: "My Requests", Url: "/", IconPath: "/public/icons/projects.svg"})
menu = append(menu, model.Menu{Name: "My Approvals", Url: "/myapprovals", IconPath: "/public/icons/approvals.svg"})
masterPageData := model.Headers{Menu: menu, Page: getUrlPath(r.URL.Path)}

//Footers
var footers []model.Footer
@@ -42,9 +41,9 @@ func UseTemplate(w *http.ResponseWriter, r *http.Request, page string, pageData
footers = append(footers, model.Footer{Text: f[0], Url: f[1]})
}

data := model.TypPageData{
data := model.MasterPageData{
Header: masterPageData,
Profile: profile,
Profile: user,
Content: pageData,
Footers: footers,
OrganizationName: os.Getenv("ORGANIZATION_NAME"),
@@ -53,7 +52,7 @@ func UseTemplate(w *http.ResponseWriter, r *http.Request, page string, pageData
tmpl := template.Must(
template.ParseFiles("templates/master.html", "templates/buttons.html",
fmt.Sprintf("templates/%v.html", page)))
(*w).WriteHeader(http.StatusBadRequest)

return tmpl.Execute(*w, data)
}

8 changes: 2 additions & 6 deletions src/goapp/public/css/output.css
Original file line number Diff line number Diff line change
@@ -1847,18 +1847,14 @@ select {
top: 50%;
}

.top-5 {
top: 1.25rem;
.top-8 {
top: 2rem;
}

.top-full {
top: 100%;
}

.top-8 {
top: 2rem;
}

.isolate {
isolation: isolate;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package application

import (
"main/model"
)

type ApplicationRepository interface {
GetApplicationById(id string) (*model.Application, error)
}
44 changes: 44 additions & 0 deletions src/goapp/repository/application/application-repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package application

import (
"database/sql"
db "main/infrastructure/database"
"main/model"
)

type applicationRepository struct {
*db.Database
}

func NewApplicationRepository(db *db.Database) ApplicationRepository {
return &applicationRepository{
Database: db,
}
}

func (r *applicationRepository) GetApplicationById(id string) (*model.Application, error) {
var application model.Application
rowApplication, err := r.Query("PR_Applications_Select_ById", sql.Named("Id", id))
if err != nil {
return nil, err
}

applications, err := r.RowsToMap(rowApplication)
if err != nil {
return nil, err
}

if len(applications) == 0 {
return nil, nil
} else {
application.Id = applications[0]["Id"].(string)
application.Name = applications[0]["Name"].(string)
if applications[0]["ExportUrl"] != nil {
application.ExportUrl = applications[0]["ExportUrl"].(string)
}
if applications[0]["OrganizationTypeUrl"] != nil {
application.OrganizationTypeUrl = applications[0]["OrganizationTypeUrl"].(string)
}
}
return &application, nil
}
2 changes: 2 additions & 0 deletions src/goapp/repository/item/item-repository-interface.go
Original file line number Diff line number Diff line change
@@ -5,10 +5,12 @@ import (
)

type ItemRepository interface {
GetFailedCallbacks() ([]string, error)
GetItemById(id string) (*model.Item, error)
GetItemsBy(itemOptions model.ItemOptions) ([]model.Item, error)
GetTotalItemsBy(itemOptions model.ItemOptions) (int, error)
InsertItem(appModuleId, subject, body, requesterEmail string) (string, error)
ItemIsAuthorized(appId, appModuleId, itemId, approverEmail string) (*model.ItemIsAuthorized, error)
UpdateItemApproverEmail(id, approverEmail, username string) error
UpdateItemCallback(id string, isCallbackFailed bool) error
UpdateItemDateSent(id string) error
53 changes: 53 additions & 0 deletions src/goapp/repository/item/item-repository.go
Original file line number Diff line number Diff line change
@@ -19,6 +19,26 @@ func NewItemRepository(db *db.Database) ItemRepository {
}
}

func (r *itemRepository) GetFailedCallbacks() ([]string, error) {
row, err := r.Query("PR_Items_Select_FailedCallbacks")
if err != nil {
return []string{}, err
}
defer row.Close()

result, err := r.RowsToMap(row)
if err != nil {
return []string{}, err
}

var failedCallbacks []string
for _, v := range result {
failedCallbacks = append(failedCallbacks, v["Id"].(string))
}

return failedCallbacks, nil
}

func (r *itemRepository) GetItemById(id string) (*model.Item, error) {
row, err := r.Query("PR_Items_Select_ById", sql.Named("Id", id))
if err != nil {
@@ -235,6 +255,39 @@ func (r *itemRepository) UpdateItemApproverEmail(id, approverEmail, username str
defer row.Close()
return nil
}
func (r *itemRepository) ItemIsAuthorized(appId, appModuleId, itemId, approverEmail string) (*model.ItemIsAuthorized, error) {
row, err := r.Query("PR_Items_IsAuthorized",
sql.Named("ApplicationId", appId),
sql.Named("ApplicationModuleId", appModuleId),
sql.Named("ItemId", itemId),
sql.Named("ApproverEmail", approverEmail),
)
if err != nil {
return nil, err
}
defer row.Close()

result, err := r.RowsToMap(row)
if err != nil {
return nil, err
}

i := model.ItemIsAuthorized{
IsAuthorized: result[0]["IsAuthorized"] == "1",
}

if result[0]["IsApproved"] != nil {
i.IsApproved = &model.NullBool{Value: result[0]["IsApproved"].(bool)}
} else {
i.IsApproved = nil
}

if result[0]["RequireRemarks"] != nil {
i.RequireRemarks = result[0]["RequireRemarks"].(bool)
}

return &i, nil
}

func (r *itemRepository) UpdateItemCallback(id string, isCallbackFailed bool) error {
row, err := r.Query("PR_Items_Update_Callback",
10 changes: 9 additions & 1 deletion src/goapp/repository/repository.go
Original file line number Diff line number Diff line change
@@ -3,14 +3,16 @@ package repository
import (
"main/infrastructure/database"
rAppModule "main/repository/app-module"
rApplication "main/repository/application"
rApprovalRequestApprover "main/repository/approval-request-approver"
rItem "main/repository/item"
)

type Repository struct {
Application rApplication.ApplicationRepository
ApplicationModule rAppModule.ApplicationModuleRepository
Item rItem.ItemRepository
ApprovalRequestApprover rApprovalRequestApprover.ApprovalRequestApproverRepository
Item rItem.ItemRepository
}

type RepositoryOptionFunc func(*Repository)
@@ -25,6 +27,12 @@ func NewRepository(opts ...RepositoryOptionFunc) *Repository {
return repo
}

func NewApplication(db *database.Database) RepositoryOptionFunc {
return func(r *Repository) {
r.Application = rApplication.NewApplicationRepository(db)
}
}

func NewApplicationModule(db *database.Database) RepositoryOptionFunc {
return func(r *Repository) {
r.ApplicationModule = rAppModule.NewApplicationModuleRepository(db)
11 changes: 5 additions & 6 deletions src/goapp/routes.go
Original file line number Diff line number Diff line change
@@ -5,14 +5,13 @@ import (
ev "main/pkg/envvar"
rtAzure "main/routes/login/azure"
rtPages "main/routes/pages"
rtApprovals "main/routes/pages/approvals"
)

func setPageRoutes() {
httpRouter.GET("/", m.Chain(rtApprovals.MyRequestsHandler, m.AzureAuth()))
httpRouter.GET("/myapprovals", m.Chain(rtApprovals.MyApprovalsHandler, m.AzureAuth()))
httpRouter.GET("/response/{appGuid}/{appModuleGuid}/{itemGuid}/{isApproved}", m.Chain(rtApprovals.ResponseHandler, m.AzureAuth()))
httpRouter.GET("/responsereassigned/{appGuid}/{appModuleGuid}/{itemGuid}/{isApproved}/{ApproveText}/{RejectText}", m.Chain(rtApprovals.ResponseReassignedeHandler, m.AzureAuth()))
httpRouter.GET("/", m.Chain(ctrl.ItemPage.MyRequests, m.AzureAuth()))
httpRouter.GET("/myapprovals", m.Chain(ctrl.ItemPage.MyApprovals, m.AzureAuth()))
httpRouter.GET("/response/{appGuid}/{appModuleGuid}/{itemGuid}/{isApproved}", m.Chain(ctrl.ItemPage.RespondToItem, m.AzureAuth()))
httpRouter.GET("/responsereassigned/{appGuid}/{appModuleGuid}/{itemGuid}/{isApproved}/{ApproveText}/{RejectText}", m.Chain(ctrl.ItemPage.ReassignApproval, m.AzureAuth()))

httpRouter.GET("/loginredirect", rtPages.LoginRedirectHandler)
httpRouter.GET("/login/azure", rtAzure.LoginHandler)
@@ -26,7 +25,7 @@ func setApiRoutes() {
httpRouter.POST("/api/process", ctrl.Item.ProcessResponse)
httpRouter.GET("/api/items/type/{type:[0-2]+}/status/{status:[0-3]+}", m.Chain(ctrl.Item.GetItems, m.AzureAuth()))
httpRouter.GET("/api/search/users/{search}", m.Chain(ctrl.User.SearchUserFromActiveDirectory, m.AzureAuth()))
httpRouter.GET("/api/responsereassignedapi/{itemGuid}/{approver}/{ApplicationId}/{ApplicationModuleId}/{ApproveText}/{RejectText}", m.Chain(rtApprovals.ReAssignApproverHandler, m.AzureAuth()))
httpRouter.GET("/api/responsereassignedapi/{itemGuid}/{approver}/{ApplicationId}/{ApplicationModuleId}/{ApproveText}/{RejectText}", m.Chain(ctrl.Item.ReassignItem, m.AzureAuth()))
}

func serve() {
199 changes: 0 additions & 199 deletions src/goapp/routes/pages/approvals/myapprovals.go

This file was deleted.

90 changes: 0 additions & 90 deletions src/goapp/routes/pages/approvals/myrequests.go

This file was deleted.

311 changes: 0 additions & 311 deletions src/goapp/routes/pages/approvals/response.go

This file was deleted.

10 changes: 8 additions & 2 deletions src/goapp/routes/pages/redirect.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package routes

import (
"net/http"
"html/template"
"net/http"
)

func LoginRedirectHandler(w http.ResponseWriter, r *http.Request) {
@@ -14,6 +14,12 @@ func LoginRedirectHandler(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{
"redirect": redirect,
}

c := http.Cookie{
Name: "auth-session",
MaxAge: -1}
http.SetCookie(w, &c)

tmpl := template.Must(template.ParseFiles("templates/loginredirect.html"))
tmpl.Execute(w, data)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package application

import (
"main/model"
)

type ApplicationService interface {
GetApplicationById(id string) (*model.Application, error)
}
24 changes: 24 additions & 0 deletions src/goapp/service/application/application-service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package application

import (
"main/model"
"main/repository"
)

type applicationService struct {
Repository *repository.Repository
}

func NewApplicationService(r *repository.Repository) ApplicationService {
return &applicationService{
Repository: r,
}
}

func (s *applicationService) GetApplicationById(id string) (*model.Application, error) {
application, err := s.Repository.Application.GetApplicationById(id)
if err != nil {
return nil, err
}
return application, nil
}
2 changes: 2 additions & 0 deletions src/goapp/service/item/item-service-interface.go
Original file line number Diff line number Diff line change
@@ -5,9 +5,11 @@ import (
)

type ItemService interface {
GetFailedCallbacks() ([]string, error)
GetItemById(id string) (*model.Item, error)
GetAll(itemOptions model.ItemOptions) (model.Response, error)
InsertItem(item model.ItemInsertRequest) (string, error)
ItemIsAuthorized(appId, appModuleId, itemId, approverEmail string) (*model.ItemIsAuthorized, error)
UpdateItemApproverEmail(itemId, approverEmail, username string) error
UpdateItemCallback(itemId string, isCallbackFailed bool) error
UpdateItemDateSent(itemId string) error
16 changes: 16 additions & 0 deletions src/goapp/service/item/item-service.go
Original file line number Diff line number Diff line change
@@ -21,6 +21,14 @@ func NewItemService(repo *repository.Repository, configManager config.ConfigMana
}
}

func (s *itemService) GetFailedCallbacks() ([]string, error) {
failedCallbacks, err := s.Repository.Item.GetFailedCallbacks()
if err != nil {
return []string{}, err
}
return failedCallbacks, nil
}

func (s *itemService) GetItemById(id string) (*model.Item, error) {
item, err := s.Repository.Item.GetItemById(id)
if err != nil {
@@ -83,6 +91,14 @@ func (s *itemService) InsertItem(item model.ItemInsertRequest) (string, error) {
return id, nil
}

func (s *itemService) ItemIsAuthorized(appId, appModuleId, itemId, approverEmail string) (*model.ItemIsAuthorized, error) {
itemIsAuthorized, err := s.Repository.Item.ItemIsAuthorized(appId, appModuleId, itemId, approverEmail)
if err != nil {
return nil, err
}
return itemIsAuthorized, nil
}

func (s *itemService) UpdateItemApproverEmail(itemId, approverEmail, username string) error {
err := s.Repository.Item.UpdateItemApproverEmail(itemId, approverEmail, username)
if err != nil {
16 changes: 16 additions & 0 deletions src/goapp/service/service.go
Original file line number Diff line number Diff line change
@@ -4,18 +4,22 @@ import (
"main/config"
"main/repository"
sApplicationModule "main/service/app-module"
sApplication "main/service/application"
sApprovalRequestApprover "main/service/approval-request-approver"
sEmail "main/service/email"
sItem "main/service/item"
sMsGraph "main/service/msgraph"
sTemplate "main/service/template"
)

type Service struct {
Application sApplication.ApplicationService
ApplicationModule sApplicationModule.ApplicationModuleService
Item sItem.ItemService
Email sEmail.EmailService
ApprovalRequestApprover sApprovalRequestApprover.ApprovalRequestApproverService
MsGraph sMsGraph.MsGraphService
Template sTemplate.TemplateService
}

type ServiceOptionFunc func(*Service)
@@ -30,6 +34,12 @@ func NewService(opts ...ServiceOptionFunc) *Service {
return service
}

func NewApplicationService(repo *repository.Repository) ServiceOptionFunc {
return func(s *Service) {
s.Application = sApplication.NewApplicationService(repo)
}
}

func NewApplicationModuleService(repo *repository.Repository) ServiceOptionFunc {
return func(s *Service) {
s.ApplicationModule = sApplicationModule.NewApplicationModuleService(repo)
@@ -59,3 +69,9 @@ func NewMsGraphService(conf config.ConfigManager) ServiceOptionFunc {
s.MsGraph = sMsGraph.NewMsGraphService(conf)
}
}

func NewTemplateService(conf config.ConfigManager) ServiceOptionFunc {
return func(s *Service) {
s.Template = sTemplate.NewTemplateService(conf)
}
}
10 changes: 10 additions & 0 deletions src/goapp/service/template/template-service-interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package template

import (
"main/model"
"text/template"
)

type TemplateService interface {
UseTemplate(page, path string, user model.AzureUser, pageData interface{}) (*template.Template, *model.MasterPageData)
}
61 changes: 61 additions & 0 deletions src/goapp/service/template/template-service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package template

import (
"fmt"
"main/config"
"main/model"
"strings"
"text/template"
)

type templateService struct {
LinkFooters string
OrganizationName string
}

func NewTemplateService(config config.ConfigManager) TemplateService {
return &templateService{
LinkFooters: config.GetLinkFooters(),
OrganizationName: config.GetOrganizationName(),
}
}

func (t *templateService) UseTemplate(page, path string, user model.AzureUser, pageData interface{}) (*template.Template, *model.MasterPageData) {
// Data on master page
var menu []model.Menu
menu = append(menu, model.Menu{Name: "My Requests", Url: "/", IconPath: "/public/icons/projects.svg"})
menu = append(menu, model.Menu{Name: "My Approvals", Url: "/myapprovals", IconPath: "/public/icons/approvals.svg"})
masterPageData := model.Headers{Menu: menu, Page: getUrlPath(path)}

//Footers
var footers []model.Footer
footerString := t.LinkFooters
res := strings.Split(footerString, ";")
for _, footer := range res {
f := strings.Split(footer, ">")
footers = append(footers, model.Footer{Text: f[0], Url: f[1]})
}

data := model.MasterPageData{
Header: masterPageData,
Profile: user,
Content: pageData,
Footers: footers,
OrganizationName: t.OrganizationName,
}

tmpl := template.Must(
template.ParseFiles("templates/master.html", "templates/buttons.html",
fmt.Sprintf("templates/%v.html", page)))

return tmpl, &data
}

func getUrlPath(path string) string {
p := strings.Split(path, "/")
if p[1] == "" {
return "/"
} else {
return fmt.Sprintf("/%s", p[1])
}
}
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
<main class="sm:flex">
<div style="text-align: center;">
<h1 class="text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl">Processed</h1>
<p class="mt-1 text-base text-gray-500">You have already {{.response}} this item.</p>
<p class="mt-1 text-base text-gray-500">You have already {{.Response}} this item.</p>
</div>
</main>
</div>
6 changes: 3 additions & 3 deletions src/goapp/templates/master.html
Original file line number Diff line number Diff line change
@@ -158,7 +158,7 @@
<p
class="text-sm font-medium text-gray-700 group-hover:text-gray-900"
>
{{ .Profile.name }}
{{ .Profile.Name }}
</p>
<p
class="text-xs font-medium text-gray-500 group-hover:text-gray-700"
@@ -254,8 +254,8 @@
function masterData(){
return {
profile:{
name:'{{.Profile.name}}',
username:'{{.Profile.preferred_username}}'
name:'{{.Profile.Name}}',
username:'{{.Profile.Email}}'
},
get profileDomain() {
var domain = this.profile.username.split('@')[1]
2 changes: 1 addition & 1 deletion src/goapp/templates/myapprovals.html
Original file line number Diff line number Diff line change
@@ -157,7 +157,7 @@
onChangeTabs(tab){
this.activeTab = this.tabs[tab];
},
exportMyApprovals(username, exportUrls){
exportMyApprovals(username, exportUrl){
window.open(exportUrl.replace('{username}', username), '_blank')
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{{ define "content" }}
<div x-data="data()" x-init="onLoad()">
<h1 class="text-4xl">{{.Data.Subject}}</h1>
<h1 class="text-2xl mb-5">Reassign Approval Request</h1>
<span x-html="body"></span>

<form onsubmit="event.preventDefault()" id="form">
@@ -103,21 +103,17 @@ <h3 class="text-lg leading-6 font-medium text-gray-900" x-text="modalText"></h3>
}
function data(){
return {
body: "{{.Data.Body}}",
body: `{{.Data.Body}}`,
form: {
applicationId: "{{.ApplicationId}}",
applicationModuleId: "{{.ApplicationModuleId}}",
itemId: "{{.ItemId}}",
approverEmail: "{{.ApproverEmail}}",
ApproveText: "{{.ApproveText}}",
RejectText: "{{.RejectText}}",
remarks: "",
isApproved: "{{.IsApproved}}",
name : '',
approver : [],
status : null,
},
requireRemarks: "{{.RequireRemarks}}",
showModal: false,
status: "",
modalText: "Please wait...",
376 changes: 256 additions & 120 deletions src/goapp/templates/response.html

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions src/goapp/timed-jobs/timed-jobs-interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package timedjobs

type TimedJobs interface {
ReprocessFailedCallbacks()
}
83 changes: 83 additions & 0 deletions src/goapp/timed-jobs/timed-jobs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package timedjobs

import (
"bytes"
"encoding/json"
"fmt"
"main/config"
"main/model"
"main/service"
"net/http"
"strconv"
"time"
)

type timedJobs struct {
Service *service.Service
configManager config.ConfigManager
}

func NewTimedJobs(s *service.Service, configManager config.ConfigManager) TimedJobs {
return &timedJobs{
Service: s,
configManager: configManager,
}
}

func (t *timedJobs) ReprocessFailedCallbacks() {
freq := t.configManager.GetCallbackRetryFreq()
freqInt, _ := strconv.ParseInt(freq, 0, 64)
if freqInt > 0 {
for range time.NewTicker(time.Duration(freqInt) * time.Minute).C {
f, err := t.Service.Item.GetFailedCallbacks()
if err != nil {
fmt.Printf("Failed to get failed callbacks: %v", err.Error())
return
}

for _, id := range f {
go t.postCallback(id)
}
}
}
}

func (t *timedJobs) postCallback(id string) {
item, err := t.Service.Item.GetItemById(id)
if err != nil {
fmt.Println("Error getting item by id: ", id)
return
}

if item.CallbackUrl == "" {
fmt.Println("No callback url found")
return
} else {
params := model.ResponseCallback{
ItemId: id,
IsApproved: item.IsApproved,
Remarks: item.ApproverRemarks,
ResponseDate: item.DateResponded,
RespondedBy: item.RespondedBy,
}

jsonReq, err := json.Marshal(params)
if err != nil {
return
}

res, err := http.Post(item.CallbackUrl, "application/json", bytes.NewBuffer(jsonReq))
if err != nil {
fmt.Println("Error posting callback: ", err)
return
}

isCallbackFailed := res.StatusCode != 200

err = t.Service.Item.UpdateItemCallback(id, isCallbackFailed)
if err != nil {
fmt.Println("Error updating item callback: ", err)
return
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
CREATE PROCEDURE [dbo].[PR_RESPONSE_IsAuthorized]
CREATE PROCEDURE [dbo].[PR_Items_IsAuthorized]

@ApplicationId UNIQUEIDENTIFIER,
@ApplicationModuleId UNIQUEIDENTIFIER,

0 comments on commit 188f833

Please sign in to comment.