feat(consent): user_terms_consents + /v1/me/consent
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { buildErasurePlan, eraseUser, schema } from "@app/database";
|
||||
import {
|
||||
consentInputSchema,
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
updateMeInputSchema,
|
||||
updatePreferencesInputSchema,
|
||||
} from "@app/validation";
|
||||
import { TERMS_VERSION } from "@app/shared-types";
|
||||
import { computeDailyTargets, DEFAULT_TARGETS } from "@app/nutrition-engine";
|
||||
import { errors, parse } from "../lib/errors.js";
|
||||
import { audit, generateInviteCode, getActiveHouseholdId } from "../lib/helpers.js";
|
||||
@@ -166,6 +167,39 @@ export async function meRoutes(app: FastifyInstance) {
|
||||
return { ok: true, householdId };
|
||||
});
|
||||
|
||||
// --- Villkorssamtycke (terms) ---
|
||||
app.post("/v1/me/consent", auth, async (req) => {
|
||||
const now = new Date();
|
||||
const [row] = await app.db
|
||||
.insert(schema.userTermsConsents)
|
||||
.values({ userId: req.userId, termsVersion: TERMS_VERSION, acceptedAt: now })
|
||||
.onConflictDoNothing({ target: [schema.userTermsConsents.userId, schema.userTermsConsents.termsVersion] })
|
||||
.returning();
|
||||
return {
|
||||
accepted: true,
|
||||
version: TERMS_VERSION,
|
||||
acceptedAt: row?.acceptedAt ?? now,
|
||||
};
|
||||
});
|
||||
|
||||
app.get("/v1/me/consent", auth, async (req) => {
|
||||
const [row] = await app.db
|
||||
.select()
|
||||
.from(schema.userTermsConsents)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.userTermsConsents.userId, req.userId),
|
||||
eq(schema.userTermsConsents.termsVersion, TERMS_VERSION),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
return {
|
||||
accepted: row != null,
|
||||
version: TERMS_VERSION,
|
||||
acceptedAt: row?.acceptedAt ?? null,
|
||||
};
|
||||
});
|
||||
|
||||
// --- Samtycken (spec §33: separata) ---
|
||||
app.get("/v1/me/consents", auth, async (req) => {
|
||||
return app.db
|
||||
|
||||
@@ -28,6 +28,7 @@ STATIC_CONFIG = {
|
||||
"LOG_LEVEL": "info",
|
||||
"AAMOS_MODE": "gemini",
|
||||
"AAMOS_API_URL": "https://amos.aamos.systems",
|
||||
"EOC_URL": "https://eoc.aamos.systems",
|
||||
"AAMOS_TIMEOUT_MS": "60000",
|
||||
# GEMINI_MODEL hämtas från SSM (String-typ)
|
||||
"GEMINI_TIMEOUT_MS": "60000",
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
CREATE TABLE "gemini_usage" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"task_type" text NOT NULL,
|
||||
"prompt_tokens" integer,
|
||||
"output_tokens" integer,
|
||||
"total_tokens" integer,
|
||||
"cost_microcents" bigint
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "user_terms_consents" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"terms_version" text NOT NULL,
|
||||
"accepted_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "user_terms_consents" ADD CONSTRAINT "user_terms_consents_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "user_terms_consents_user_version_unique" ON "user_terms_consents" USING btree ("user_id","terms_version");
|
||||
File diff suppressed because it is too large
Load Diff
@@ -183,6 +183,13 @@
|
||||
"when": 1786397281333,
|
||||
"tag": "0026_s5_onboarding_memory",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 26,
|
||||
"version": "7",
|
||||
"when": 1786561534897,
|
||||
"tag": "0026_light_infant_terrible",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -23,3 +23,4 @@ export * from "./ops.js";
|
||||
export * from "./platform.js";
|
||||
export * from "./releaseGates.js";
|
||||
export * from "./gemini-usage.js";
|
||||
export * from "./terms.js";
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core";
|
||||
import { createdAt } from "./_shared.js";
|
||||
import { users } from "./users.js";
|
||||
|
||||
/**
|
||||
* Godkännande av allmänna villkor. En rad per (userId, termsVersion) gör
|
||||
* bevislagringen otvetydig: vilken version som gällde när användaren
|
||||
* accepterade och exakt när det skedde.
|
||||
*/
|
||||
export const userTermsConsents = pgTable(
|
||||
"user_terms_consents",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
userId: uuid("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
termsVersion: text("terms_version").notNull(),
|
||||
acceptedAt: timestamp("accepted_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
createdAt: createdAt(),
|
||||
},
|
||||
(t) => [uniqueIndex("user_terms_consents_user_version_unique").on(t.userId, t.termsVersion)],
|
||||
);
|
||||
@@ -180,3 +180,6 @@ export const STORE_SECTION_LABELS_SV: Record<StoreSection, string> = {
|
||||
|
||||
export const AI_CONTRACT_VERSION = "1.0.0";
|
||||
export const API_VERSION = "v1";
|
||||
|
||||
/** Gällande version av användarvillkoren. Skrivs till user_terms_consents. */
|
||||
export const TERMS_VERSION = "1.0";
|
||||
|
||||
Reference in New Issue
Block a user