Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Delete comments over API #72

Merged
merged 6 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ typings/
# dotenv environment variables file
.env
.env.test
docker-compose.env

# parcel-bundler cache (https://parceljs.org/)
.cache
Expand Down
21 changes: 21 additions & 0 deletions AzureCommentsApiDelete/function.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"delete"
],
"route": "comments/{commentId}"
},
{
"type": "http",
"direction": "out",
"name": "res"
}
],
"scriptFile": "../build/AzureCommentsApiDelete/index.js"
}

67 changes: 67 additions & 0 deletions AzureCommentsApiDelete/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import _ from 'lodash'
import { appContext } from '../src/azure'
import { AzureFunction, Context, HttpRequest } from '@azure/functions'
import { CommentService } from '../src/shared/comments/comment.service'
import { AuthService, HmacValidationResult } from '../src/shared/auth/auth.service'

const commentsApi: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
context.log.info('HTTP trigger function processing a request to delete a comment')

const app = await appContext()
const authService = app.get(AuthService)
const commentService = app.get(CommentService)
const timestamp = req.headers['x-timestamp'] as string
const providedSignature = req.headers['x-signature'] as string
const accountId = req.headers['x-account-id'] as string
const commentId = req.params.commentId

if (!timestamp || !providedSignature || !accountId) {
context.log.warn('Missing auth headers :(')
context.res = {
status: 400,
}

return
}

const forDate = new Date()

const signatureVerificationResult = await authService.isHmacSignatureValid(
'DELETE',
`/comments/${commentId}`,
300,
accountId,
timestamp,
providedSignature,
forDate
)

// Verify the request is not too old (e.g., 5 minutes)
if (signatureVerificationResult === HmacValidationResult.REQUEST_TOO_OLD) {
const now = Math.floor(forDate.getTime() / 1000)
context.log.warn(`Request too old: ${Math.abs(now - parseInt(timestamp))}s`)
context.res = {
status: 429,
}

return
}

if (signatureVerificationResult !== HmacValidationResult.OK) {
context.log.warn(`Invalid signature: ${signatureVerificationResult}`)
context.res = {
status: 403,
}

return
}

await commentService.deleteSingleById(commentId)
context.log.info(`Deleted comment ${commentId}`)

context.res = {
status: 201,
}
}

export default commentsApi
8 changes: 7 additions & 1 deletion AzureCommentsApiPost/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,20 @@ const commentsApi: AzureFunction = async function (context: Context, req: HttpRe
// no email notification for spam
return
}
const commentEntity = await commentService.findById(account, result as string)
if (!commentEntity) {
context.log.warn(`Comment ${result} not found - cannot send email notification`)

return
}

