diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 0e27423..f860e69 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -10,30 +10,42 @@ on: branches: [ "develop" ] jobs: - - build: + run: + name: Build runs-on: ubuntu-latest + timeout-minutes: 5 + strategy: + fail-fast: true + matrix: + go: ['stable', 'oldstable'] + steps: - - uses: actions/checkout@v4 + - name: Check out code + uses: actions/checkout@v3 + + - name: Install Go + uses: actions/setup-go@v4 + with: + go-version: ${{ matrix.go }} + check-latest: true + + - name: Go Format + run: gofmt -s -w . && git diff --exit-code + + - name: Go Vet + run: go vet ./... - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: '1.21.5' + - name: Go Tidy + run: go mod tidy && git diff --exit-code - - name: Cache Go modules - uses: actions/cache@v2 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- + - name: Go Mod + run: go mod download - - name: Install dependencies - run: go mod download + - name: Go Mod Verify + run: go mod verify - - name: Build - run: go build -v ./... + - name: Go Generate + run: go generate ./... && git diff --exit-code - - name: Test - run: go test -v ./... + - name: Go Build + run: go build -o /dev/null ./... diff --git a/README.md b/README.md index 69143d4..a378b24 100644 --- a/README.md +++ b/README.md @@ -14,17 +14,7 @@ Phase 2: Phase 3: * Interactable UI * Have a UI in which you can interact with the transactions - give them additional metadata such as tags, photos (receipts/invoices) - * Mutate the graphs - apply filters to change what the graphs output - - -## Notes - -* To generate the open-api interface run the following command: -``` -sudo docker run --rm -v ${PWD}:/local openapitools/openapi-generator-cli generate -i /local/openapi.json -g go -o /local/expense-manager/up-bank-interface -``` - - + * Mutate the graphs - apply filters to change what the graphs output ## Modules @@ -67,6 +57,10 @@ LICENSE ``` This will stop the generator from generating those files (which will mess up the Go compilation) +4. Generate the TEMPL components +`templ generate` + + * Note if you're finding it hard to run `templ`, it might be because `$GOPATH` is not on your `$PATH` so your computer can't find the executable. In that case either update your path or run `./$GOPATH/templ` 4. Format code `gofmt -s -w .` diff --git a/datafetcher/handlers/accounts.go b/datafetcher/handlers/accounts.go new file mode 100644 index 0000000..5facf25 --- /dev/null +++ b/datafetcher/handlers/accounts.go @@ -0,0 +1,42 @@ +package handlers + +import ( + "context" + "fmt" + "log" + "net/http" + + "github.com/esteanes/expense-manager/datafetcher/upclient" +) + +type AccountHandler struct { + *BaseHandler +} + +func NewAccountHandler(log *log.Logger, upclient *upclient.APIClient, auth context.Context) *AccountHandler { + handler := &AccountHandler{} + handler.BaseHandler = &BaseHandler{ + Uri: "/accounts", + Log: log, + UpClient: upclient, + UpAuth: auth, + Handler: handler, // Set the Handler interface to the specific handler + } + return handler +} + +func (h *AccountHandler) Post(w http.ResponseWriter, r *http.Request) {} +func (h *AccountHandler) Get(w http.ResponseWriter, r *http.Request) { + pageSize := int32(30) + filterAccountType := upclient.AccountTypeEnum("SAVER") + filterOwnershipType := upclient.OwnershipTypeEnum("INDIVIDUAL") + resp, r2, err := h.UpClient.AccountsAPI.AccountsGet(h.UpAuth).PageSize(pageSize).FilterAccountType(filterAccountType).FilterOwnershipType(filterOwnershipType).Execute() + + if err != nil { + fmt.Fprintf(w, "Error when calling `AccountsAPI.AccountsGet``: %v\n", err) + fmt.Fprintf(w, "Full HTTP response: %v\n", r2) + h.Log.Println("Unable to get account information") + } + + fmt.Fprintf(w, "Response from `AccountsAPI.AccountsGet`: %v\n", resp) +} diff --git a/datafetcher/handlers/basehandler.go b/datafetcher/handlers/basehandler.go new file mode 100644 index 0000000..70c2a52 --- /dev/null +++ b/datafetcher/handlers/basehandler.go @@ -0,0 +1,33 @@ +package handlers + +import ( + "context" + "log" + "net/http" + + "github.com/esteanes/expense-manager/datafetcher/upclient" +) + +type Handler interface { + Post(w http.ResponseWriter, r *http.Request) + Get(w http.ResponseWriter, r *http.Request) +} + +type BaseHandler struct { + Uri string + Log *log.Logger + UpClient *upclient.APIClient + UpAuth context.Context + Handler Handler // Embed the Handler interface +} + +func (h *BaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + h.Handler.Post(w, r) + case http.MethodGet: + h.Handler.Get(w, r) + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} diff --git a/datafetcher/handlers/transactions.go b/datafetcher/handlers/transactions.go new file mode 100644 index 0000000..a48ae86 --- /dev/null +++ b/datafetcher/handlers/transactions.go @@ -0,0 +1,39 @@ +package handlers + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + + "github.com/esteanes/expense-manager/datafetcher/upclient" +) + +type TransactionsHandler struct { + *BaseHandler +} + +func NewTransactionHandler(log *log.Logger, upclient *upclient.APIClient, auth context.Context) *AccountHandler { + handler := &AccountHandler{} + handler.BaseHandler = &BaseHandler{ + Uri: "/transactions", + Log: log, + UpClient: upclient, + UpAuth: auth, + Handler: handler, // Set the Handler interface to the specific handler + } + return handler +} + +func (h *TransactionsHandler) Post(w http.ResponseWriter, r *http.Request) {} +func (h *TransactionsHandler) Get(w http.ResponseWriter, r *http.Request) { + pageSize := int32(30) // int32 | The number of records to return in each page. (optional) + resp2, r2, err := h.UpClient.TransactionsAPI.TransactionsGet(h.UpAuth).PageSize(pageSize).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `TransactionsAPI.TransactionsGet``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r2.Body) + } + // response from `TransactionsGet`: ListTransactionsResponse + fmt.Fprintf(os.Stdout, "Response from `TransactionsAPI.TransactionsGet`: %v\n", resp2) +} diff --git a/datafetcher/server.go b/datafetcher/server.go index caba004..177fdcd 100644 --- a/datafetcher/server.go +++ b/datafetcher/server.go @@ -5,9 +5,14 @@ import ( "fmt" "log" "net/http" - "os" + "time" + "github.com/a-h/templ" + "github.com/esteanes/expense-manager/datafetcher/handlers" + templates "github.com/esteanes/expense-manager/datafetcher/templ" "github.com/esteanes/expense-manager/datafetcher/upclient" + + "github.com/alexedwards/scs/v2" ) // homePage function to handle requests to the root URL @@ -17,36 +22,84 @@ func homePage(w http.ResponseWriter, r *http.Request) { } func getInfo(w http.ResponseWriter, r *http.Request) { - pageSize := int32(30) // int32 | The number of records to return in each page. (optional) - filterAccountType := upclient.AccountTypeEnum("SAVER") // AccountTypeEnum | The type of account for which to return records. This can be used to filter Savers from spending accounts. (optional) - filterOwnershipType := upclient.OwnershipTypeEnum("INDIVIDUAL") // OwnershipTypeEnum | The account ownership structure for which to return records. This can be used to filter 2Up accounts from Up accounts. (optional) - auth := context.WithValue(context.Background(), upclient.ContextAccessToken, os.Getenv("up-bank-bearer-token")) - configuration := upclient.NewConfiguration() - apiClient := upclient.NewAPIClient(configuration) - resp, r2, err := apiClient.AccountsAPI.AccountsGet(auth).PageSize(pageSize).FilterAccountType(filterAccountType).FilterOwnershipType(filterOwnershipType).Execute() - if err != nil { - fmt.Fprintf(os.Stderr, "Error when calling `AccountsAPI.AccountsGet``: %v\n", err) - fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r2) - } - // response from `AccountsGet`: ListAccountsResponse - fmt.Fprintf(os.Stdout, "Response from `AccountsAPI.AccountsGet`: %v\n", resp) +} + +func NewNowHandler(now func() time.Time) NowHandler { + return NowHandler{Now: now} +} + +type NowHandler struct { + Now func() time.Time +} + +func (nh NowHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + templates.TimeComponent(nh.Now()).Render(r.Context(), w) +} + +type GlobalState struct { + Count int +} - resp2, r2, err := apiClient.TransactionsAPI.TransactionsGet(auth).PageSize(pageSize).Execute() - if err != nil { - fmt.Fprintf(os.Stderr, "Error when calling `TransactionsAPI.TransactionsGet``: %v\n", err) - fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r2.Body) +var global GlobalState +var sessionManager *scs.SessionManager + +func getHandler(w http.ResponseWriter, r *http.Request) { + userCount := sessionManager.GetInt(r.Context(), "count") + component := templates.Page(global.Count, userCount) + component.Render(r.Context(), w) +} + +func postHandler(w http.ResponseWriter, r *http.Request) { + // Update state. + r.ParseForm() + + // Check to see if the global button was pressed. + if r.Form.Has("global") { + global.Count++ } - // response from `TransactionsGet`: ListTransactionsResponse - fmt.Fprintf(os.Stdout, "Response from `TransactionsAPI.TransactionsGet`: %v\n", resp2) + //TODO: Update session. + if r.Form.Has("user") { + currentCount := sessionManager.GetInt(r.Context(), "count") + sessionManager.Put(r.Context(), "count", currentCount+1) + } + // Display the form. + getHandler(w, r) +} +func handleInfo(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + postHandler(w, r) + return + } + getHandler(w, r) } // HandleRequests function to define the routes and start the server -func HandleRequests() { - http.HandleFunc("/", homePage) // Set the root URL to call homePage function - http.HandleFunc("/info", getInfo) - log.Default().Println("Serving request at localhost:8080") - log.Fatal(http.ListenAndServe(":8080", nil)) // Start the server on port 8080 +func HandleRequests(upBankToken string, log *log.Logger) { + sessionManager = scs.New() + sessionManager.Lifetime = 24 * time.Hour + auth := context.WithValue(context.Background(), upclient.ContextAccessToken, upBankToken) + + configuration := upclient.NewConfiguration() + apiClient := upclient.NewAPIClient(configuration) + + accountHandler := handlers.NewAccountHandler(log, apiClient, auth) + transactionsHandler := handlers.NewTransactionHandler(log, apiClient, auth) + component := templates.Hello("its ya boi") + mux := http.NewServeMux() + mux.HandleFunc(accountHandler.Uri, accountHandler.ServeHTTP) + mux.HandleFunc(transactionsHandler.Uri, transactionsHandler.ServeHTTP) + + mux.HandleFunc("/", homePage) + mux.HandleFunc("/info", getInfo) + mux.Handle("/time", NewNowHandler(time.Now)) + mux.Handle("/hello", templ.Handler(component)) + mux.HandleFunc("/counter", handleInfo) + log.Println("Serving request at localhost:8080") + muxWithSessionMiddleware := sessionManager.LoadAndSave(mux) + if err := http.ListenAndServe("localhost:8080", muxWithSessionMiddleware); err != nil { + log.Printf("error listening: %v", err) + } } diff --git a/datafetcher/templ/components.templ b/datafetcher/templ/components.templ new file mode 100644 index 0000000..2bbda5a --- /dev/null +++ b/datafetcher/templ/components.templ @@ -0,0 +1,31 @@ +package templ + +import ( + "strconv" + "time" +) +templ TimeComponent(d time.Time) { +