diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..30d108a --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,70 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ development, master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ development ] + schedule: + - cron: '0 8 * * 1' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'go', 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.gitignore b/.gitignore index 7c5b4ba..8b62380 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,7 @@ _ignored/ .vscode/launch.json .vscode/extensions.json *.code-workspace - +*.gitconfig # VisualStudioCode Patch # Ignore all local history of files .history @@ -37,4 +37,7 @@ _ignored/ # MacOS .DS_Store -.idea \ No newline at end of file +.idea + +samconfig.yaml +scripts/badges/__pycache__/create.cpython-39.pyc diff --git a/badges/dynamodb.py b/badges/dynamodb.py new file mode 100644 index 0000000..5ac8f63 --- /dev/null +++ b/badges/dynamodb.py @@ -0,0 +1,73 @@ +import boto3 +d_client = boto3.client('dynamodb') + + +class ddb_ops(): + """Get Item""" + + def get_item(self, table, crowdaction_id): + self.table = table + self.crowdaction_id = crowdaction_id + + res = d_client.get_item( + TableName=table, + Key={ + 'pk': {'S': 'act'}, + 'sk': {'S': crowdaction_id} + } + ) + return res + + """Query Items""" + + def query(self, table, crowdaction_id): + self.table = table + self.crowdaction_id = crowdaction_id + + res = d_client.query( + TableName=table, + IndexName='invertedIndex', + KeyConditionExpression="sk = :sk", + ExpressionAttributeValues={ + ':sk': {'S': f'prt#act#{crowdaction_id}'} + }, + ) + return res + + """Update Items""" + + def update(self, table, usr_id, reward, crowdaction_id): + self.table = table + self.usr_id = usr_id + self.reward = reward + self.crowdaction_id = crowdaction_id + + print("update", table, usr_id, reward, crowdaction_id) + + d_client.update_item( + TableName=table, + Key={ + 'userid': { + 'S': usr_id, + } + }, + AttributeUpdates={ + 'reward': { + 'Value': { + 'L': [ + { + "M": { + "award": { + "S": reward + }, + "crowdactionID": { + "S": crowdaction_id + } + } + }, + ], + }, + 'Action': 'ADD' # this operations still pending + } + }, + ) diff --git a/badges/eventbus.py b/badges/eventbus.py new file mode 100644 index 0000000..87d1ad7 --- /dev/null +++ b/badges/eventbus.py @@ -0,0 +1,132 @@ +# This function should be responsible for point system calculation +# as well as the event rule deletion from the bus once the crowdaction +# terminates + +import json +import logging +import boto3 +import os +from dynamodb import * + +e_client = boto3.client('events') +l_client = boto3.client('lambda') + +ddb = ddb_ops() + +commit_dict = {} # this may be global for now + + +def compute_badge_award(points, reward_list): + """ + There is an assumption about the order + of the reward_list. This is taken into + account a descending order + """ + if int(points) >= int(reward_list[3]): + return "Diamond" + elif int(points) >= int(reward_list[2]): + return "Golden" + elif int(points) >= int(reward_list[1]): + return "Silver" + elif int(points) >= int(reward_list[0]): + return "Bronze" + else: + return "No reward" + + +def tree_recursion(tree): + for i in range(0, len(tree)): + t = tree[i]['M'] + commit_key = t['id']['S'] + commit_points = t['points']['N'] + commit_dict[commit_key] = commit_points + if 'requires' in t: + tree_recursion(t['requires']['L']) + + +def lambda_handler(event, context): + badge_reward_list = [] + + target_name = 'lambda_cron_test_end_date' + single_table = 'collaction-dev-edreinoso-SingleTable-BAXICTFSQ4WV' + profile_table = 'collaction-dev-edreinoso-ProfileTable-XQEJJNBK6UUY' + crowdaction_id = event['resources'][0].split( + '/')[1].replace('_', '#') # prod + print('Lambda Crontab!', crowdaction_id) + + """ + POINT CALCULATION LOGIC + """ + + # 1. fetch the badge scale for crowdaction ✅ + badge_scale = ddb.get_item(single_table, crowdaction_id) + + tree = badge_scale['Item']['commitment_options']['L'] + for reward in badge_scale['Item']['badges']['L']: + badge_reward_list.append(reward['N']) + # print(badge_reward_list) # verfying the badge reward list + + # 2. restructure the tree to a dictionary ✅ + tree_recursion(tree) + print(commit_dict) # verifying the dictionary convertion + + # 3. go through all participants ✅ + participant_list = ddb.query(single_table, crowdaction_id) + print(participant_list) + + # 4. map user commitment level ✅ + user_prt_list = [] # list required to store individual participations + for i in range(0, len(participant_list['Items'])): + prt_details = participant_list['Items'][i] + usr_id = prt_details['userID']['S'] + prt_lvl = prt_details['commitments']['L'] + usr_prt_counter = 0 + for n in range(0, len(prt_lvl)): + usr_prt_counter += int(commit_dict[prt_lvl[n]['S']]) + usr_obj = { + "userid": usr_id, + "prt": prt_lvl, + "points": usr_prt_counter + } + print(usr_obj) + # if prt_lvl in commit_dict: # would I be assuming that a user would always have a participation + usr_obj['badge'] = compute_badge_award( + usr_prt_counter, badge_reward_list) + user_prt_list.append(usr_obj) + + print(user_prt_list) + + # 5. award badge ✅ + for usr in user_prt_list: + ddb.update( + profile_table, usr['userid'], usr['badge'], crowdaction_id) + + """ + CLEANING UP TARGETS AND EVENTS + """ + + crowdaction_id_e = crowdaction_id.replace('#', '_') + + # 6. delete permission ✅ + l_client.remove_permission( + FunctionName=target_name, + StatementId=crowdaction_id_e, + ) + + # 7. delete targets ✅ + e_client.remove_targets( + Rule=crowdaction_id_e, + Ids=[ + crowdaction_id_e, + ], + ) + + # 8. delete event ✅ + e_client.delete_rule( + Name=crowdaction_id_e, + ) + + return { + 'statusCode': 200, + 'body': json.dumps('Crowdaction Ended!') + } diff --git a/badges/trigger.py b/badges/trigger.py new file mode 100644 index 0000000..537d251 --- /dev/null +++ b/badges/trigger.py @@ -0,0 +1,84 @@ +""" + This function should be responsible for keeping track of the end date + of the crowdaction, to record it in the event bus for later point + system calculation +""" + +from datetime import datetime +import logging +import boto3 +import json +import random +import string + +e_client = boto3.client('events') +l_client = boto3.client('lambda') + + +def lambda_handler(event, context): + if(event['Records'][0]['eventName'] == 'INSERT'): + record = event['Records'][0]['dynamodb']['NewImage'] + + # TODO: test this! + # checking for records that are only for crowdactions + if (record['pk']['S'] != "act"): + return { + 'statusCode': 200, + 'body': json.dumps('Event is not crowdaction') + } + + target_arn = 'arn:aws:lambda:eu-central-1:156764677614:function:lambda_cron_test_end_date' + target_name = 'lambda_cron_test_end_date' + action = 'lambda:InvokeFunction' + + title = record['title']['S'] + description = record['description']['S'] + date_end = record['date_end']['S'] + crowdaction_id = record['sk']['S'].replace('#', '_') + # may have to handle date exception + date_end_expr = datetime_to_cron( + datetime.strptime(date_end, "%Y-%m-%d %H:%M:%S")) + + # PUT TARGET + e_client.put_rule( + Name=crowdaction_id, + ScheduleExpression=date_end_expr, + State='ENABLED', + Description='event rule for ' + crowdaction_id, + ) + + # PUT TARGET + e_client.put_targets( + Rule=crowdaction_id, + Targets=[ + { + 'Id': crowdaction_id, + 'Arn': target_arn, + }, + ] + ) + + # ADD PERMISSIONS + l_client.add_permission( + FunctionName=target_name, + StatementId=crowdaction_id, + Action=action, + Principal='events.amazonaws.com', + SourceArn='arn:aws:events:eu-central-1:156764677614:rule/'+crowdaction_id, + ) + + print('rule has been placed on the bus') + + return { + 'statusCode': 200, + 'body': json.dumps('End day has been scheduled!') + } + + +""" + Function that would convert datetime into cronjobs +""" + + +def datetime_to_cron(dt): + return f"cron({dt.minute} {dt.hour} {dt.day} {dt.month} ? {dt.year})" diff --git a/docs/api2.yaml b/docs/api2.yaml index d1aa6c1..601a626 100644 --- a/docs/api2.yaml +++ b/docs/api2.yaml @@ -84,18 +84,6 @@ paths: tags: - Crowdaction summary: Get list of crowdactions - parameters: - - $ref: "#/components/parameters/ApiVersionParameter" - - name: status - in: query - required: false - schema: - type: string - enum: - - featured - - joinable - - active - - ended responses: "200": description: List of crowdactions @@ -399,10 +387,17 @@ paths: type: string default: success data: - $ref: "#/components/schemas/Profile" - "403": - $ref: "#/components/responses/UnsupportedClientVersion" - "404": + allOf: + - $ref: '#/components/schemas/Profile' + - type: object + properties: + badges: + type: array + items: + $ref: '#/components/schemas/Badge' + '403': + $ref: '#/components/responses/UnsupportedClientVersion' + '404': description: Profile was not found content: application/json: @@ -1143,6 +1138,9 @@ components: label: type: string example: Becoming vegetarian + points: + type: int + example: 30 description: type: string example: I will not eat any meat from any animal (including fish). @@ -1157,6 +1155,7 @@ components: id: no-beef label: Not eating beef description: I will avoid eating beef (Goodbye stake). + points: 30 Date: type: string pattern: '\d{4}-\d{2}-\d{2}' @@ -1225,6 +1224,23 @@ components: example: >- Hi, I am Max and I am trying to eat less meat to stop animal suffering. + Badge: + type: object + properties: + crowdactionID: + type: string + example: 'sustainability#food#88615462-2789-4159-8659-2ecfd33ef305' + title: + type: string + description: Title of the crowdactions + badge_title: + type: string + example: 'vegan hero' + badge_description: + type: string + example: 'The highest sacrifices deserve the highest awards' + badge_image: + $ref: '#/components/schemas/Image' Email: type: object properties: diff --git a/internal/contact/email.go b/internal/contact/email.go index 2bc33b6..4629546 100644 --- a/internal/contact/email.go +++ b/internal/contact/email.go @@ -3,6 +3,7 @@ package contact import ( "context" "fmt" + "github.com/CollActionteam/collaction_backend/internal/constants" "github.com/CollActionteam/collaction_backend/internal/models" ) @@ -33,16 +34,16 @@ func NewContactService(emailRepository EmailRepository, configManager ConfigMana } func (e *contact) SendEmail(ctx context.Context, req models.EmailContactRequest) error { - recipient, err := e.configManager.GetParameter(fmt.Sprintf(constants.RecipientEmail, e.stage)) + senderRecipient, err := e.configManager.GetParameter(fmt.Sprintf(constants.RecipientEmail, e.stage)) if err != nil { return err } return e.emailRepository.Send(ctx, models.EmailData{ - Recipient: recipient, + Recipient: senderRecipient, Message: fmt.Sprintf(EmailMessageFormat, req.Data.Message, Separator, req.Data.AppVersion), Subject: req.Data.Subject, - Sender: req.Data.Email, + Sender: senderRecipient, ReplyEmail: req.Data.Email, }) } diff --git a/internal/contact/email_test.go b/internal/contact/email_test.go index da3e053..36f0153 100644 --- a/internal/contact/email_test.go +++ b/internal/contact/email_test.go @@ -28,7 +28,7 @@ func TestContact_SendEmail(t *testing.T) { Recipient: recipientValue, Message: fmt.Sprintf(contact.EmailMessageFormat, emailRequest.Data.Message, contact.Separator, emailRequest.Data.AppVersion), Subject: emailRequest.Data.Subject, - Sender: emailRequest.Data.Email, + Sender: recipientValue, ReplyEmail: emailRequest.Data.Email, } @@ -52,7 +52,7 @@ func TestContact_SendEmail(t *testing.T) { Recipient: recipientValue, Message: fmt.Sprintf(contact.EmailMessageFormat, emailRequest.Data.Message, contact.Separator, emailRequest.Data.AppVersion), Subject: emailRequest.Data.Subject, - Sender: emailRequest.Data.Email, + Sender: recipientValue, ReplyEmail: emailRequest.Data.Email, } diff --git a/internal/crowdactions/crowdaction.go b/internal/crowdactions/crowdaction.go new file mode 100644 index 0000000..3bc5b34 --- /dev/null +++ b/internal/crowdactions/crowdaction.go @@ -0,0 +1,56 @@ +package crowdaction + +import ( + "context" + + m "github.com/CollActionteam/collaction_backend/internal/models" + "github.com/CollActionteam/collaction_backend/utils" +) + +type Service interface { + GetAllCrowdactions(ctx context.Context) ([]m.CrowdactionData, error) + GetCrowdactionById(ctx context.Context, crowdactionId string) (*m.CrowdactionData, error) + GetCrowdactionsByStatus(ctx context.Context, status string, startFrom *utils.PrimaryKey) ([]m.CrowdactionData, error) + RegisterCrowdaction(ctx context.Context, payload m.CrowdactionData) (*m.CrowdactionData, error) +} +type CrowdactionManager interface { + GetAll() ([]m.CrowdactionData, error) + GetById(pk string, crowdactionId string) (*m.CrowdactionData, error) + GetByStatus(filterCond string, startFrom *utils.PrimaryKey) ([]m.CrowdactionData, error) + Register(ctx context.Context, payload m.CrowdactionData) (*m.CrowdactionData, error) +} + +const ( + KeyDateStart = "date_start" + KeyDateEnd = "date_end" + KeyDateJoinBefore = "date_limit_join" +) + +type crowdactionService struct { + crowdactionRepository CrowdactionManager +} + +func NewCrowdactionService(crowdactionRepository CrowdactionManager) Service { + return &crowdactionService{crowdactionRepository: crowdactionRepository} +} + +func (e *crowdactionService) GetAllCrowdactions(ctx context.Context) ([]m.CrowdactionData, error) { + return e.crowdactionRepository.GetAll() +} + +func (e *crowdactionService) GetCrowdactionById(ctx context.Context, crowdactionID string) (*m.CrowdactionData, error) { + return e.crowdactionRepository.GetById(utils.PKCrowdaction, crowdactionID) +} + +func (e *crowdactionService) GetCrowdactionsByStatus(ctx context.Context, status string, startFrom *utils.PrimaryKey) ([]m.CrowdactionData, error) { + return e.crowdactionRepository.GetByStatus(status, startFrom) +} + +func (e *crowdactionService) RegisterCrowdaction(ctx context.Context, payload m.CrowdactionData) (*m.CrowdactionData, error) { + res, err := e.crowdactionRepository.Register(ctx, payload) + + if err != nil { + return res, err + } + return res, err +} diff --git a/internal/crowdactions/crowdaction_test.go b/internal/crowdactions/crowdaction_test.go new file mode 100644 index 0000000..7818ffa --- /dev/null +++ b/internal/crowdactions/crowdaction_test.go @@ -0,0 +1,33 @@ +package crowdaction_test + +import ( + "context" + "testing" + + cwd "github.com/CollActionteam/collaction_backend/internal/crowdactions" + m "github.com/CollActionteam/collaction_backend/internal/models" + "github.com/CollActionteam/collaction_backend/pkg/mocks/repository" + + "github.com/CollActionteam/collaction_backend/utils" + "github.com/stretchr/testify/assert" +) + +func TestCrowdaction_GetCrowdactionById(t *testing.T) { + as := assert.New(t) + dynamoRepository := &repository.Dynamo{} + var ctx context.Context + var crowdactions *m.CrowdactionData + crowdactionID := "sustainability#food#185f66fd" + + t.Run("dev stage", func(t *testing.T) { + dynamoRepository.On("GetById", utils.PKCrowdaction, crowdactionID).Return(crowdactions, nil).Once() + + service := cwd.NewCrowdactionService(dynamoRepository) + + _, err := service.GetCrowdactionById(ctx, crowdactionID) + + as.NoError(err) + + dynamoRepository.AssertExpectations(t) + }) +} diff --git a/internal/models/commitment.go b/internal/models/commitment.go new file mode 100644 index 0000000..46eec0d --- /dev/null +++ b/internal/models/commitment.go @@ -0,0 +1,9 @@ +package models + +type CommitmentOption struct { + Id string `json:"id"` + Label string `json:"label"` + Description string `json:"description"` + Requires []CommitmentOption `json:"requires,omitempty"` + Points int `json:"points"` +} diff --git a/internal/models/contact.go b/internal/models/contact.go index 750fc32..0306501 100644 --- a/internal/models/contact.go +++ b/internal/models/contact.go @@ -1,16 +1,23 @@ package models +import ( + "context" + "regexp" + + "github.com/CollActionteam/collaction_backend/utils" + "github.com/go-playground/validator/v10" +) + type EmailContactRequest struct { Data EmailRequestData `json:"data" validate:"required"` Nonce string `json:"nonce"` } type EmailRequestData struct { - Email string `json:"email" validate:"required,email" binding:"required"` - Subject string `json:"subject" validate:"required,lte=50" binding:"required"` - Message string `json:"message" validate:"required,lte=500" binding:"required"` - //TODO 11.01.22 mrsoftware: fix regx - AppVersion string `json:"app_version" validate:"required" binding:"required"` // ,regexp=^(?:ios|android) [0-9]+\\.[0-9]+\\.[0-9]+\\+[0-9]+$ + Email string `json:"email" validate:"required,email" binding:"required"` + Subject string `json:"subject" validate:"required,lte=50" binding:"required"` + Message string `json:"message" validate:"required,lte=500" binding:"required"` + AppVersion string `json:"app_version" validate:"required" binding:"required"` } type EmailData struct { @@ -20,3 +27,17 @@ type EmailData struct { Sender string ReplyEmail string } + +func (e EmailContactRequest) Validate(ctx context.Context) validator.ValidationErrorsTranslations { + validate := validator.New() + if err := validate.StructCtx(ctx, e); err != nil { + return utils.ValidationResponse(err, validate) + } + + reg := regexp.MustCompile(`^(?:ios|android) [0-9]+\.[0-9]+\.[0-9]+\+[0-9]+$`) + if match := reg.MatchString(e.Data.AppVersion); !match { + return validator.ValidationErrorsTranslations{"err": "app version is not valid"} + } + + return nil +} diff --git a/internal/models/crowdaction.go b/internal/models/crowdaction.go new file mode 100644 index 0000000..15ae68f --- /dev/null +++ b/internal/models/crowdaction.go @@ -0,0 +1,33 @@ +package models + +type CrowdactionRequest struct { + Data CrowdactionData `json:"data" validate:"required"` +} + +type CrowdactionParticipant struct { + Name string `json:"name,omitempty"` + UserID string `json:"userID,omitempty"` +} + +type CrowdactionImages struct { + Card string `json:"card,omitempty"` + Banner string `json:"banner,omitempty"` +} + +type CrowdactionData struct { + Title string `json:"title"` + Description string `json:"description"` + Category string `json:"category"` + Subcategory string `json:"subcategory"` + Location string `json:"location"` + DateEnd string `json:"date_end"` + DateLimitJoin string `json:"date_limit_join"` + PasswordJoin string `json:"password_join"` + Images CrowdactionImages `json:"images"` + CrowdactionID string `json:"crowdactionID"` + Badges []int `json:"badges"` + DateStart string `json:"date_start"` + ParticipationCount int `json:"participant_count"` + TopParticipants []CrowdactionParticipant `json:"top_participants"` + CommitmentOptions []CommitmentOption `json:"commitment_options"` +} diff --git a/internal/uploads/profilePicture.go b/internal/uploads/profilePicture.go new file mode 100644 index 0000000..5429b64 --- /dev/null +++ b/internal/uploads/profilePicture.go @@ -0,0 +1,23 @@ +package uploads + +import "context" + +type ProfileImageUploadRepository interface { + GetUploadUrl(ctx context.Context, ext string, userID string) (string, error) +} + +type Service interface { + GetUploadUrl(ctx context.Context, ext string, userID string) (string, error) +} + +type image struct { + imageUploadRepository ProfileImageUploadRepository +} + +func NewProfileImageUploadService(profileImageUploadRepo ProfileImageUploadRepository) Service { + return &image{imageUploadRepository: profileImageUploadRepo} +} + +func (i *image) GetUploadUrl(ctx context.Context, ext string, userID string) (string, error) { + return i.imageUploadRepository.GetUploadUrl(ctx, ext, userID) +} diff --git a/internal/uploads/profilePicture_test.go b/internal/uploads/profilePicture_test.go new file mode 100644 index 0000000..fdf32cf --- /dev/null +++ b/internal/uploads/profilePicture_test.go @@ -0,0 +1,64 @@ +package uploads_test + +import ( + "context" + "errors" + "testing" + + "github.com/CollActionteam/collaction_backend/internal/uploads" + "github.com/CollActionteam/collaction_backend/pkg/mocks/repository" + "github.com/stretchr/testify/assert" +) + +func TestProfile_GetUploadUrl(t *testing.T) { + as := assert.New(t) + imageUploadRepository := &repository.ProfilePicture{} + + t.Run("happy path", func(t *testing.T) { + ext := "jpeg" + userID := "1" + service := uploads.NewProfileImageUploadService(imageUploadRepository) + + imageUploadRepository.On("GetUploadUrl", context.Background(), ext, userID).Return("https://sample-user-url-generated", nil).Once() + url, err := service.GetUploadUrl(context.Background(), ext, userID) + as.Equal("https://sample-user-url-generated", url) + as.NoError(err) + imageUploadRepository.AssertExpectations(t) + }) + + t.Run("input error: userID empty", func(t *testing.T) { + ext := "jpeg" + userID := "" + service := uploads.NewProfileImageUploadService(imageUploadRepository) + + imageUploadRepository.On("GetUploadUrl", context.Background(), ext, userID).Return("", errors.New("user not found")).Once() + url, err := service.GetUploadUrl(context.Background(), ext, userID) + as.Equal("", url) + as.Error(err) + imageUploadRepository.AssertExpectations(t) + }) + + t.Run("input error: invalid image format", func(t *testing.T) { + ext := "agsgfh" + userID := "1" + service := uploads.NewProfileImageUploadService(imageUploadRepository) + + imageUploadRepository.On("GetUploadUrl", context.Background(), ext, userID).Return("", errors.New("unknown file format")).Once() + url, err := service.GetUploadUrl(context.Background(), ext, userID) + as.Equal("", url) + as.Error(err) + imageUploadRepository.AssertExpectations(t) + }) + + t.Run("system error", func(t *testing.T) { + ext := "jpeg" + userID := "1" + service := uploads.NewProfileImageUploadService(imageUploadRepository) + + imageUploadRepository.On("GetUploadUrl", context.Background(), ext, userID).Return("", errors.New("something failed")).Once() + url, err := service.GetUploadUrl(context.Background(), ext, userID) + as.Equal("", url) + as.Error(err) + imageUploadRepository.AssertExpectations(t) + }) +} diff --git a/pkg/handler/aws/crowdaction/crowdaction.go b/pkg/handler/aws/crowdaction/crowdaction.go new file mode 100644 index 0000000..83e7deb --- /dev/null +++ b/pkg/handler/aws/crowdaction/crowdaction.go @@ -0,0 +1,114 @@ +package main + +import ( + "context" + "encoding/json" + "net/http" + + cwd "github.com/CollActionteam/collaction_backend/internal/crowdactions" + m "github.com/CollActionteam/collaction_backend/internal/models" + hnd "github.com/CollActionteam/collaction_backend/pkg/handler" + awsRepository "github.com/CollActionteam/collaction_backend/pkg/repository/aws" + "github.com/CollActionteam/collaction_backend/utils" + "github.com/aws/aws-lambda-go/events" + "github.com/go-playground/validator/v10" +) + +type CrowdactionHandler struct { + service cwd.Service +} + +func NewCrowdactionHandler() *CrowdactionHandler { + crowdactionParticipation := awsRepository.NewCrowdaction(awsRepository.NewDynamo()) + return &CrowdactionHandler{service: cwd.NewCrowdactionService(crowdactionParticipation)} +} + +func (c *CrowdactionHandler) createCrowdaction(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { + var payload m.CrowdactionData + if err := json.Unmarshal([]byte(req.Body), &payload); err != nil { + return utils.CreateMessageHttpResponse(http.StatusInternalServerError, err.Error()), nil + } + + res, err := c.service.RegisterCrowdaction(ctx, payload) + + if err != nil { + return utils.CreateMessageHttpResponse(http.StatusInternalServerError, err.Error()), nil + + } + // return call to client + body, _ := json.Marshal(hnd.Response{Status: hnd.StatusSuccess, Data: res}) + + return events.APIGatewayV2HTTPResponse{ + Body: string(body), + StatusCode: http.StatusOK, + }, nil +} + +/** + Gateway for all the GET crowdaction methods +**/ + +func (c *CrowdactionHandler) getCrowdaction(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { + crowdactionID := req.PathParameters["crowdactionID"] + var request m.CrowdactionRequest + + validate := validator.New() + if err := validate.StructCtx(ctx, request); err != nil { + body, _ := json.Marshal(hnd.Response{Status: hnd.StatusFail, Data: map[string]interface{}{"error": utils.ValidationResponse(err, validate)}}) + return events.APIGatewayV2HTTPResponse{Body: string(body), StatusCode: http.StatusBadRequest}, nil + } + + if crowdactionID == "" && req.QueryStringParameters["status"] != "" { // get only by status + status := req.QueryStringParameters["status"] + return c.getCrowdactionsByStatus(ctx, status) + } else if crowdactionID == "" { // to get all crowdactions + return c.getAllCrowdactions(ctx) + } + + return c.getCrowdactionByID(ctx, crowdactionID) // get crowdaction by id +} + +func (c *CrowdactionHandler) getAllCrowdactions(ctx context.Context) (events.APIGatewayV2HTTPResponse, error) { + getCrowdactions, err := c.service.GetAllCrowdactions(ctx) + if err != nil { + return utils.CreateMessageHttpResponse(http.StatusInternalServerError, err.Error()), nil + } + + jsonPayload, _ := json.Marshal(getCrowdactions) + + return events.APIGatewayV2HTTPResponse{ + Body: string(jsonPayload), + StatusCode: http.StatusOK, + }, nil +} + +func (c *CrowdactionHandler) getCrowdactionByID(ctx context.Context, crowdactionID string) (events.APIGatewayV2HTTPResponse, error) { + getCrowdaction, err := c.service.GetCrowdactionById(ctx, crowdactionID) + + if err != nil { + return utils.CreateMessageHttpResponse(http.StatusInternalServerError, err.Error()), nil + } + if getCrowdaction == nil { + return utils.CreateMessageHttpResponse(http.StatusNotFound, "Crowdaction not found!"), nil + } + + jsonPayload, _ := json.Marshal(getCrowdaction) + return events.APIGatewayV2HTTPResponse{ + Body: string(jsonPayload), + StatusCode: http.StatusOK, + }, nil +} + +func (c *CrowdactionHandler) getCrowdactionsByStatus(ctx context.Context, status string) (events.APIGatewayV2HTTPResponse, error) { + getCrowdactions, err := c.service.GetCrowdactionsByStatus(ctx, status, nil) + + if err != nil { + return utils.CreateMessageHttpResponse(http.StatusInternalServerError, err.Error()), nil + } + jsonPayload, _ := json.Marshal(getCrowdactions) + + return events.APIGatewayV2HTTPResponse{ + Body: string(jsonPayload), + StatusCode: http.StatusOK, + }, nil +} diff --git a/pkg/handler/aws/crowdaction/main.go b/pkg/handler/aws/crowdaction/main.go new file mode 100644 index 0000000..61a0ae7 --- /dev/null +++ b/pkg/handler/aws/crowdaction/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "context" + "net/http" + "strings" + + "github.com/CollActionteam/collaction_backend/utils" + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-lambda-go/lambda" +) + +func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { + method := strings.ToLower(req.RequestContext.HTTP.Method) + + switch method { + case "post": + return NewCrowdactionHandler().createCrowdaction(ctx, req) + case "get": + return NewCrowdactionHandler().getCrowdaction(ctx, req) + default: + return utils.CreateMessageHttpResponse(http.StatusNotImplemented, "not implemented"), nil + } +} + +func main() { + lambda.Start(handler) +} diff --git a/pkg/handler/aws/emailContact/main.go b/pkg/handler/aws/emailContact/main.go index ac7129e..fb699e2 100644 --- a/pkg/handler/aws/emailContact/main.go +++ b/pkg/handler/aws/emailContact/main.go @@ -3,16 +3,15 @@ package main import ( "context" "encoding/json" + "net/http" + "github.com/CollActionteam/collaction_backend/internal/contact" "github.com/CollActionteam/collaction_backend/internal/models" hnd "github.com/CollActionteam/collaction_backend/pkg/handler" awsRepository "github.com/CollActionteam/collaction_backend/pkg/repository/aws" - "github.com/CollActionteam/collaction_backend/utils" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go/aws/session" - "github.com/go-playground/validator/v10" - "net/http" ) func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { @@ -22,9 +21,8 @@ func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.AP } // TODO implement POW verification using nonce (see https://github.com/CollActionteam/collaction_backend/issues/58) - validate := validator.New() - if err := validate.StructCtx(ctx, request); err != nil { - body, _ := json.Marshal(hnd.Response{Status: hnd.StatusFail, Data: map[string]interface{}{"error": utils.ValidationResponse(err, validate)}}) + if err := request.Validate(ctx); err != nil { + body, _ := json.Marshal(hnd.Response{Status: hnd.StatusFail, Data: map[string]interface{}{"error": err}}) return events.APIGatewayV2HTTPResponse{Body: string(body), StatusCode: http.StatusBadRequest}, nil } diff --git a/pkg/handler/aws/uploadProfilePicture/main.go b/pkg/handler/aws/uploadProfilePicture/main.go new file mode 100644 index 0000000..7309ce5 --- /dev/null +++ b/pkg/handler/aws/uploadProfilePicture/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "context" + "encoding/json" + "net/http" + + "github.com/CollActionteam/collaction_backend/auth" + "github.com/CollActionteam/collaction_backend/internal/uploads" + awsRepository "github.com/CollActionteam/collaction_backend/pkg/repository/aws" + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-lambda-go/lambda" + "github.com/aws/aws-sdk-go/aws/session" +) + +func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (res events.APIGatewayV2HTTPResponse, err error) { + usrInf, err := auth.ExtractUserInfo(req) + if err != nil { + res = events.APIGatewayV2HTTPResponse{StatusCode: http.StatusForbidden, Body: "user not authorized"} + return res, err + } + + userID := usrInf.UserID() + sess := session.Must(session.NewSession()) + profileImageUploadRepo := awsRepository.NewProfilePicture(sess) + + strUrl, err := uploads.NewProfileImageUploadService(profileImageUploadRepo).GetUploadUrl(ctx, "png", userID) + if err != nil { + res = events.APIGatewayV2HTTPResponse{StatusCode: http.StatusInternalServerError, Body: "error generating link"} + return res, err + } + + response, _ := json.Marshal(map[string]interface{}{"upload_url": strUrl}) + + return events.APIGatewayV2HTTPResponse{ + StatusCode: http.StatusOK, + Body: string(response), + }, nil +} + +func main() { + lambda.Start(handler) +} diff --git a/pkg/mocks/repository/dynamoManager.go b/pkg/mocks/repository/dynamoManager.go new file mode 100644 index 0000000..e5a725a --- /dev/null +++ b/pkg/mocks/repository/dynamoManager.go @@ -0,0 +1,33 @@ +package repository + +import ( + "context" + + m "github.com/CollActionteam/collaction_backend/internal/models" + "github.com/CollActionteam/collaction_backend/utils" + "github.com/stretchr/testify/mock" +) + +type Dynamo struct { + mock.Mock +} + +func (d *Dynamo) GetAll() ([]m.CrowdactionData, error) { + args := d.Mock.Called() + return args.Get(0).([]m.CrowdactionData), args.Error(1) +} + +func (d *Dynamo) GetById(pk string, sk string) (*m.CrowdactionData, error) { + args := d.Mock.Called(pk, sk) + return args.Get(0).(*m.CrowdactionData), args.Error(1) +} + +func (d *Dynamo) GetByStatus(filterCond string, startFrom *utils.PrimaryKey) ([]m.CrowdactionData, error) { + args := d.Mock.Called(filterCond, startFrom) + return args.Get(0).([]m.CrowdactionData), args.Error(1) +} + +func (d *Dynamo) Register(ctx context.Context, payload m.CrowdactionData) (*m.CrowdactionData, error) { + d.Mock.Called(payload) + return &payload, nil +} diff --git a/pkg/mocks/repository/uploadProfilePictureManager.go b/pkg/mocks/repository/uploadProfilePictureManager.go new file mode 100644 index 0000000..f3098f4 --- /dev/null +++ b/pkg/mocks/repository/uploadProfilePictureManager.go @@ -0,0 +1,20 @@ +package repository + +import ( + "context" + + "github.com/stretchr/testify/mock" +) + +type ProfilePicture struct { + mock.Mock +} + +func (p *ProfilePicture) GetUploadUrl(ctx context.Context, ext string, userID string) (string, error) { + outputs := p.Mock.Called(ctx, ext, userID) + + uploadUrl := outputs.String(0) + err := outputs.Error(1) + + return uploadUrl, err +} diff --git a/pkg/repository/aws/crowdactionManager.go b/pkg/repository/aws/crowdactionManager.go new file mode 100644 index 0000000..27ea510 --- /dev/null +++ b/pkg/repository/aws/crowdactionManager.go @@ -0,0 +1,142 @@ +package aws + +import ( + "context" + "fmt" + "math/rand" + "time" + + "github.com/CollActionteam/collaction_backend/internal/constants" + m "github.com/CollActionteam/collaction_backend/internal/models" + "github.com/CollActionteam/collaction_backend/utils" + "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" + "github.com/aws/aws-sdk-go/service/dynamodb/expression" +) + +type Crowdaction interface { + GetAll() ([]m.CrowdactionData, error) + GetById(pk string, sk string) (*m.CrowdactionData, error) + GetByStatus(status string, startFrom *utils.PrimaryKey) ([]m.CrowdactionData, error) + Register(ctx context.Context, payload m.CrowdactionData) (*m.CrowdactionData, error) +} + +const ( + KeyDateStart = "date_start" + KeyDateEnd = "date_end" + KeyDateJoinBefore = "date_limit_join" +) + +const charset = "abcdefghijklmnopqrstuvwxyz0123456789" + +var seededRand *rand.Rand = rand.New( + rand.NewSource(time.Now().UnixNano())) + +type crowdaction struct { + dbClient *Dynamo +} + +func NewCrowdaction(dynamo *Dynamo) Crowdaction { + return &crowdaction{dbClient: dynamo} +} + +func StringWithCharset(length int, charset string) string { + b := make([]byte, length) + for i := range b { + b[i] = charset[seededRand.Intn(len(charset))] + } + return string(b) +} + +func RandomIDPrefix(length int) string { + return StringWithCharset(length, charset) +} + +func (s *crowdaction) GetById(pk string, sk string) (*m.CrowdactionData, error) { + item, err := s.dbClient.GetDBItem(constants.TableName, pk, sk) + + if item == nil || err != nil { + return nil, err + } + + var c m.CrowdactionData + err = dynamodbattribute.UnmarshalMap(item, &c) + + return &c, err +} + +func (s *crowdaction) GetAll() ([]m.CrowdactionData, error) { + crowdactions := []m.CrowdactionData{} // crowdactions array + var filterCond = expression.Name(utils.PartitionKey).Equal(expression.Value(utils.PKCrowdaction)) + + item, err := s.dbClient.Scan(constants.TableName, filterCond) + if item == nil || err != nil { + return nil, err + } + + for _, itemIterator := range item { + var crowdaction m.CrowdactionData + err := dynamodbattribute.UnmarshalMap(itemIterator, &crowdaction) + + if err == nil { + crowdactions = append(crowdactions, crowdaction) + } + } + + if len(item) != len(crowdactions) { + err = fmt.Errorf("error unmarshallaing %d items", len(item)-len(crowdactions)) + } + + return crowdactions, err +} + +func (s *crowdaction) GetByStatus(status string, startFrom *utils.PrimaryKey) ([]m.CrowdactionData, error) { + crowdactions := []m.CrowdactionData{} + var filterCond expression.ConditionBuilder + + switch status { + case "joinable": + filterCond = expression.Name(KeyDateJoinBefore).GreaterThan(expression.Value(utils.GetDateStringNow())) + case "active": + filterCond = expression.Name(KeyDateStart).LessThanEqual(expression.Value(utils.GetDateStringNow())) + case "ended": + filterCond = expression.Name(KeyDateEnd).LessThanEqual(expression.Value(utils.GetDateStringNow())) + default: + } + + items, err := s.dbClient.Query(constants.TableName, filterCond, startFrom) + + if items == nil || err != nil { + return nil, err + } + + for _, itemIterator := range items { + var crowdaction m.CrowdactionData + err := dynamodbattribute.UnmarshalMap(itemIterator, &crowdaction) + + if err == nil { + crowdactions = append(crowdactions, crowdaction) + } + } + + if len(items) != len(crowdactions) { + err = fmt.Errorf("error unmarshelling %d items", len(items)-len(crowdactions)) + } + + return crowdactions, err +} + +func (s *crowdaction) Register(ctx context.Context, payload m.CrowdactionData) (*m.CrowdactionData, error) { + var response m.CrowdactionData + generatedID := RandomIDPrefix(8) + pk := utils.PKCrowdaction + sk := payload.Category + "#" + payload.Subcategory + "#" + generatedID + payload.CrowdactionID = sk + response = payload + + err := s.dbClient.PutDBItem(constants.TableName, pk, sk, payload) + + if err != nil { + return &response, nil + } + return &response, err +} diff --git a/pkg/repository/aws/dynamo.go b/pkg/repository/aws/dynamo.go index d915d43..628b591 100644 --- a/pkg/repository/aws/dynamo.go +++ b/pkg/repository/aws/dynamo.go @@ -4,11 +4,13 @@ import ( "fmt" "github.com/CollActionteam/collaction_backend/internal/constants" + "github.com/CollActionteam/collaction_backend/utils" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" + "github.com/aws/aws-sdk-go/service/dynamodb/expression" ) const ( @@ -90,6 +92,46 @@ func (s *Dynamo) GetDBItem(tableName string, pk string, sk string) (map[string]* return result.Item, nil } +func (s *Dynamo) Scan(tableName string, filterCond expression.ConditionBuilder) ([]map[string]*dynamodb.AttributeValue, error) { + expr, _ := expression.NewBuilder().WithFilter(filterCond).Build() + + result, err := s.dbClient.Scan(&dynamodb.ScanInput{ + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + FilterExpression: expr.Filter(), + ProjectionExpression: expr.Projection(), + TableName: aws.String(tableName), + }) + + return result.Items, err +} + +func (s *Dynamo) Query(tableName string, filterCond expression.ConditionBuilder, startFrom *utils.PrimaryKey) ([]map[string]*dynamodb.AttributeValue, error) { + keyCond := expression.Key(utils.PartitionKey).Equal(expression.Value(utils.PKCrowdaction)) + expr, _ := expression.NewBuilder().WithKeyCondition(keyCond).WithFilter(filterCond).Build() + + var exclusiveStartKey utils.PrimaryKey + + if startFrom != nil { + exclusiveStartKey = *startFrom + } + + // here filter does have a reference + fmt.Println("Query expr", expr.Names(), expr.Filter(), expr.Values()) + + result, err := s.dbClient.Query(&dynamodb.QueryInput{ + Limit: aws.Int64(utils.CrowdactionsPageLength), + ExclusiveStartKey: exclusiveStartKey, + TableName: aws.String(tableName), + ExpressionAttributeNames: expr.Names(), + ExpressionAttributeValues: expr.Values(), + KeyConditionExpression: expr.KeyCondition(), + FilterExpression: expr.Filter(), + }) + + return result.Items, err +} + func (s *Dynamo) PutDBItem(tableName string, pk string, sk string, record interface{}) error { av, err := dynamodbattribute.MarshalMap(record) if err != nil { diff --git a/pkg/repository/aws/uploadProfilePictureManager.go b/pkg/repository/aws/uploadProfilePictureManager.go new file mode 100644 index 0000000..11616bd --- /dev/null +++ b/pkg/repository/aws/uploadProfilePictureManager.go @@ -0,0 +1,38 @@ +package aws + +import ( + "context" + "os" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" +) + +type ProfilePicture struct { + Client *s3.S3 +} + +func NewProfilePicture(sess *session.Session) *ProfilePicture { + return &ProfilePicture{Client: s3.New(sess)} +} + +func (p *ProfilePicture) GetUploadUrl(ctx context.Context, ext string, userID string) (string, error) { + var ( + bucket = os.Getenv("BUCKET") + filekey = userID + "." + ext + ) + + reqs, _ := p.Client.PutObjectRequest(&s3.PutObjectInput{ + Bucket: aws.String(bucket), + Key: aws.String(filekey), + }) + + str, err := reqs.Presign(15 * time.Minute) + + if err != nil { + return "", err + } + return str, nil +} diff --git a/scripts/badges/api.py b/scripts/badges/api.py new file mode 100644 index 0000000..b256861 --- /dev/null +++ b/scripts/badges/api.py @@ -0,0 +1,69 @@ +""" + Script for calling API gateway endpoints + if the API is not available, then try to use + boto3 to put records in DynamoDB +""" +from create import test +import string +import random +import json + +create = test() # init test class + + +def id_generator(size=6, chars=string.ascii_letters + string.digits): + return ''.join(random.choice(chars) for _ in range(size)) + + +# Goal! +# 5 users +# 1 crowdaction +# 5 different participations +def main(): + commitment_arr = [ + ["no-cheese"], + ["no-dairy", "no-cheese"], + ["no-beef"], + ["pescatarian", "no-beef"], + ["vegetarian", "pescatarian", "no-beef"], + ["vegan", "vegetarian", "pescatarian", "no-beef", "no-dairy", "no-cheese"], + ] + + usr_list = [] + + """create users""" + for i in range(0, 5): + usr_id = id_generator(28) + randomNum = random.randrange(6) + + usr_obj = { + "id": usr_id, + "commitment": commitment_arr[randomNum] + } + + usr_list.append(usr_obj) + + res = create.profile(usr_id) + + print('user id:', usr_id, 'res:', res) + + print() + + """create crowdaction""" + category = id_generator(8) + subcategory = id_generator(8) + cid = create.crowdaction(category, subcategory) + + print() + + """create participation""" + for n in range(0, len(usr_list)): + res = create.participation( + cid, usr_list[n]['id'], usr_list[n]['commitment']) + + print('user id:', usr_list[n]['id'], 'commitment:', + usr_list[n]['commitment'], 'res:', res) + + +if __name__ == '__main__': + main() diff --git a/scripts/badges/create.py b/scripts/badges/create.py new file mode 100644 index 0000000..00d95c4 --- /dev/null +++ b/scripts/badges/create.py @@ -0,0 +1,176 @@ +""" + Create endpoints file +""" +from datetime import datetime +from datetime import timedelta +import json +import boto3 +import requests + +l_client = boto3.client('lambda') + + +class test(): + def profile(self, usr_id): + self.usr_id = usr_id + usr_payload = { + "body": "{\"displayname\": \"Timothy\", \"city\": \"New York\", \"country\": \"USA\", \"bio\": \"Hi, I'm Timothy\"}", + "pathParameters": { + "userID": usr_id + }, + "requestContext": { + "authorizer": { + "jwt": { + "claims": { + "name": "Timothy", + "user_id": usr_id, + "sub": usr_id, + "phone_number": "+31612345678" + } + } + }, + "http": { + "method": "POST" + } + } + } + + res = l_client.invoke( + FunctionName='collaction-dev-edreinoso-ProfileCRUDFunction-gLFLlRBye4eA', + InvocationType='RequestResponse', + Payload=bytes(json.dumps(usr_payload), encoding='utf8'), + ) + return res + + def crowdaction(self, category, subcategory): + self.category = category + self.subcategory = subcategory + + gmt_time = datetime.now() - timedelta(hours=2) # converting time to GMT + crowdacticon_date_end = gmt_time + timedelta(minutes=3) + crowdacticon_date_limit = gmt_time + timedelta(minutes=4) + + crowdaction_start_time = gmt_time.replace(microsecond=0) + crowdaction_expiry_time = crowdacticon_date_end.replace(microsecond=0) + crowdaction_join_limit = crowdacticon_date_limit.replace(microsecond=0) + + print(crowdaction_expiry_time, crowdaction_join_limit) + + cwr_payload = { + "title": "querico", + "description": "test1", + "category": category, + "subcategory": subcategory, + "location": "test1", + "date_end": str(crowdaction_expiry_time), + "date_start": str(crowdaction_start_time), + "date_limit_join": "2022-06-19", + # "date_limit_join": str(crowdaction_join_limit), # this should be tested + "password_join": "", + "images": { + "card": "hello", + "banner": "world" + }, + "badges": [20, 40, 60, 80], + "participation_count": 0, + "top_participants": [], + "commitment_options": [ + { + "description": "(in case you dont want to commit to 7/7 days a week)", + "id": "working-days-only", + "label": "5/7 days a week", + "points": 0 + }, + { + "description": "", + "id": "vegan", + "label": "Vegan", + "points": 20, + "requires": [ + { + "description": "", + "id": "vegetarian", + "label": "Vegetarian", + "points": 20, + "requires": [ + { + "description": "", + "id": "pescatarian", + "label": "Pescatarian", + "points": 5, + "requires": [ + { + "description": "", + "id": "no-beef", + "label": "No Beef", + "points": 5 + } + ] + } + ] + }, + { + "description": "", + "id": "no-dairy", + "label": "No Dairy", + "points": 10, + "requires": [ + { + "description": "", + "id": "no-cheese", + "label": "No Cheese", + "points": 10 + } + ] + } + ] + } + ], + } + + uri = 'https://5y310ujdy1.execute-api.eu-central-1.amazonaws.com/dev/cms/crowdactions' + + res = requests.post(uri, json=cwr_payload) + crowdaction = res.json() + cid = crowdaction['data']['crowdactionID'] + return cid + + def participation(self, cid, usr_id, commitment): + self.cid = cid + self.usr_id = usr_id + self.commitment = commitment + + # dynamic commitments + body = { + "password": "myEvent-myCompany2021", + "commitments": commitment + } + + prt_payload = { + "body": json.dumps(body), + "pathParameters": { + "crowdactionID": cid + }, + "requestContext": { + "authorizer": { + "jwt": { + "claims": { + "name": "Hello World", + "user_id": usr_id, + "sub": usr_id, + "phone_number": "+31612345678" + } + } + }, + "http": { + "method": "POST" + } + } + } + + res = l_client.invoke( + FunctionName='collaction-dev-edreinoso-ParticipationFunction-zc9MRMJVkIjO', + InvocationType='RequestResponse', + Payload=bytes(json.dumps(prt_payload), encoding='utf8'), + ) + return res diff --git a/scripts/badges/delete_rules.py b/scripts/badges/delete_rules.py new file mode 100644 index 0000000..25fc197 --- /dev/null +++ b/scripts/badges/delete_rules.py @@ -0,0 +1,10 @@ +import boto3 + +client = boto3.client('events') + +response = client.list_rules() + +for rules in response['Rules']: + client.delete_rule( + Name=rules['Name'] + ) diff --git a/template.yaml b/template.yaml index 81eae6f..9b76c4c 100644 --- a/template.yaml +++ b/template.yaml @@ -1,9 +1,8 @@ -AWSTemplateFormatVersion: '2010-09-09' +AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > CollAction backend # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst - Globals: Function: Timeout: 10 @@ -38,13 +37,12 @@ Parameters: Type: String NoEcho: true Description: "ARN of certificate for CloudFront in region us-east-1 (Only for custom domain)" - + Conditions: shouldUseCustomDomainNames: !Not [!Equals [!Ref DomainParameter, ""]] Resources: - - DnsRecords: + DnsRecords: Type: AWS::Route53::RecordSetGroup Condition: shouldUseCustomDomainNames Properties: @@ -85,14 +83,14 @@ Resources: StaticContentDistribution: Type: AWS::CloudFront::Distribution Condition: shouldUseCustomDomainNames - Properties: + Properties: DistributionConfig: DefaultCacheBehavior: AllowedMethods: [HEAD, DELETE, POST, GET, OPTIONS, PUT, PATCH] TargetOriginId: StaticBucketOrigin ViewerProtocolPolicy: redirect-to-https ForwardedValues: - QueryString: 'false' + QueryString: "false" Cookies: Forward: none Enabled: true @@ -102,7 +100,7 @@ Resources: - Id: StaticBucketOrigin DomainName: !Sub ${StaticHostingBucket}.s3.${ AWS::Region }.amazonaws.com S3OriginConfig: - OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/${CloudFrontOriginIdentity}' + OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${CloudFrontOriginIdentity}" ViewerCertificate: AcmCertificateArn: !Ref AcmCertificateArnParameter MinimumProtocolVersion: TLSv1.1_2016 @@ -111,21 +109,21 @@ Resources: HttpApiDomainName: Type: AWS::ApiGatewayV2::DomainName Condition: shouldUseCustomDomainNames - Properties: + Properties: DomainName: !Sub "api${SubdomainSuffixParameter}.${DomainParameter}" - DomainNameConfigurations: + DomainNameConfigurations: - EndpointType: REGIONAL CertificateArn: !Ref Certificate HttpApiMapping: Type: AWS::ApiGatewayV2::ApiMapping Condition: shouldUseCustomDomainNames - Properties: + Properties: ApiMappingKey: "" DomainName: !Sub "api${SubdomainSuffixParameter}.${DomainParameter}" ApiId: !Ref HttpApi Stage: !Ref HttpApi.Stage - DependsOn: + DependsOn: - HttpApiDomainName - HttpApi @@ -152,7 +150,7 @@ Resources: CorsConfiguration: AllowMethods: [GET] AllowOrigins: [http://localhost:8080] - + EmailContactFunction: Type: AWS::Serverless::Function Properties: @@ -169,19 +167,19 @@ Resources: Auth: Authorizer: "NONE" Policies: - - Version: '2012-10-17' + - Version: "2012-10-17" Statement: - Effect: Allow Action: - - 'ses:SendEmail' - - 'ses:SendRawEmail' - - 'ssm:GetParameter' - Resource: '*' + - "ses:SendEmail" + - "ses:SendRawEmail" + - "ssm:GetParameter" + Resource: "*" ProfilePictureUploadBucket: - Type: 'AWS::S3::Bucket' + Type: "AWS::S3::Bucket" StaticHostingBucket: - Type: 'AWS::S3::Bucket' + Type: "AWS::S3::Bucket" Properties: AccessControl: Private WebsiteConfiguration: @@ -197,7 +195,7 @@ Resources: UploadProfilePictureFunction: Type: AWS::Serverless::Function Properties: - CodeUri: upload-profile-picture/ + CodeUri: pkg/handler/aws/uploadProfilePicture Handler: upload-profile-picture Runtime: go1.x Environment: @@ -205,10 +203,10 @@ Resources: BUCKET: !Ref ProfilePictureUploadBucket Policies: - Statement: - - Effect: Allow - Action: - - s3:PutObject* - Resource: '*' + - Effect: Allow + Action: + - s3:PutObject* + Resource: "*" Events: ProfilePictureUpload: Type: HttpApi @@ -218,7 +216,7 @@ Resources: ApiId: !Ref HttpApi ProcessProfilePictureFunction: - Type: 'AWS::Serverless::Function' + Type: "AWS::Serverless::Function" Properties: CodeUri: process-profile-picture/ Handler: process-profile-picture @@ -228,17 +226,18 @@ Resources: # Beware of recursive execution! Double check referenced buckets! OUTPUT_BUCKET_NAME: !Ref StaticHostingBucket KEY_PREIFX: profile-pictures/ - CLOUDFRONT_DISTRIBUTION: !If [shouldUseCustomDomainNames, !Ref StaticContentDistribution, ""] + CLOUDFRONT_DISTRIBUTION: + !If [shouldUseCustomDomainNames, !Ref StaticContentDistribution, ""] Policies: - Statement: - - Effect: Allow - Action: - - s3:GetObject* - - s3:PutObject* - - s3:DeleteObject* - - rekognition:DetectModerationLabels - - cloudfront:CreateInvalidation - Resource: '*' + - Effect: Allow + Action: + - s3:GetObject* + - s3:PutObject* + - s3:DeleteObject* + - rekognition:DetectModerationLabels + - cloudfront:CreateInvalidation + Resource: "*" Events: S3Event: Type: S3 @@ -265,10 +264,10 @@ Resources: GlobalSecondaryIndexes: - IndexName: "invertedIndex" KeySchema: - - AttributeName: "sk" - KeyType: "HASH" - - AttributeName: "pk" - KeyType: "RANGE" + - AttributeName: "sk" + KeyType: "HASH" + - AttributeName: "pk" + KeyType: "RANGE" Projection: ProjectionType: "ALL" # Data duplication is less costly than additional per primary key lookups (?) ProvisionedThroughput: @@ -278,10 +277,19 @@ Resources: CrowdactionFunction: Type: AWS::Serverless::Function Properties: - CodeUri: crowdaction/ + CodeUri: pkg/handler/aws/crowdaction Handler: crowdaction Runtime: go1.x Events: + # TODO feature wip (cms) + CreateCrowdaction: + Type: HttpApi + Properties: + Path: /cms/crowdactions + Method: post + ApiId: !Ref HttpApi + Auth: + Authorizer: "NONE" FetchCrowdaction: Type: HttpApi Properties: @@ -303,8 +311,7 @@ Resources: TABLE_NAME: !Ref SingleTable Policies: - DynamoDBCrudPolicy: - TableName: - !Ref SingleTable + TableName: !Ref SingleTable ParticipationQueue: Type: AWS::SQS::Queue @@ -327,8 +334,7 @@ Resources: MaximumBatchingWindowInSeconds: 300 #5min Policies: - DynamoDBCrudPolicy: - TableName: - !Ref SingleTable + TableName: !Ref SingleTable ParticipationFunction: Type: AWS::Serverless::Function @@ -350,15 +356,14 @@ Resources: ApiId: !Ref HttpApi Policies: - DynamoDBCrudPolicy: - TableName: - !Ref SingleTable + TableName: !Ref SingleTable - Statement: - - Sid: ParticipationQueuePutRecordPolicy - Effect: Allow - Action: - - sqs:SendMessage - Resource: !GetAtt ParticipationQueue.Arn - + - Sid: ParticipationQueuePutRecordPolicy + Effect: Allow + Action: + - sqs:SendMessage + Resource: !GetAtt ParticipationQueue.Arn + ProfileCRUDFunction: Type: AWS::Serverless::Function Properties: @@ -377,8 +382,7 @@ Resources: PROFILE_TABLE: !Ref ProfileTable Policies: - DynamoDBCrudPolicy: - TableName: - !Ref ProfileTable + TableName: !Ref ProfileTable # TODO use table SingleTabel instead ProfileTable: @@ -401,5 +405,5 @@ Outputs: Description: "CloudFront distribution endpoint URL for static files" Value: !Sub "https://static${SubdomainSuffixParameter}.${DomainParameter}/" TableName: - Value: !Ref SingleTable - Description: Table name of the newly created DynamoDB table \ No newline at end of file + Value: !Ref SingleTable + Description: Table name of the newly created DynamoDB table