Skip to content

Commit d9a5f06

Browse files
authored
Merge branch 'master' into dependabot/go_modules/backend/github.com/buger/jsonparser-1.1.1
2 parents 3a8e10a + 242f29a commit d9a5f06

File tree

129 files changed

+3930
-1145
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

129 files changed

+3930
-1145
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ While we still have the waitlist in place (until October 7th), these are the ste
120120
Follow these steps, and you should see what a new user to General Task would see when signing up.
121121

122122
## Deploying backend
123-
Our backend is currently on AWS EKS in us-west-1 region. These are the steps to setup access
123+
Our backend is currently on AWS EKS in us-west-1 region. These are the steps to setup access.
124+
124125
We currently perform backend deploys using the Heroku CLI. Assuming you have the heroku credentials, you can deploy with the following steps:
125126

126127
### One-time Kubernetes setup
@@ -178,7 +179,7 @@ core-service NodePort 172.19.64.51 <none> 8080:31254/TCP 21d
178179
```
179180

180181
#### Common Commands
181-
Here's a list of nice k8s commands to add to your shell file:
182+
Here's a list of nice k8s commands to add to your shell startup file:
182183
```
183184
alias kp="kubectl config use-context prod --namespace prd-gtsk-uswest1"
184185
alias kgp="kubectl get pods"

backend/.env

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,6 @@ JIRA_OAUTH_CLIENT_ID=y86GV794HPeNmsyIodonW9wFvKK4MaOK
3131
JIRA_OAUTH_CLIENT_SECRET=dummy_value
3232
# Client ID here is for local App, should be different for prod app
3333
ASANA_OAUTH_CLIENT_ID=1203537986844495
34-
ASANA_OAUTH_CLIENT_SECRET=dummy_value
34+
ASANA_OAUTH_CLIENT_SECRET=dummy_value
35+
# Open AI only requires secret
36+
OPEN_AI_CLIENT_SECRET=dummy_value

backend/api/calendar_list.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package api
2+
3+
import (
4+
"context"
5+
"github.com/GeneralTask/task-manager/backend/database"
6+
"github.com/gin-gonic/gin"
7+
"go.mongodb.org/mongo-driver/bson"
8+
)
9+
10+
type CalendarResult struct {
11+
CalendarID string `json:"calendar_id,omitempty"`
12+
ColorID string `json:"color_id,omitempty"`
13+
}
14+
15+
type CalendarAccountResult struct {
16+
AccountID string `json:"account_id"`
17+
Calendars []CalendarResult `json:"calendars"`
18+
HasMulticalScope bool `json:"needs_multical_scopes"`
19+
}
20+
21+
func (api *API) CalendarsList(c *gin.Context) {
22+
userID := getUserIDFromContext(c)
23+
var userObject database.User
24+
userCollection := database.GetUserCollection(api.DB)
25+
err := userCollection.FindOne(context.Background(), bson.M{"_id": userID}).Decode(&userObject)
26+
if err != nil {
27+
api.Logger.Error().Err(err).Msg("failed to find user")
28+
Handle500(c)
29+
return
30+
}
31+
32+
calendarAccounts, err := database.GetCalendarAccounts(api.DB, userID)
33+
if err != nil {
34+
Handle500(c)
35+
return
36+
}
37+
results := []*CalendarAccountResult{}
38+
for _, account := range *calendarAccounts {
39+
// for implicit memory aliasing
40+
calendars := []CalendarResult{}
41+
for _, calendar := range account.Calendars {
42+
calendarResult := CalendarResult{
43+
CalendarID: calendar.CalendarID,
44+
ColorID: calendar.ColorID,
45+
}
46+
calendars = append(calendars, calendarResult)
47+
48+
}
49+
result := CalendarAccountResult{
50+
AccountID: account.IDExternal,
51+
Calendars: calendars,
52+
HasMulticalScope: database.HasUserGrantedMultiCalendarScope(account.Scopes),
53+
}
54+
results = append(results, &result)
55+
}
56+
57+
c.JSON(200, results)
58+
}