// send email notification as another Azure function triggered by ServiceBus
try {
const emailSettings = await accountService.emailSettingsFor(account)
if (emailSettings?.notifyOnComments) {
// notify
context.log('Scheduling an email notification about a new comment')
jobQueue.publish({ account, comment })
jobQueue.publish({ account, comment: commentEntity })
}
} catch (oops) {
context.log.warn(`Trouble scheduling email notification: ${(oops as Error)?.message}`)
Expand Down
6 changes: 5 additions & 1 deletion AzureCommentsNotify/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ const commentEventHandler: AzureFunction = async function (context: Context, eve
const configService = app.get(ConfigService)
const emailService = app.get(EmailService)
const { account, comment } = eventBody
const email = emailService.notifyOnSingleComment(comment, `${configService.adminUrl()}/dashboard`)
const commentEntity = {
...comment,
postedAt: new Date(comment.postedAt),
}
const email = emailService.notifyOnSingleComment(commentEntity, `${configService.adminUrl()}/dashboard`)
context.log(`Notifying ${account.email} on comment posted`)

await sendMailService.send(configService.mailgunSender(), account.email, email.subject, email.html, email.text)
Expand Down
8 changes: 8 additions & 0 deletions asyncApi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,21 @@ components:
comment:
type: object
required:
- id
- postUrl
- postedAt
- text
- author
additionalProperties: false
properties:
id:
type: string
format: guid
postUrl:
type: string
postedAt:
type: string
format: datetime
postTitle:
type: string
text:
Expand Down
4 changes: 4 additions & 0 deletions docker-compose.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Set these and align with what you put in .env
POSTGRES_DB=
POSTGRES_USER=
POSTGRES_PASSWORD=
7 changes: 4 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ services:

db:
image: postgres:14-alpine
hostname: db
ports:
- "5432:5432"
environment:
POSTGRES_DB: master
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
networks:
- my_network
volumes:
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
},
"scripts": {
"generate": "json2ts -i schemas/**/* -o generated/ && ts-auto-guard --paths ./generated/*.ts --export-all && ts-auto-guard --paths ./src/**/*.interface.ts --export-all",
"build": "tsc -p tsconfig.json",
"watch": "tsc -p tsconfig.json -w",
"api": "ts-node src/api.ts",
"web": "ts-node src/web.ts",
"cli": "ts-node src/cli.ts",
"sql": "pgtyped -c config.json",
"test": "jest",
"build:sass": "node-sass assets/scss -o public/css --output-style compressed",
"prebuild": "yarn generate",
"build": "tsc -p tsconfig.json",
"postbuild": "copyfiles \"./src/**/*.hbs\" \"./public/**/*\" \"./public/assets/**/*\" build",
"clean": "rm -rf build && rm -rf generated",
"ts-node": "ts-node",
Expand All @@ -25,7 +26,6 @@
"prestart": "node ./build/src/cli.js db:migrate",
"start": "node ./build/src/web.js",
"start-api": "node ./build/src/api.js",
"watch": "tsc -p tsconfig.json -w",
"func": "func start --verbose",
"zip:app": "zip -q -r webapp.zip . -x@.appserviceignore -x .appserviceignore",
"zip:api": "zip -q -r funcapp.zip . -x@.funcignore -x .funcignore"
Expand Down
111 changes: 111 additions & 0 deletions scripts/rmcomment.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/bin/bash

# Configuration
missing_vars=()

if [ -z "${API_SECRET_KEY}" ]; then
missing_vars+=("API_SECRET_KEY")
fi

if [ -z "${API_BASE_URL}" ]; then
missing_vars+=("API_BASE_URL")
fi

if [ -z "${ACCOUNT_ID}" ]; then
missing_vars+=("ACCOUNT_ID")
fi

if [ ${#missing_vars[@]} -ne 0 ]; then
echo "Error: Required environment variables are not set:"
printf ' %s\n' "${missing_vars[@]}"
echo ""
echo "Please set them first:"
echo " export API_SECRET_KEY='your-secret-key-here'"
echo " export API_BASE_URL='https://your-function-app.azurewebsites.net'"
echo " export ACCOUNT_ID='your-account-id-here'"
exit 1
fi

# Function to create HMAC signature
create_signature() {
local method="$1"
local url_path="$2"
local timestamp="$3"

# Create string to sign (same format as server)
local string_to_sign="${method}\n${url_path}\n${timestamp}"

# Create HMAC signature using sha256
echo -en "$string_to_sign" | openssl dgst -sha256 -hmac "$API_SECRET_KEY" -hex | sed 's/^.* //'
}

# Function to delete an item
delete_item() {
local item_id="$1"

if [ -z "$item_id" ]; then
echo "Error: Item ID is required"
echo "Usage: $0 delete <item-id>"
exit 1
fi

# Create timestamp (seconds since epoch)
local timestamp=$(date +%s)

# Construct the URL path (must match what's used in signature)
local url_path="/comments/${item_id}"

# Generate signature
local signature=$(create_signature "DELETE" "$url_path" "$timestamp")

# Make the DELETE request using curl
response=$(curl -s -w "\n%{http_code}" \
-X DELETE \
-H "x-timestamp: ${timestamp}" \
-H "x-signature: ${signature}" \
-H "x-account-id: ${ACCOUNT_ID}" \
"${API_BASE_URL}${url_path}")

# Extract status code and body
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | sed '$d')

# Handle response
if [ "$http_code" -eq 200 ]; then
echo "Successfully deleted item ${item_id}"
[ ! -z "$body" ] && echo "Response: $body"
else
echo "Error deleting item ${item_id}"
echo "Status code: ${http_code}"
[ ! -z "$body" ] && echo "Error: $body"
exit 1
fi
}

# Function to display help
show_help() {
echo "Usage: $0 <command> [options]"
echo ""
echo "Commands:"
echo " delete <item-id> Delete an item by ID"
echo " help Show this help message"
echo ""
echo "Example:"
echo " $0 delete 12345"
}

# Main script logic
case "$1" in
delete)
delete_item "$2"
;;
help|--help|-h)
show_help
;;
*)
echo "Error: Unknown command '$1'"
echo ""
show_help
exit 1
;;
esac
29 changes: 13 additions & 16 deletions src/api/api.queries.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,34 @@
/** Types generated for queries found in "src/api/api.sql" */
import { PreparedQuery } from '@pgtyped/runtime'
import { PreparedQuery } from '@pgtyped/runtime';

/** 'LoginFromToken' parameters type */
export interface ILoginFromTokenParams {
token: string | null | void
token?: string | null | void;
}

/** 'LoginFromToken' return type */
export interface ILoginFromTokenResult {
created_at: Date
email: string
id: string
password: Buffer
username: string
created_at: Date;
email: string;
id: string;
password: Buffer;
username: string;
}

/** 'LoginFromToken' query type */
export interface ILoginFromTokenQuery {
params: ILoginFromTokenParams
result: ILoginFromTokenResult
params: ILoginFromTokenParams;
result: ILoginFromTokenResult;
}

const loginFromTokenIR: any = {
usedParamSet: { token: true },
params: [{ name: 'token', required: false, transform: { type: 'scalar' }, locs: [{ a: 87, b: 92 }] }],
statement:
'SELECT DISTINCT a.* FROM accounts a JOIN tokens t ON (a.id=t.account_id) WHERE t.token=:token AND t.revoked_at IS NULL',
}
const loginFromTokenIR: any = {"usedParamSet":{"token":true},"params":[{"name":"token","required":false,"transform":{"type":"scalar"},"locs":[{"a":87,"b":92}]}],"statement":"SELECT DISTINCT a.* FROM accounts a JOIN tokens t ON (a.id=t.account_id) WHERE t.token=:token AND t.revoked_at IS NULL"};

/**
* Query generated from SQL:
* ```
* SELECT DISTINCT a.* FROM accounts a JOIN tokens t ON (a.id=t.account_id) WHERE t.token=:token AND t.revoked_at IS NULL
* ```
*/
export const loginFromToken = new PreparedQuery<ILoginFromTokenParams, ILoginFromTokenResult>(loginFromTokenIR)
export const loginFromToken = new PreparedQuery<ILoginFromTokenParams,ILoginFromTokenResult>(loginFromTokenIR);


Loading