Skip to content
This repository was archived by the owner on Jan 1, 2026. It is now read-only.
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
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
* text=auto
*.sql linguist-language=SQL linguist-detectable=true
*.test.sql -linguist-language
35 changes: 35 additions & 0 deletions .github/workflows/create-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
name: Create Release

permissions:
contents: write

on:
push:
branches:
- 'main'
paths:
- 'database/**/*.sql'

jobs:
suggest-bump:
uses: ./.github/workflows/suggest-version-bump.yml

create-release:
runs-on: ubuntu-latest
needs: suggest-bump
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Create release draft
uses: actions/create-release@v1
with:
tag_name: ${{ needs.suggest-bump.outputs.next_version }}
release_name: ${{ needs.suggest-bump.outputs.next_version }}
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35 changes: 35 additions & 0 deletions .github/workflows/get-pr-labels.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
name: Get PR Labels

permissions:
contents: read
statuses: read
pull-requests: read

on:
workflow_call:
outputs:
labels:
description: 'Labels on the pull request'
value: ${{ jobs.get-labels.outputs.labels }}

jobs:
get-labels:
name: Get PR Labels
runs-on: ubuntu-latest
outputs:
labels: ${{ steps.labels.outputs.labels }}

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Get PR labels
id: labels
run: |
LABELS=$(gh pr view "${{ github.event.pull_request.number }}" --json labels --jq '[.labels[].name] | join(" ")')
echo "labels=$LABELS" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ github.token }}
83 changes: 83 additions & 0 deletions .github/workflows/suggest-version-bump.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
name: Suggest Version Bump

permissions:
contents: read
pull-requests: read
statuses: read

on:
pull_request:
branches:
- main
paths:
- 'database/**/*.sql'
workflow_call:
outputs:
next_version:
description: 'The next suggested version'
value: ${{ jobs.suggest-bump.outputs.next_version }}

jobs:
get-labels:
name: Get PR Labels
uses: ./.github/workflows/get-pr-labels.yml

suggest-bump:
name: Suggest Version Bump
needs: get-labels
runs-on: ubuntu-latest
outputs:
next_version: ${{ steps.next_version.outputs.next_version }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Determine bump type
id: bump
run: |
LABELS="${{ needs.get-labels.outputs.labels }}"
BUMP="patch"
echo "$LABELS" | grep -q 'type: feature' && BUMP="minor"
echo "$LABELS" | grep -q 'type: security' && BUMP="minor"
echo "$LABELS" | grep -q 'type: breaking' && BUMP="major"
echo "bump=$BUMP" >> "$GITHUB_OUTPUT"

- name: Get latest tag
id: latest_tag
run: |
TAG=$(git tag --list 'v*' --sort=-v:refname | head -n1)
echo "tag=$TAG" >> "$GITHUB_OUTPUT"

- name: Calculate next version
id: next_version
run: |
TAG="${{ steps.latest_tag.outputs.tag }}"
BUMP="${{ steps.bump.outputs.bump }}"
if [ -z "$TAG" ]; then
TAG="v0.0.0"
fi
VERSION=$(echo "$TAG" | sed -E 's/^v([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
PRERELEASE=$(echo "$TAG" | sed -nE 's/^v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?$/\1/p')
IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
case "$BUMP" in
major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;;
minor) MINOR=$((MINOR + 1)); PATCH=0 ;;
patch) PATCH=$((PATCH + 1)) ;;
esac
if [ -n "$PRERELEASE" ]; then
NEXT_VERSION="v${MAJOR}.${MINOR}.${PATCH}${PRERELEASE}"
else
NEXT_VERSION="v${MAJOR}.${MINOR}.${PATCH}"
fi
echo "next_version=$NEXT_VERSION" >> "$GITHUB_OUTPUT"

- name: Report summary
run: |
{
echo "### 🚀 Suggested Version Bump: **${{ steps.bump.outputs.bump }}**"
echo "#### Latest tag: \`${{ steps.latest_tag.outputs.tag }}\`"
echo "#### Next version: \`${{ steps.next_version.outputs.next_version }}\`"
} >> "$GITHUB_STEP_SUMMARY"
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@
}
],
"triggerTaskOnSave.tasks": {
"Lint SQL File": ["**/*.sql"]
"Lint SQL File": ["database/**/*.sql"]
}
}
27 changes: 27 additions & 0 deletions database/functions/authenticate_user.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-- Authenticate a user and log the result
CREATE OR REPLACE FUNCTION authenticate_user(
p_username VARCHAR,
p_password_hash VARCHAR,
p_ip_address INET,
p_user_agent TEXT
) RETURNS BOOLEAN AS $$
DECLARE
v_user_id INTEGER;
BEGIN
-- Attempt to find the user by username and hashed password
SELECT user_id INTO v_user_id
FROM users
WHERE username = p_username AND password_hash = p_password_hash
LIMIT 1;