backend/api/calendar_list_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package api
2+
3+
import (
4+
"encoding/json"
5+
"github.com/GeneralTask/task-manager/backend/database"
6+
"github.com/stretchr/testify/assert"
7+
"go.mongodb.org/mongo-driver/bson/primitive"
8+
"net/http"
9+
"testing"
10+
)
11+
12+
func TestCalendarList(t *testing.T) {
13+
authToken := login("[email protected]", "")
14+
db, dbCleanup, err := database.GetDBConnection()
15+
assert.NoError(t, err)
16+
defer dbCleanup()
17+
userID := getUserIDFromAuthToken(t, db, authToken)
18+
notUserID := primitive.NewObjectID()
19+
_, err = database.UpdateOrCreateCalendarAccount(
20+
db,
21+
userID,
22+
"123abc",
23+
"foobar_source",
24+
&database.CalendarAccount{
25+
UserID: userID,
26+
IDExternal: "account1",
27+
Calendars: []database.Calendar{
28+
{
29+
CalendarID: "cal1",
30+
ColorID: "col1",
31+
},
32+
},
33+
Scopes: []string{"https://www.googleapis.com/auth/calendar"},
34+
},
35+
nil,
36+
)
37+
assert.NoError(t, err)
38+
_, err = database.UpdateOrCreateCalendarAccount(
39+
db,
40+
userID,
41+
"123def",
42+
"foobar_source",
43+
&database.CalendarAccount{
44+
UserID: userID,
45+
IDExternal: "account2",
46+
Calendars: []database.Calendar{
47+
{
48+
CalendarID: "cal2",
49+
ColorID: "col2",
50+
},
51+
{
52+
CalendarID: "cal3",
53+
ColorID: "col3",
54+
},
55+
},
56+
},
57+
nil,
58+
)
59+
assert.NoError(t, err)
60+
_, err = database.UpdateOrCreateCalendarAccount(
61+
db,
62+
userID,
63+
"123abc",
64+
"foobar_source",
65+
&database.CalendarAccount{
66+
UserID: notUserID,
67+
IDExternal: "account3",
68+
Calendars: []database.Calendar{
69+
{
70+
CalendarID: "cal4",
71+
ColorID: "col4",
72+
},
73+
},
74+
},
75+
nil,
76+
)
77+
assert.NoError(t, err)
78+
79+
UnauthorizedTest(t, "GET", "/calendars/", nil)
80+
t.Run("Success", func(t *testing.T) {
81+
api, dbCleanup := GetAPIWithDBCleanup()
82+
defer dbCleanup()
83+
84+
response := ServeRequest(t, authToken, "GET", "/calendars/?", nil, http.StatusOK, api)
85+
var result []CalendarAccountResult
86+
err = json.Unmarshal(response, &result)
87+
88+
assert.NoError(t, err)
89+
assert.Equal(t, 2, len(result))
90+
91+
assert.Equal(t, []CalendarAccountResult{
92+
{AccountID: "account1", Calendars: []CalendarResult{{CalendarID: "cal1", ColorID: "col1"}}, HasMulticalScope: true},
93+
{AccountID: "account2", Calendars: []CalendarResult{{CalendarID: "cal2", ColorID: "col2"}, {CalendarID: "cal3", ColorID: "col3"}}, HasMulticalScope: false},
94+
},
95+
result)
96+
})
97+
}

backend/api/helpers.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package api
22

