Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
5db8f45
Add user passwords migration
Jul 9, 2025
7120691
Add users password to database when logging in
Jul 9, 2025
ba1e424
Send info to tmc that users password is stored in db
Jul 10, 2025
23d2578
Url and payload change to set user password
Jul 11, 2025
e261563
User authentication when logging in
Jul 14, 2025
ed671f1
Changed function name
Jul 15, 2025
3396bc9
Endpoint for changing password from tmc and little fixes
Jul 15, 2025
facc79a
Update password to database upon sign up
Jul 15, 2025
0c5f2d5
Small fixes
Jul 16, 2025
2e7bfc8
Try to auth user from database when logging in
Jul 17, 2025
a4e2f4c
Added test for creating an account and logging in
Jul 17, 2025
1ec83cd
Refactored log in function
Jul 17, 2025
47e770d
Auth and password change uses user_id instead of email
Jul 23, 2025
08ee405
User can change password with reset link
Jul 25, 2025
83a218b
Reset email in different languages and added authentication to emai d…
Aug 13, 2025
f9c95df
Merge remote-tracking branch 'origin/master' into move-passwords-from…
Aug 21, 2025
aa3ad19
Merge remote-tracking branch 'origin/master' into move-passwords-from…
Aug 21, 2025
2ab91f8
Check email from tmc if not stored in courses mooc
Aug 21, 2025
33f858b
Changed expires_at to 1 hour
Aug 21, 2025
b602c56
Addes reset password emails to seed
Aug 21, 2025
04a5bf9
Little changes and fixes
Aug 22, 2025
7c84a09
Little improvements
Aug 25, 2025
e2b5c9a
Merge remote-tracking branch 'origin/master' into move-passwords-from…
Aug 25, 2025
8d647bd
Redirect to login after inserting new password
Aug 25, 2025
5e4a6f3
Test for resetting password
Aug 25, 2025
1a683e0
Mock-up secrets
Aug 26, 2025
c4d5d1a
Refactoring
Aug 26, 2025
10f0fa6
Create account here, don't fetch details from mooc.fi
Redande Aug 27, 2025
e5cfdc4
Remove unused import and parameter
Redande Aug 27, 2025
486e919
Chatbot bug fix (#1531)
HeljaeRaeisaenen Aug 26, 2025
1631329
Restored package-lock files
Aug 27, 2025
7aa3a9b
Delete account form
Aug 28, 2025
95034eb
Backend for deleting user
Sep 2, 2025
4b2ad4e
Save upstream id from tmc during signup
Redande Sep 9, 2025
2c62428
Added single-use-code when deleting account
Sep 10, 2025
2806570
Refactored to work with multiple email placeholders
Sep 10, 2025
ba0a090
Merge branch 'move-passwords-from-tmc' of github.com:rage/secret-proj…
Sep 10, 2025
e26b541
Removed &mut
Sep 10, 2025
47d9001
Merge remote-tracking branch 'origin/master' into move-passwords-from…
Sep 11, 2025
5e75d21
Little test fix
Sep 12, 2025
4f664a2
Merge remote-tracking branch 'origin/master' into move-passwords-from…
Sep 12, 2025
f7f6136
Added delete account email template to seed
Sep 15, 2025
690357b
Merge remote-tracking branch 'origin/master' into move-passwords-from…
Sep 15, 2025
17bd8b7
Delete account from TMC
Sep 15, 2025
1210b07
Minor improvements and timer for resend button
Sep 15, 2025
c153fb7
Improvements to VerifyOneTimeCodeForm
Sep 16, 2025
b64c495
Old password optional and updated screenshots
Sep 22, 2025
3a95ec0
Merge remote-tracking branch 'origin/master' into move-passwords-from…
Oct 8, 2025
df88247
User can change password in user settings -page
Oct 13, 2025
2771ba7
Refactor to fetch user data from TMC not mooc.fi
Redande Oct 28, 2025
ead3008
Use SecretString for access tokens
Redande Oct 28, 2025
9f7c00e
Borrow the SecretStrings while passing them to functions
Redande Oct 28, 2025
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
54 changes: 54 additions & 0 deletions kubernetes/base/headless-lms/email-deliver.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: email-deliver
labels:
app: email-deliver
deploymentType: with-init-container
needs-db: "true"
spec:
replicas: 1
selector:
matchLabels:
app: email-deliver
template:
metadata:
annotations:
linkerd.io/inject: enabled
labels:
app: email-deliver
spec:
containers:
Comment on lines +20 to +21
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Set Pod-level security context (runAsNonRoot/fsgroup).

To avoid UID drift and ensure non-root across all containers, define a pod-level securityContext. Choose a UID/GID your image supports.

     spec:
+      securityContext:
+        runAsNonRoot: true
+        runAsUser: 10001
+        runAsGroup: 10001
+        fsGroup: 10001
+        seccompProfile:
+          type: RuntimeDefault
       containers:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
spec:
containers:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
🤖 Prompt for AI Agents
In kubernetes/base/headless-lms/email-deliver.yml around lines 20 to 21, add a
pod-level securityContext under spec (not per-container) that sets runAsNonRoot:
true, runAsUser to a non-root UID that your container image supports, and
fsGroup to an appropriate GID to ensure shared volumes are correctly owned;
ensure values match the image capabilities (or the Dockerfile USER) and remove
or reconcile any conflicting container-level securityContext entries so all
containers inherit the pod-level settings.

- name: email-deliver
image: headless-lms
command: ["bin/run", "email-deliver"]
Comment on lines +23 to +24
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Pin image (avoid mutable tags) and define imagePullPolicy.

Using an untagged image resolves to “latest” and is mutable. Pin to a version or digest to prevent supply-chain drift; set an explicit pull policy.

-          image: headless-lms
+          image: ghcr.io/your-org/headless-lms@sha256:<digest>
+          imagePullPolicy: IfNotPresent

If you must stay on a mutable tag temporarily:

-          image: headless-lms
+          image: your-registry/headless-lms:latest
+          imagePullPolicy: Always
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
image: headless-lms
command: ["bin/run", "email-deliver"]
image: ghcr.io/your-org/headless-lms@sha256:<digest>
imagePullPolicy: IfNotPresent
command: ["bin/run", "email-deliver"]
🤖 Prompt for AI Agents
In kubernetes/base/headless-lms/email-deliver.yml around lines 23 to 24, the
container image is referenced without a tag and no imagePullPolicy is set;
update the image field to a fixed tag or digest (e.g. image:
headless-lms:<version> or image: headless-lms@sha256:<digest>) and add an
explicit imagePullPolicy (e.g. IfNotPresent for pinned tags or Always for
digests/CI flows) to avoid mutable “latest” behavior and supply-chain drift.

resources:
requests:
memory: 100Mi
cpu: 20m
limits:
memory: 300Mi
cpu: 200m
envFrom:
- secretRef:
name: headless-lms-secrets
initContainers:
Comment on lines +21 to +35
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Harden container: drop privileges and disallow privilege escalation.

Static analysis flags CKV_K8S_20 and CKV_K8S_23 are valid here. The workload has no declared securityContext, so it will likely run as root and with allowPrivilegeEscalation by default. Add a restrictive securityContext to the app container.

Apply:

         - name: email-deliver
           image: headless-lms
           command: ["bin/run", "email-deliver"]
+          securityContext:
+            allowPrivilegeEscalation: false
+            readOnlyRootFilesystem: true
+            capabilities:
+              drop: ["ALL"]
+            seccompProfile:
+              type: RuntimeDefault
           resources:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
containers:
- name: email-deliver
image: headless-lms
command: ["bin/run", "email-deliver"]
resources:
requests:
memory: 100Mi
cpu: 20m
limits:
memory: 300Mi
cpu: 200m
envFrom:
- secretRef:
name: headless-lms-secrets
initContainers:
containers:
- name: email-deliver
image: headless-lms
command: ["bin/run", "email-deliver"]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
resources:
requests:
memory: 100Mi
cpu: 20m
limits:
memory: 300Mi
cpu: 200m
envFrom:
- secretRef:
name: headless-lms-secrets
initContainers:
🤖 Prompt for AI Agents
In kubernetes/base/headless-lms/email-deliver.yml around lines 21 to 35, the
email-deliver container has no securityContext so it may run as root and allow
privilege escalation; add a restrictive securityContext for the app container
that enforces running as non-root (set runAsNonRoot: true and a non-zero
runAsUser), disallows privilege escalation (allowPrivilegeEscalation: false),
drops all Linux capabilities (capabilities.drop: ["ALL"]), and enable additional
hardening like readOnlyRootFilesystem: true (and optionally set a seccompProfile
to RuntimeDefault).

- name: headless-lms-wait-for-db
image: headless-lms
command:
- bash
- "-c"
- |
echo Waiting for postgres to be available
timeout 120 ./wait-for-db.sh
./wait-for-db-migrations.sh
resources:
requests:
memory: 100Mi
cpu: 20m
limits:
memory: 300Mi
cpu: 200m
envFrom:
- secretRef:
name: headless-lms-secrets
Comment on lines +35 to +54
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Harden initContainer as well.

Same principle of least privilege should apply to the init container, otherwise the Pod still violates the checks.

         - name: headless-lms-wait-for-db
           image: headless-lms
           command:
             - bash
             - "-c"
             - |
               echo Waiting for postgres to be available
               timeout 120 ./wait-for-db.sh
               ./wait-for-db-migrations.sh
+          securityContext:
+            allowPrivilegeEscalation: false
+            readOnlyRootFilesystem: true
+            capabilities:
+              drop: ["ALL"]
+            seccompProfile:
+              type: RuntimeDefault
           resources:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
initContainers:
- name: headless-lms-wait-for-db
image: headless-lms
command:
- bash
- "-c"
- |
echo Waiting for postgres to be available
timeout 120 ./wait-for-db.sh
./wait-for-db-migrations.sh
resources:
requests:
memory: 100Mi
cpu: 20m
limits:
memory: 300Mi
cpu: 200m
envFrom:
- secretRef:
name: headless-lms-secrets
initContainers:
- name: headless-lms-wait-for-db
image: headless-lms
command:
- bash
- "-c"
- |
echo Waiting for postgres to be available
timeout 120 ./wait-for-db.sh
./wait-for-db-migrations.sh
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
resources:
requests:
memory: 100Mi
cpu: 20m
limits:
memory: 300Mi
cpu: 200m
envFrom:
- secretRef:
name: headless-lms-secrets
🧰 Tools
🪛 Checkov (3.2.334)

[MEDIUM] 1-54: Containers should not run with allowPrivilegeEscalation

(CKV_K8S_20)


[MEDIUM] 1-54: Minimize the admission of root containers

(CKV_K8S_23)

🤖 Prompt for AI Agents
In kubernetes/base/headless-lms/email-deliver.yml around lines 35 to 54 the
initContainer is not hardened and therefore can violate pod
security/least-privilege checks; add a securityContext for the initContainer (or
for all initContainers) that enforces least privilege: set runAsNonRoot: true
and a non-root runAsUser, set allowPrivilegeEscalation: false, set
readOnlyRootFilesystem: true, drop all non-essential capabilities (e.g., drop:
["ALL"]) and set a restrictive seccomp/profile if available; ensure uid/gid are
consistent with the image and, if needed, mirror the pod-level fsGroup/file
permissions rather than granting broader privileges.

1 change: 1 addition & 0 deletions kubernetes/base/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ resources:
- headless-lms/sync-tmc-users.yml
- headless-lms/chatbot-syncer.yml
- headless-lms/mailchimp-syncer.yml
- headless-lms/email-deliver.yml
6 changes: 6 additions & 0 deletions kubernetes/dev/headless-lms/env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,10 @@ data:
ICU4X_POSTCARD_PATH: "L2ljdTR4LnBvc3RjYXJkLjI="
# for local development only, intentionally public
TMC_SERVER_SECRET_FOR_COMMUNICATING_TO_SECRET_PROJECT: Zm9yIGxvY2FsIGRldmVsb3BtZW50IG9ubHksIGludGVudGlvbmFsbHkgcHVibGlj
# for local development only, intentionally public
SMTP_FROM: "bm9yZXBseUBleGFtcGxlLmRldg=="
SMTP_HOST: "c210cC5kZXYuZXhhbXBsZS5vcmc="
SMTP_PORT: "NTg3"
SMTP_USER: "ZGV2LXVzZXI="
SMTP_PASS: "ZGV2LXBhc3M="
USE_MOCK_AZURE_CONFIGURATION: "dHJ1ZQ=="
6 changes: 6 additions & 0 deletions kubernetes/test/headless-lms/env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,10 @@ data:
ICU4X_POSTCARD_PATH: "L2ljdTR4LnBvc3RjYXJkLjI="
# for local development only, intentionally public
TMC_SERVER_SECRET_FOR_COMMUNICATING_TO_SECRET_PROJECT: Zm9yIGxvY2FsIGRldmVsb3BtZW50IG9ubHksIGludGVudGlvbmFsbHkgcHVibGlj
# for local development only, intentionally public
SMTP_FROM: "bm9yZXBseUBleGFtcGxlLmRldg=="
SMTP_HOST: "c210cC5kZXYuZXhhbXBsZS5vcmc="
SMTP_PORT: "NTg3"
SMTP_USER: "ZGV2LXVzZXI="
SMTP_PASS: "ZGV2LXBhc3M="
USE_MOCK_AZURE_CONFIGURATION: "dHJ1ZQ=="
47 changes: 47 additions & 0 deletions services/headless-lms/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions services/headless-lms/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,5 @@ sqlx = { version = "0.8.6", features = [
"chrono",
"json",
] }

secrecy = { version = "0.10.3", features = ["serde"] }
7 changes: 6 additions & 1 deletion services/headless-lms/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Built from DockerfileBase.dockerfile
FROM eu.gcr.io/moocfi-public/project-331-headless-lms-dev-base:latest as chef
RUN chown -R user /app

# Make /app writable for the non-root user
USER root
RUN mkdir -p /app && chown user:user /app

WORKDIR /app
USER user

FROM chef AS planner
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE user_passwords;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
CREATE TABLE user_passwords (
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
password_hash TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE
);
CREATE TRIGGER set_timestamp BEFORE
UPDATE ON user_passwords FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp();

COMMENT ON TABLE user_passwords IS 'This table is used to keep a record of the users password';
COMMENT ON COLUMN user_passwords.user_id IS 'References the unique identifier of the user.';
COMMENT ON COLUMN user_passwords.password_hash IS 'Hashed password of the user';
;
COMMENT ON COLUMN user_passwords.created_at IS 'Timestamp of when the record was created.';
COMMENT ON COLUMN user_passwords.updated_at IS 'Timestamp when the record was last updated. The field is updated automatically by the set_timestamp trigger.';
COMMENT ON COLUMN user_passwords.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.';
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
DROP TABLE password_reset_tokens;

DELETE FROM email_deliveries
WHERE email_template_id IN (
SELECT id
FROM email_templates
WHERE course_instance_id IS NULL
);

DELETE FROM email_templates
WHERE course_instance_id IS NULL;

ALTER TABLE email_templates
ALTER COLUMN course_instance_id
SET NOT NULL;

ALTER TABLE email_templates DROP COLUMN IF EXISTS language;

DROP INDEX IF EXISTS unique_email_templates_name_language_general;


COMMENT ON TABLE email_templates IS 'An email template table, which contains the email subject and content written in the Gutenberg Editor. Supports adding exercise points/completions threshold templates for course instances.';
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
CREATE TABLE password_reset_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
token UUID NOT NULL UNIQUE,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Store only hashed reset tokens; avoid plaintext UUID tokens.

Saving raw reset tokens (even UUID v4) is a security risk if the DB is leaked. Best practice is to hash the token server-side, store only the hash, and perform constant‑time comparisons on lookup. This prevents immediate account takeover from DB dumps and limits blast radius.

Suggested schema change (requires coordinated Rust changes to compute and query by hash):

-CREATE TABLE password_reset_tokens (
-  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-  token UUID NOT NULL UNIQUE,
+CREATE TABLE password_reset_tokens (
+  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+  token_hash BYTEA NOT NULL,
   user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
   expires_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() + INTERVAL '1 hour',
   used_at TIMESTAMP WITH TIME ZONE,
   created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
   updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
   deleted_at TIMESTAMP WITH TIME ZONE
 );

Additional indices and constraints to add in this or a follow-up migration:

-- Uniqueness is optional but helps guard against accidental duplicates
CREATE UNIQUE INDEX IF NOT EXISTS idx_password_reset_tokens_token_hash
  ON password_reset_tokens (token_hash);

-- Enforce at most one active token per user (see concurrency note below)
CREATE UNIQUE INDEX IF NOT EXISTS idx_password_reset_tokens_one_active_per_user
  ON password_reset_tokens (user_id)
  WHERE used_at IS NULL AND deleted_at IS NULL;

I can provide the Rust-side changes (issue token, hash with SHA-256, store hash, and compare in constant time) if you want to move forward.

user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() + INTERVAL '1 hour',
used_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE
);


CREATE TRIGGER set_timestamp BEFORE
UPDATE ON password_reset_tokens FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp();

CREATE UNIQUE INDEX IF NOT EXISTS unique_active_password_reset_tokens_user ON password_reset_tokens(user_id)
WHERE deleted_at IS NULL
AND used_at IS NULL;


COMMENT ON TABLE password_reset_tokens IS 'Stores one-time password reset tokens with expiration and usage tracking.';
COMMENT ON COLUMN password_reset_tokens.id IS 'A unique identifier for the resetting user password';
COMMENT ON COLUMN password_reset_tokens.token IS 'Token sent to user for resetting their password.';
COMMENT ON COLUMN password_reset_tokens.user_id IS 'References the user the token belongs to.';
COMMENT ON COLUMN password_reset_tokens.expires_at IS 'Time after which the token becomes invalid.';
COMMENT ON COLUMN password_reset_tokens.used_at IS 'Time when the token was used. Null if unused.';
COMMENT ON COLUMN password_reset_tokens.created_at IS 'Time when the token was created.';
COMMENT ON COLUMN password_reset_tokens.updated_at IS 'Time when the token was last updated. Automatically set by trigger.';
COMMENT ON COLUMN password_reset_tokens.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.';

ALTER TABLE email_templates
ALTER COLUMN course_instance_id DROP NOT NULL;

ALTER TABLE email_templates
ADD COLUMN language VARCHAR(255);

CREATE UNIQUE INDEX IF NOT EXISTS unique_email_templates_name_language_general ON email_templates(name, language)
WHERE course_instance_id IS NULL;

COMMENT ON COLUMN email_templates.course_instance_id IS 'If not null the template is considered course instance specific. If null, the template is considered general.';

COMMENT ON COLUMN email_templates.language IS 'Language code for the template, e.g. fi, en, sv. If null, language is not specified';

COMMENT ON TABLE email_templates IS 'An email template table, which contains the email subject and content written in the Gutenberg Editor. Template is general if course_instance_id is NULL, or specific to a course instance if course_instance_id is set. Supports adding exercise points/completions threshold templates for course instances.';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE user_email_codes;
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
CREATE TABLE user_email_codes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(16) NOT NULL,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() + INTERVAL '1 hour',
used_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE
);

CREATE TRIGGER set_timestamp BEFORE
UPDATE ON user_email_codes FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp();

CREATE UNIQUE INDEX IF NOT EXISTS unique_active_user_email_codes_user ON user_email_codes(user_id, code)
WHERE deleted_at IS NULL
AND used_at IS NULL;

COMMENT ON TABLE user_email_codes IS 'Stores single-use codes for actions like user account deletion verification.';
COMMENT ON COLUMN user_email_codes.id IS 'A unique identifier for this code record';
COMMENT ON COLUMN user_email_codes.code IS 'The single-use code sent to the user.';
COMMENT ON COLUMN user_email_codes.user_id IS 'References the user the code belongs to.';
COMMENT ON COLUMN user_email_codes.expires_at IS 'Time after which the code becomes invalid.';
COMMENT ON COLUMN user_email_codes.used_at IS 'Time when the code was used. Null if unused.';
COMMENT ON COLUMN user_email_codes.created_at IS 'Time when the code was created.';
COMMENT ON COLUMN user_email_codes.updated_at IS 'Time when the code was last updated. Automatically set by trigger.';
COMMENT ON COLUMN user_email_codes.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.';
Loading
Loading