diff --git a/Dockerfile b/Dockerfile index 3561b5e..8644ff4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,35 +1,35 @@ -# syntax=docker/dockerfile:1 - -FROM golang:1.22.2 - -WORKDIR /app - -COPY go.mod go.sum ./ - -RUN go mod download - -COPY auth ./auth -COPY config ./config -COPY db ./db -COPY docs ./docs -COPY handlers ./handlers -COPY models ./models -COPY services ./services -COPY setup ./setup -COPY utils ./utils -COPY webhooks ./webhooks -COPY data ./data -COPY web-ui ./web-ui -COPY config.yaml key.pem cert.pem main.go rsakey.pem build.sh run.sh ./ - -RUN CGO_ENABLED=1 GOOS=linux go build -o dingus-server - -# Optional: -# To bind to a TCP port, runtime parameters must be supplied to the docker command. -# But we can document in the Dockerfile what ports -# the application is going to listen on by default. -# https://docs.docker.com/reference/dockerfile/#expose -EXPOSE 443 - -# Run -CMD ["/app/run.sh"] \ No newline at end of file +# syntax=docker/dockerfile:1 + +FROM golang:1.22.2 + +WORKDIR /app + +COPY go.mod go.sum ./ + +RUN go mod download + +COPY auth ./auth +COPY config ./config +COPY db ./db +COPY docs ./docs +COPY handlers ./handlers +COPY models ./models +COPY services ./services +COPY setup ./setup +COPY utils ./utils +COPY webhooks ./webhooks +COPY data ./data +COPY web-ui ./web-ui +COPY config.yaml key.pem cert.pem main.go rsakey.pem build.sh run.sh ./ + +RUN CGO_ENABLED=1 GOOS=linux go build -o dingus-server + +# Optional: +# To bind to a TCP port, runtime parameters must be supplied to the docker command. +# But we can document in the Dockerfile what ports +# the application is going to listen on by default. +# https://docs.docker.com/reference/dockerfile/#expose +EXPOSE 443 + +# Run +CMD ["/app/run.sh"] diff --git a/build.sh b/build.sh index dc70b99..93f6d93 100644 --- a/build.sh +++ b/build.sh @@ -1,10 +1,10 @@ #!/bin/sh -export WILD_APRICOT_API_KEY=ilyglbbwzai5suuxt4nai0kqx5evvh +export WILD_APRICOT_API_KEY=v97hk30e4t8qm1nrwb6exsfibiisvy export WILD_APRICOT_WEBHOOK_TOKEN=08a7gv08abwDYGd77cxv980asdfy98zxc87 -export WILD_APRICOT_SSO_CLIENT_ID=t8jw60pj3f -export WILD_APRICOT_SSO_CLIENT_SECRET=5k8nct39ootl8wt0gkpxe22v8phi5a -export WILD_APRICOT_SSO_REDIRECT_URI=/replaceme +export WILD_APRICOT_SSO_CLIENT_ID=654628fgDFc68hH +export WILD_APRICOT_SSO_CLIENT_SECRET=07pb835b7pw3s8r0qrvmtlipuwjtt6 +export WILD_APRICOT_SSO_REDIRECT_URI=https://dingus.hackpgh.org/web-ui/home export LOG_LEVEL=INFO export COOKIE_STORE_SECRET=cookiestoresecret export CGO_ENABLED=1 diff --git a/compose.yaml b/compose.yaml index efdce01..dd75259 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,5 +1,5 @@ -services: - dingus-server: - build: . - ports: +services: + dingus-server: + build: . + ports: - "443:443" \ No newline at end of file diff --git a/config/config.go b/config/config.go index ffd4265..6622116 100644 --- a/config/config.go +++ b/config/config.go @@ -31,6 +31,7 @@ type Config struct { CookieStoreSecret string `mapstructure:"cookie_store_secret" json:"cookie_store_secret"` WildApricotApiKey string WildApricotWebhookToken string + RFIDFieldName string log *logrus.Logger } diff --git a/run.sh b/run.sh index 10f461b..d30ff8f 100644 --- a/run.sh +++ b/run.sh @@ -1,10 +1,11 @@ #!/bin/sh -export WILD_APRICOT_API_KEY=ilyglbbwzai5suuxt4nai0kqx5evvh +export WILD_APRICOT_API_KEY=v97hk30e4t8qm1nrwb6exsfibiisvy export WILD_APRICOT_WEBHOOK_TOKEN=08a7gv08abwDYGd77cxv980asdfy98zxc87 -export WILD_APRICOT_SSO_CLIENT_ID=t8jw60pj3f -export WILD_APRICOT_SSO_CLIENT_SECRET=5k8nct39ootl8wt0gkpxe22v8phi5a -export WILD_APRICOT_SSO_REDIRECT_URI=/replaceme +export WILD_APRICOT_SSO_CLIENT_ID=654628fgDFc68hH +export WILD_APRICOT_SSO_CLIENT_SECRET=07pb835b7pw3s8r0qrvmtlipuwjtt6 +export WILD_APRICOT_SSO_REDIRECT_URI=https://dingus.hackpgh.org/web-ui/home +#export WILD_APRICOT_SSO_REDIRECT_URI=https://localhost/web-ui/home export LOG_LEVEL=INFO export COOKIE_STORE_SECRET=cookiestoresecret export CGO_ENABLED=1 diff --git a/services/dbService_test.go b/services/dbService_test.go index 977757a..5cb480c 100644 --- a/services/dbService_test.go +++ b/services/dbService_test.go @@ -1,214 +1,214 @@ -package services - -import ( - "database/sql" - "rfid-backend/config" - "testing" - - _ "github.com/mattn/go-sqlite3" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func mockConfig() *config.Config { - return &config.Config{ - CertFile: "path/to/test/cert.pem", - KeyFile: "path/to/test/key.pem", - DatabasePath: "path/to/test/database.db", - RFIDFieldName: "RFID", - TrainingFieldName: "Training", - WildApricotAccountId: 12345, - ContactFilterQuery: "status eq Active or status eq 'Pending - Renewal'", - } -} - -func setupTestDB(t *testing.T) *sql.DB { - // Create an in-memory SQLite database - db, err := sql.Open("sqlite3", ":memory:") - require.NoError(t, err) - - // Create Members table - _, err = db.Exec(` - CREATE TABLE IF NOT EXISTS members ( - tag_id INTEGER PRIMARY KEY, - membership_level INTEGER NOT NULL - );`) - require.NoError(t, err) - - // Create SafetyTrainings table - _, err = db.Exec(` - CREATE TABLE IF NOT EXISTS trainings ( - label TEXT PRIMARY KEY - );`) - require.NoError(t, err) - - // Create SafetyTrainingMembersLink table - _, err = db.Exec(` - CREATE TABLE IF NOT EXISTS members_trainings_link ( - tag_id INTEGER NOT NULL, - label TEXT NOT NULL, - FOREIGN KEY (tag_id) REFERENCES members(tag_id), - FOREIGN KEY (label) REFERENCES trainings(label), - UNIQUE (tag_id, label) - );`) - require.NoError(t, err) - - return db -} - -func TestGetAllRFIDs(t *testing.T) { - cfg := mockConfig() - - db := setupTestDB(t) - defer db.Close() - - // Insert test data into members table - _, err := db.Exec("INSERT INTO members (tag_id, membership_level) VALUES (11111, 1), (22222, 1)") - require.NoError(t, err) - - dbService := NewDBService(db, cfg) - - // Execute the test function - rfids, err := dbService.GetAllRFIDs() - - // Assertions - assert.NoError(t, err) - assert.Len(t, rfids, 2) - assert.Equal(t, uint32(11111), rfids[0]) - assert.Equal(t, uint32(22222), rfids[1]) -} - -func TestGetRFIDsForMachine(t *testing.T) { - cfg := mockConfig() - - db := setupTestDB(t) - defer db.Close() - - // Insert test data into members table - _, err := db.Exec("INSERT INTO members (tag_id, membership_level) VALUES (12345, 1), (67890, 1)") - require.NoError(t, err) - - // Insert test data into members_trainings_link table - _, err = db.Exec("INSERT INTO members_trainings_link (tag_id, label) VALUES (12345, 'MachineA'), (67890, 'MachineA')") - require.NoError(t, err) - - dbService := NewDBService(db, cfg) - - tags, err := dbService.GetRFIDsForMachine("MachineA") - assert.NoError(t, err) - assert.Len(t, tags, 2) - assert.Equal(t, uint32(12345), tags[0]) - assert.Equal(t, uint32(67890), tags[1]) -} - -func TestInsertOrUpdateMembers(t *testing.T) { - cfg := mockConfig() - - db := setupTestDB(t) - - // Start a transaction - tx, err := db.Begin() - require.NoError(t, err) - - dbService := NewDBService(db, cfg) - - allRFIDs := []uint32{1234, 5678} - err = dbService.insertOrUpdateMembers(tx, allRFIDs) - assert.NoError(t, err) - - // Commit the transaction - err = tx.Commit() - assert.NoError(t, err) - // Verify the data - var count int - err = db.QueryRow("SELECT COUNT(*) FROM members").Scan(&count) - assert.NoError(t, err) - assert.Equal(t, 2, count) -} - -func TestInsertTrainings(t *testing.T) { - cfg := mockConfig() - - db := setupTestDB(t) - - tx, err := db.Begin() - require.NoError(t, err) - - dbService := NewDBService(db, cfg) - - trainingMap := map[string][]uint32{ - "Metal Lathe": {1234}, - "CNC": {5678}, - } - err = dbService.insertTrainings(tx, trainingMap) - assert.NoError(t, err) - - err = tx.Commit() - assert.NoError(t, err) - - // Verify the data - var count int - err = db.QueryRow("SELECT COUNT(*) FROM trainings").Scan(&count) - assert.NoError(t, err) - assert.Equal(t, len(trainingMap), count) -} - -func TestManageMemberTrainingLinks(t *testing.T) { - cfg := mockConfig() - - db := setupTestDB(t) - - // Start a transaction - tx, err := db.Begin() - require.NoError(t, err) - - dbService := NewDBService(db, cfg) - - trainingMap := map[string][]uint32{ - "Metal Lathe": {1234}, - "CNC": {5678}, - } - err = dbService.manageMemberTrainingLinks(tx, trainingMap) - assert.NoError(t, err) - - err = tx.Commit() - assert.NoError(t, err) - - // Verify the data - var count int - err = db.QueryRow("SELECT COUNT(*) FROM members_trainings_link").Scan(&count) - assert.NoError(t, err) - assert.Equal(t, 2, count) // Assuming each training has one RFID -} - -func TestDeleteInactiveMembers(t *testing.T) { - cfg := mockConfig() - - db := setupTestDB(t) - defer db.Close() - - // Insert 2 test members into members table - _, err := db.Exec("INSERT INTO members (tag_id, membership_level) VALUES (1234, 1), (67890, 1)") - require.NoError(t, err) - - // Start a transaction - tx, err := db.Begin() - require.NoError(t, err) - - dbService := NewDBService(db, cfg) - - allRFIDs := []uint32{1234} // only 1 active member, remove the inactive record - err = dbService.deleteInactiveMembers(tx, allRFIDs) - assert.NoError(t, err) - - // Commit the transaction - err = tx.Commit() - assert.NoError(t, err) - - // Verify that only the active members remain - var count int - err = db.QueryRow("SELECT COUNT(*) FROM members").Scan(&count) - assert.NoError(t, err) - assert.Equal(t, 1, count) // Expect only 1 active member in the table -} +package services + +import ( + "database/sql" + "rfid-backend/config" + "testing" + + _ "github.com/mattn/go-sqlite3" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mockConfig() *config.Config { + return &config.Config{ + CertFile: "path/to/test/cert.pem", + KeyFile: "path/to/test/key.pem", + DatabasePath: "path/to/test/database.db", + RFIDFieldName: "RFID", + TrainingFieldName: "Training", + WildApricotAccountId: 12345, + ContactFilterQuery: "status eq Active or status eq 'Pending - Renewal'", + } +} + +func setupTestDB(t *testing.T) *sql.DB { + // Create an in-memory SQLite database + db, err := sql.Open("sqlite3", ":memory:") + require.NoError(t, err) + + // Create Members table + _, err = db.Exec(` + CREATE TABLE IF NOT EXISTS members ( + tag_id INTEGER PRIMARY KEY, + membership_level INTEGER NOT NULL + );`) + require.NoError(t, err) + + // Create SafetyTrainings table + _, err = db.Exec(` + CREATE TABLE IF NOT EXISTS trainings ( + label TEXT PRIMARY KEY + );`) + require.NoError(t, err) + + // Create SafetyTrainingMembersLink table + _, err = db.Exec(` + CREATE TABLE IF NOT EXISTS members_trainings_link ( + tag_id INTEGER NOT NULL, + label TEXT NOT NULL, + FOREIGN KEY (tag_id) REFERENCES members(tag_id), + FOREIGN KEY (label) REFERENCES trainings(label), + UNIQUE (tag_id, label) + );`) + require.NoError(t, err) + + return db +} + +func TestGetAllRFIDs(t *testing.T) { + cfg := mockConfig() + + db := setupTestDB(t) + defer db.Close() + + // Insert test data into members table + _, err := db.Exec("INSERT INTO members (tag_id, membership_level) VALUES (11111, 1), (22222, 1)") + require.NoError(t, err) + + dbService := NewDBService(db, cfg) + + // Execute the test function + rfids, err := dbService.GetAllRFIDs() + + // Assertions + assert.NoError(t, err) + assert.Len(t, rfids, 2) + assert.Equal(t, uint32(11111), rfids[0]) + assert.Equal(t, uint32(22222), rfids[1]) +} + +func TestGetRFIDsForMachine(t *testing.T) { + cfg := mockConfig() + + db := setupTestDB(t) + defer db.Close() + + // Insert test data into members table + _, err := db.Exec("INSERT INTO members (tag_id, membership_level) VALUES (12345, 1), (67890, 1)") + require.NoError(t, err) + + // Insert test data into members_trainings_link table + _, err = db.Exec("INSERT INTO members_trainings_link (tag_id, label) VALUES (12345, 'MachineA'), (67890, 'MachineA')") + require.NoError(t, err) + + dbService := NewDBService(db, cfg) + + tags, err := dbService.GetRFIDsForMachine("MachineA") + assert.NoError(t, err) + assert.Len(t, tags, 2) + assert.Equal(t, uint32(12345), tags[0]) + assert.Equal(t, uint32(67890), tags[1]) +} + +func TestInsertOrUpdateMembers(t *testing.T) { + cfg := mockConfig() + + db := setupTestDB(t) + + // Start a transaction + tx, err := db.Begin() + require.NoError(t, err) + + dbService := NewDBService(db, cfg) + + allRFIDs := []uint32{1234, 5678} + err = dbService.insertOrUpdateMembers(tx, allRFIDs) + assert.NoError(t, err) + + // Commit the transaction + err = tx.Commit() + assert.NoError(t, err) + // Verify the data + var count int + err = db.QueryRow("SELECT COUNT(*) FROM members").Scan(&count) + assert.NoError(t, err) + assert.Equal(t, 2, count) +} + +func TestInsertTrainings(t *testing.T) { + cfg := mockConfig() + + db := setupTestDB(t) + + tx, err := db.Begin() + require.NoError(t, err) + + dbService := NewDBService(db, cfg) + + trainingMap := map[string][]uint32{ + "Metal Lathe": {1234}, + "CNC": {5678}, + } + err = dbService.insertTrainings(tx, trainingMap) + assert.NoError(t, err) + + err = tx.Commit() + assert.NoError(t, err) + + // Verify the data + var count int + err = db.QueryRow("SELECT COUNT(*) FROM trainings").Scan(&count) + assert.NoError(t, err) + assert.Equal(t, len(trainingMap), count) +} + +func TestManageMemberTrainingLinks(t *testing.T) { + cfg := mockConfig() + + db := setupTestDB(t) + + // Start a transaction + tx, err := db.Begin() + require.NoError(t, err) + + dbService := NewDBService(db, cfg) + + trainingMap := map[string][]uint32{ + "Metal Lathe": {1234}, + "CNC": {5678}, + } + err = dbService.manageMemberTrainingLinks(tx, trainingMap) + assert.NoError(t, err) + + err = tx.Commit() + assert.NoError(t, err) + + // Verify the data + var count int + err = db.QueryRow("SELECT COUNT(*) FROM members_trainings_link").Scan(&count) + assert.NoError(t, err) + assert.Equal(t, 2, count) // Assuming each training has one RFID +} + +func TestDeleteInactiveMembers(t *testing.T) { + cfg := mockConfig() + + db := setupTestDB(t) + defer db.Close() + + // Insert 2 test members into members table + _, err := db.Exec("INSERT INTO members (tag_id, membership_level) VALUES (1234, 1), (67890, 1)") + require.NoError(t, err) + + // Start a transaction + tx, err := db.Begin() + require.NoError(t, err) + + dbService := NewDBService(db, cfg) + + allRFIDs := []uint32{1234} // only 1 active member, remove the inactive record + err = dbService.deleteInactiveMembers(tx, allRFIDs) + assert.NoError(t, err) + + // Commit the transaction + err = tx.Commit() + assert.NoError(t, err) + + // Verify that only the active members remain + var count int + err = db.QueryRow("SELECT COUNT(*) FROM members").Scan(&count) + assert.NoError(t, err) + assert.Equal(t, 1, count) // Expect only 1 active member in the table +} diff --git a/services/wildApricotService.go b/services/wildApricotService.go index fa303d7..2cd6239 100644 --- a/services/wildApricotService.go +++ b/services/wildApricotService.go @@ -27,6 +27,7 @@ type WildApricotService struct { } var wildApricotSvc = utils.NewSingleton(&WildApricotService{}) +var pageSize = 100 func NewWildApricotService(cfg *config.Config, logger *logrus.Logger) *WildApricotService { return wildApricotSvc.Get(func() interface{} { @@ -147,29 +148,42 @@ func (s *WildApricotService) makeHTTPRequest(method, url string, body io.Reader) } func (s *WildApricotService) GetContacts() ([]models.Contact, error) { - contactURL := s.buildURL("/%d/Contacts?$async=false&$filter=%s", - s.cfg.WildApricotAccountId, - url.QueryEscape(s.cfg.ContactFilterQuery)) - - resp, err := s.makeHTTPRequest("GET", contactURL, nil) - if err != nil { - s.logError("creating request for contacts", err) - return nil, err - } + skip := 0 + allContacts := []models.Contact{} + for { + contactURL := s.buildURL("/%d/Contacts?$async=false&$filter=%s&$top=%d&$skip=%d", + s.cfg.WildApricotAccountId, + url.QueryEscape(s.cfg.ContactFilterQuery), + pageSize, + skip) + + // Increment skip by page size. + skip += pageSize + + resp, err := s.makeHTTPRequest("GET", contactURL, nil) + if err != nil { + s.logError("creating request for contacts", err) + return nil, err + } - if err := handleHTTPError(resp); err != nil { - s.logError("handling HTTP error for contacts", err) - return nil, err - } + if err := handleHTTPError(resp); err != nil { + s.logError("handling HTTP error for contacts", err) + return nil, err + } - contacts, err := s.parseHTTPResponse(resp) - if err != nil { - s.logError("parsing HTTP response", err) - } + contacts, err := s.parseHTTPResponse(resp) + if err != nil { + s.logError("parsing HTTP response", err) + } - s.log.Infof("Parsed %d contacts from response", len(contacts)) + allContacts = append(allContacts, contacts...) - return contacts, nil + if len(contacts) < pageSize { + break + } + } + s.log.Infof("All Contacts Length %d", len(allContacts)) + return allContacts, nil } func (s *WildApricotService) GetContact(contactId int) (*models.Contact, error) { diff --git a/setup/setupRoutes.go b/setup/setupRoutes.go index dcb7b7a..4386624 100644 --- a/setup/setupRoutes.go +++ b/setup/setupRoutes.go @@ -31,7 +31,7 @@ func SetupRoutes(router *gin.Engine, cfg *config.Config, db *sql.DB, logger *log RedirectURL: cfg.SSORedirectURI, Scopes: []string{"contacts_me"}, Endpoint: oauth2.Endpoint{ - AuthURL: "https://davidbouw36728.wildapricot.org/sys/login/OAuthLogin", + AuthURL: "https://members.hackpgh.org/sys/login/OAuthLogin", TokenURL: "https://oauth.wildapricot.org/auth/token", }, }