33
import (
44
"errors"
5+
"fmt"
56
"regexp"
7+
"strconv"
68
"strings"
79
"time"
810

@@ -58,7 +60,16 @@ func GetTimezoneOffsetFromHeader(c *gin.Context) (time.Duration, error) {
5860
if len(timezoneOffsetHeader) == 0 {
5961
return time.Duration(0), errors.New("Timezone-Offset header is required")
6062
}
61-
duration, err := time.ParseDuration(timezoneOffsetHeader[0] + "m")
63+
64+
timezoneOffsetInt, err := strconv.Atoi(timezoneOffsetHeader[0])
65+
if err != nil {
66+
return 0, errors.New("Timezone-Offset header is invalid")
67+
}
68+
69+
// mod timezone to limit to minutes in a day (for security)
70+
modTimezone := timezoneOffsetInt % 1440
71+
72+
duration, err := time.ParseDuration(fmt.Sprint(modTimezone) + "m")
6273
if err != nil {
6374
return duration, errors.New("Timezone-Offset header is invalid")
6475
}

backend/api/login.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func (api *API) Login(c *gin.Context) {
8282
func (api *API) LoginCallback(c *gin.Context) {
8383
var redirectParams GoogleRedirectParams
8484
if c.ShouldBind(&redirectParams) != nil || redirectParams.State == "" || redirectParams.Code == "" || redirectParams.Scope == "" {
85-
c.JSON(400, gin.H{"detail": "missing query params"})
85+
c.Redirect(302, config.GetConfigValue("HOME_URL"))
8686
return
8787
}
8888

@@ -177,7 +177,7 @@ func createNewUserTasks(userID primitive.ObjectID, db *mongo.Database) error {
177177
SourceAccountID: external.GeneralTaskDefaultAccountID,
178178
IsCompleted: &completed,
179179
IsDeleted: &deleted,
180-
NUXNumber: index + 1,
180+
NUXNumber: constants.StarterTasksNuxIDs[index],
181181
CreatedAtExternal: primitive.NewDateTimeFromTime(time.Now()),
182182
}
183183
_, err := taskCollection.InsertOne(context.Background(), newTask)

backend/api/login_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"go.mongodb.org/mongo-driver/bson"
1111
"go.mongodb.org/mongo-driver/bson/primitive"
1212

13+
"github.com/GeneralTask/task-manager/backend/config"
1314
"github.com/GeneralTask/task-manager/backend/database"
1415
"github.com/GeneralTask/task-manager/backend/external"
1516
"github.com/stretchr/testify/assert"
@@ -120,10 +121,9 @@ func TestLoginCallback(t *testing.T) {
120121
request, _ := http.NewRequest("GET", "/login/callback/", nil)
121122
recorder := httptest.NewRecorder()
122123
router.ServeHTTP(recorder, request)
123-
assert.Equal(t, http.StatusBadRequest, recorder.Code)
124-
body, err := io.ReadAll(recorder.Body)
125-
assert.NoError(t, err)
126-
assert.Equal(t, "{\"detail\":\"missing query params\"}", string(body))
124+
assert.Equal(t, http.StatusFound, recorder.Code)
125+
// check that we redirect to the home page
126+
assert.Equal(t, config.GetConfigValue("HOME_URL"), recorder.Result().Header.Get("Location"))
127127
})
128128

129129
t.Run("Idempotent", func(t *testing.T) {

backend/api/overview.go

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -583,8 +583,8 @@ func reorderTaskResultsByDueDate(taskResults []*TaskResult) []*TaskResult {
583583
sort.SliceStable(taskResults, func(i, j int) bool {
584584
a := taskResults[i]
585585
b := taskResults[j]
586-
aTime, _ := time.Parse("2006-01-02", a.DueDate)
587-
bTime, _ := time.Parse("2006-01-02", b.DueDate)
586+
aTime, _ := time.Parse(time.RFC3339, a.DueDate)
587+
bTime, _ := time.Parse(time.RFC3339, b.DueDate)
588588
return aTime.Unix() < bTime.Unix()
589589
})
590590
for idx, result := range taskResults {
@@ -807,6 +807,53 @@ func (api *API) ViewDoesExist(db *mongo.Database, userID primitive.ObjectID, par
807807
return count > 0, nil
808808
}
809809

810+
type ViewBulkModifyParams struct {
811+
OrderedViewIDs []string `json:"ordered_view_ids" binding:"required"`
812+
}
813+
814+
// NOTE: this endpoint ONLY updates the view IDs provided, so a complete list of view IDs should be provided
815+
func (api *API) OverviewViewBulkModify(c *gin.Context) {
816+
userID := getUserIDFromContext(c)
817+
var viewModifyParams ViewBulkModifyParams
818+
err := c.BindJSON(&viewModifyParams)
819+
if err != nil {
820+
c.JSON(400, gin.H{"detail": "invalid or missing parameter"})
821+
return
822+
}
823+
824+
var operations []mongo.WriteModel
825+
for index, viewIDHex := range viewModifyParams.OrderedViewIDs {
826+
viewID, err := primitive.ObjectIDFromHex(viewIDHex)
827+
if err != nil {
828+
c.JSON(400, gin.H{"detail": "malformatted view ID"})
829+
return
830+
}
831+
newIDOrdering := index + 1
832+
operation := mongo.NewUpdateOneModel()
833+
operation.SetFilter(bson.M{
834+
"$and": []bson.M{
835+
{"user_id": userID},
836+
{"_id": viewID},
837+
},
838+
})
839+
operation.SetUpdate(bson.M{"$set": bson.M{"id_ordering": newIDOrdering}})
840+
operations = append(operations, operation)
841+
}
842+
843+
viewCollection := database.GetViewCollection(api.DB)
844+
result, err := viewCollection.BulkWrite(context.Background(), operations)
845+
if err != nil {
846+
api.Logger.Error().Err(err).Msg("failed to bulk modify view ordering")
847+
Handle500(c)
848+
return
849+
}
850+
if result.MatchedCount != int64(len(operations)) {
851+
c.JSON(400, gin.H{"detail": "invalid or duplicate view IDs provided"})
852+
return
853+
}
854+
c.JSON(200, gin.H{})
855+
}
856+
810857
type ViewModifyParams struct {
811858
IDOrdering int `json:"id_ordering" binding:"required"`
812859
}

0 commit comments

Comments
 (0)