IF v_user_id IS NOT NULL THEN
-- Successful login: handle and log
CALL handle_successful_login(v_user_id, p_ip_address, p_user_agent);
RETURN TRUE;
ELSE
-- Failed login: log with NULL user_id
CALL log_login_attempt(NULL, p_ip_address, p_user_agent, FALSE);
RETURN FALSE;
END IF;
END;
$$ LANGUAGE plpgsql;
8 changes: 8 additions & 0 deletions database/functions/disable_2fa.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- Disable 2FA for a user
CREATE OR REPLACE FUNCTION disable_2fa(p_user_id INTEGER) RETURNS VOID AS $$
BEGIN
UPDATE user_authentication_methods
SET is_enabled = FALSE, updated_at = NOW()
WHERE user_id = p_user_id;
END;
$$ LANGUAGE plpgsql;
9 changes: 9 additions & 0 deletions database/functions/get_user_2fa_secret.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- Get the user's authentication methods secret
CREATE OR REPLACE FUNCTION get_user_authentication_method_secret(p_user_id INTEGER) RETURNS TABLE (method TEXT, secret TEXT) AS $$
BEGIN
RETURN QUERY
SELECT authentication_method, user_authentication_method_secret
FROM user_authentication_methods
WHERE user_id = p_user_id AND is_enabled = TRUE;
END;
$$ LANGUAGE plpgsql;
12 changes: 12 additions & 0 deletions database/functions/is_2fa_enabled.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- Check if the user has 2FA enabled
CREATE OR REPLACE FUNCTION is_2fa_enabled(p_user_id INTEGER) RETURNS BOOLEAN AS $$
DECLARE
v_enabled BOOLEAN;
BEGIN
SELECT is_enabled INTO v_enabled
FROM user_authentication_methods
WHERE user_id = p_user_id;

RETURN COALESCE(v_enabled, FALSE);
END;
$$ LANGUAGE plpgsql;
90 changes: 0 additions & 90 deletions database/procedures/authentication.sql

This file was deleted.

16 changes: 16 additions & 0 deletions database/procedures/handle_successful_login.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- Update last_login and log successful attempt
CREATE OR REPLACE PROCEDURE handle_successful_login(
p_user_id INTEGER,
p_ip_address INET,
p_user_agent TEXT
)
AS $$
BEGIN
-- Update login timestamp
UPDATE users SET last_login_at = NOW(), updated_at = NOW()
WHERE user_id = p_user_id;

-- Log success
CALL log_login_attempt(p_user_id, p_ip_address, p_user_agent, TRUE);
END;
$$ LANGUAGE plpgsql;
13 changes: 13 additions & 0 deletions database/procedures/log_login_attempt.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- Log a login attempt (used in both success and failure cases)
CREATE OR REPLACE PROCEDURE log_login_attempt(
p_user_id INTEGER,
p_ip_address INET,
p_user_agent TEXT,
p_success BOOLEAN
)
LANGUAGE plpgsql AS $$
BEGIN
INSERT INTO login_attempts (user_id, ip_address, user_agent, success)
VALUES (p_user_id, p_ip_address, p_user_agent, p_success);
END;
$$;
46 changes: 46 additions & 0 deletions database/procedures/register_user.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
-- Register a new user
CREATE OR REPLACE PROCEDURE register_user(
p_email_encrypted TEXT,
p_email_hash TEXT,
p_username VARCHAR,
p_password_hash VARCHAR,
p_phone_encrypted TEXT,
p_phone_hash TEXT,
p_preferred_language VARCHAR
)
LANGUAGE plpgsql AS $$
BEGIN
BEGIN
INSERT INTO users (
email_encrypted,
email_hash,
username,
password_hash,
phone_encrypted,
phone_hash,
preferred_language
)
VALUES (
p_email_encrypted,
p_email_hash,
p_username,
p_password_hash,
p_phone_encrypted,
p_phone_hash,
p_preferred_language
);
EXCEPTION
WHEN unique_violation THEN
-- Identify the constraint that caused the error
IF SQLERRM LIKE '%username%' THEN
RAISE EXCEPTION 'Username % already exists', p_username;
ELSIF SQLERRM LIKE '%email_hash%' THEN
RAISE EXCEPTION 'Email address already exists';
ELSIF SQLERRM LIKE '%phone_hash%' THEN
RAISE EXCEPTION 'Phone number already exists';
ELSE
RAISE;
END IF;
END;
END;
$$;
Loading