diff --git a/app/components/header/menu/index.tsx b/app/components/header/menu/index.tsx index 8c1fed44..ca18254a 100644 --- a/app/components/header/menu/index.tsx +++ b/app/components/header/menu/index.tsx @@ -10,6 +10,7 @@ import { ExternalLink, Settings, Compass, + ScrollText, } from 'lucide-react' import { useState } from 'react' import { useTranslation } from 'react-i18next' @@ -153,6 +154,23 @@ export default function Menu() { + + + e.preventDefault()} + className="cursor-pointer" + > + + {t('tos')} + + + + diff --git a/app/components/mydevices/edit-device/edit-device-sidebar-nav.tsx b/app/components/mydevices/edit-device/edit-device-sidebar-nav.tsx index 1a93dfdc..fc0740d0 100644 --- a/app/components/mydevices/edit-device/edit-device-sidebar-nav.tsx +++ b/app/components/mydevices/edit-device/edit-device-sidebar-nav.tsx @@ -10,7 +10,7 @@ interface SidebarNavProps extends React.HTMLAttributes { }[] } -export function EditDviceSidebarNav({ +export function EditDeviceSidebarNav({ className, items, ...props diff --git a/app/components/ui/dialog.tsx b/app/components/ui/dialog.tsx index 72262a8b..7b73cf86 100644 --- a/app/components/ui/dialog.tsx +++ b/app/components/ui/dialog.tsx @@ -39,8 +39,8 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName const DialogContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( + React.ComponentPropsWithoutRef & { hideClose?: boolean } +>(({ className, children, hideClose, ...props }, ref) => ( {children} + {!hideClose &&( Close + )} )) diff --git a/app/lib/api-routes.ts b/app/lib/api-routes.ts new file mode 100644 index 00000000..3f6cf757 --- /dev/null +++ b/app/lib/api-routes.ts @@ -0,0 +1,241 @@ +import { type Route } from '../+types/root' +import { tosApiMiddleware } from '~/middleware/tos-api.server' + +type RouteInfo = { + path: string + method: 'GET' | 'PUT' | 'POST' | 'DELETE' + skipTos: boolean + deprecationNotice?: string +} + +export const middleware: Route.MiddlewareFunction[] = [tosApiMiddleware]; + +export const apiRoutes: { noauth: RouteInfo[]; auth: RouteInfo[] } = { + noauth: [ + { + path: '/', + method: 'GET', + skipTos: true + }, + { + path: '/stats', + method: 'GET', + skipTos: true + }, + { + path: '/tags', + method: 'GET', + skipTos: true + }, + // { + // path: `statistics/idw`, + // method: "GET", + + // }, + // { + // path: `statistics/descriptive`, + // method: "GET", + + // }, + { + path: `boxes`, + method: 'GET', + skipTos: true + }, + { + path: `boxes/data`, + method: 'GET', + skipTos: true + }, + // { + // path: `boxes/:boxId`, + // method: "GET", + // }, + { + path: `boxes/:boxId/sensors`, + method: 'GET', + skipTos: true + }, + { + path: `boxes/:boxId/sensors/:sensorId`, + method: 'GET', + skipTos: true + }, + // { + // path: `boxes/:boxId/data/:sensorId`, + // method: "GET", + // }, + // { + // path: `boxes/:boxId/locations`, + // method: "GET", + // }, + // { + // path: `boxes/data`, + // method: "POST", + // }, + { + path: `boxes/:boxId/data`, + method: 'POST', + skipTos: true + }, + { + path: `boxes/:boxId/:sensorId`, + method: 'POST', + skipTos: true + }, + { + path: `users/register`, + method: 'POST', + skipTos: true + }, + { + path: `users/request-password-reset`, + method: 'POST', + skipTos: true + }, + { + path: `users/password-reset`, + method: 'POST', + skipTos: true + }, + { + path: `users/confirm-email`, + method: 'POST', + skipTos: true + }, + { + path: `users/sign-in`, + method: 'POST', + skipTos: true + }, + ], + auth: [ + { + path: `users/refresh-auth`, + method: 'POST', + skipTos: true + }, + { + path: `users/me`, + method: 'GET', + skipTos: true + }, + { + path: `users/me`, + method: 'PUT', + skipTos: false + }, + { + path: `users/me/boxes`, + method: 'GET', + skipTos: true + }, + { + path: `users/me/boxes/:boxId`, + method: 'GET', + skipTos: true + }, + // { + // path: `boxes/:boxId/script`, + // method: "GET", + // }, + { + path: `boxes`, + method: 'POST', + skipTos: false + }, + { + path: `boxes/claim`, + method: 'POST', + skipTos: false + }, + { + path: `boxes/transfer`, + method: 'POST', + skipTos: false + }, + { + path: `boxes/transfer`, + method: 'DELETE', + skipTos: false + }, + { + path: `boxes/transfer/:boxId`, + method: 'GET', + skipTos: true + }, + { + path: `boxes/transfer/:boxId`, + method: 'PUT', + skipTos: false + }, + { + path: `boxes/:boxId`, + method: 'PUT', + skipTos: false + }, + { + path: `boxes/:boxId`, + method: 'DELETE', + skipTos: false + }, + { + path: `boxes/:boxId/:sensorId/measurements`, + method: 'DELETE', + skipTos: false + }, + { + path: `users/sign-out`, + method: 'POST', + skipTos: true + }, + { + path: `users/me`, + method: 'DELETE', + skipTos: true + }, + { + path: `users/me/resend-email-confirmation`, + method: 'POST', + skipTos: false + }, + ], + // management: [ + // { + // path: `${managementPath}/boxes`, + // method: "GET", + // }, + // { + // path: `${managementPath}/boxes/:boxId`, + // method: "GET", + // }, + // { + // path: `${managementPath}/boxes/:boxId`, + // method: "PUT", + // }, + // { + // path: `${managementPath}/boxes/delete`, + // method: "POST", + // }, + // { + // path: `${managementPath}/users`, + // method: "GET", + // }, + // { + // path: `${managementPath}/users/:userId`, + // method: "GET", + // }, + // { + // path: `${managementPath}/users/:userId`, + // method: "PUT", + // }, + // { + // path: `${managementPath}/users/delete`, + // method: "POST", + // }, + // { + // path: `${managementPath}/users/:userId/exec`, + // method: "POST", + // }, + // ], +} \ No newline at end of file diff --git a/app/lib/request-parsing.ts b/app/lib/request-parsing.ts index a18cdf09..a2a2c0da 100644 --- a/app/lib/request-parsing.ts +++ b/app/lib/request-parsing.ts @@ -43,6 +43,7 @@ export async function parseUserRegistrationData(request: Request): Promise<{ email: string password: string language: string + tosAccepted: boolean }> { const data = await parseRequestData(request) @@ -51,6 +52,7 @@ export async function parseUserRegistrationData(request: Request): Promise<{ email: data.email || '', password: data.password || '', language: data.language || 'en_US', + tosAccepted: data.tosAccepted || false } } diff --git a/app/lib/user-service.server.ts b/app/lib/user-service.server.ts index 991da5f1..f4340473 100644 --- a/app/lib/user-service.server.ts +++ b/app/lib/user-service.server.ts @@ -24,9 +24,11 @@ import { type UsernameValidation, validateEmail, validatePassword, + validateTosAccepted, validateUsername, } from './user-service' import { drizzleClient } from '~/db.server' +import { getCurrentEffectiveTos } from '~/models/tos.server' import { createUser, deleteUserByEmail, @@ -57,6 +59,7 @@ export const registerUser = async ( email: string, password: string, language: 'de_DE' | 'en_US', + tosAccepted: boolean, ): Promise< UsernameValidation | EmailValidation | PasswordValidation | User | null > => { @@ -69,10 +72,16 @@ export const registerUser = async ( const passwordValidation = validatePassword(password) if (!passwordValidation.isValid) return passwordValidation + const tosValidation = validateTosAccepted(tosAccepted) + if(!tosValidation.isValid) return tosValidation + + const tos = await getCurrentEffectiveTos() + invariant(tos, 'Expected tos to be configured.') + const existingUser = await getUserByEmail(email) if (existingUser) return null // no new user is created -> null - const newUsers = await createUser(username, email, language, password) + const newUsers = await createUser(username, email, language, password, tos.id) if (newUsers.length === 0) throw new Error('Something went wrong creating the user profile!') diff --git a/app/lib/user-service.ts b/app/lib/user-service.ts index c30b9929..f57ee779 100644 --- a/app/lib/user-service.ts +++ b/app/lib/user-service.ts @@ -1,5 +1,5 @@ type RegistrationInputValidation = { - validationKind: 'username' | 'email' | 'password' + validationKind: 'username' | 'email' | 'password' | 'tos' } export type UsernameValidation = { @@ -65,3 +65,17 @@ export const validatePassword = (password: string): PasswordValidation => { return { isValid: false, length: true, validationKind: 'password' } return { isValid: true, validationKind: 'password' } } + +export type TosValidation = { + isValid: boolean + required?: boolean +} & RegistrationInputValidation + +export const validateTosAccepted = (tosAccepted: boolean): TosValidation => { + if (!tosAccepted) { + return { isValid: false, required: true, validationKind: 'tos' } + } + return { isValid: true, validationKind: 'tos' } +} + + diff --git a/app/middleware/tos-api.server.ts b/app/middleware/tos-api.server.ts new file mode 100644 index 00000000..778153ec --- /dev/null +++ b/app/middleware/tos-api.server.ts @@ -0,0 +1,76 @@ +import { apiRoutes } from '~/lib/api-routes' +import { getUserFromJwt } from '~/lib/jwt' +import { getTosRequirementForUser } from '~/models/tos.server' + +type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' + +function json(body: unknown, status = 200) { + return new Response(JSON.stringify(body), { + status, + headers: { 'content-type': 'application/json; charset=utf-8' }, + }) +} + +type CompiledRule = { + method: HttpMethod | '*' + matcher: RegExp +} + +/** + * Convert a route pattern like "/api/users/me/boxes/:boxId" + * into a regex like ^/api/users/me/boxes/[^/]+$ + */ +function routeToRegex(apiPathPattern: string) { + const escaped = apiPathPattern + .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // escape regex special chars + .replace(/\\:([A-Za-z0-9_]+)/g, '[^/]+') // replace ":param" segments + return new RegExp(`^${escaped}$`) +} + +/** + * Build allowlist from route metadata: + * - `auth` routes with `skipTos: true` bypass ToS checks + */ +const API_TOS_ALLOWLIST: CompiledRule[] = [ + ...apiRoutes.auth + .filter((r: any) => r.skipTos) + .map((r: any) => ({ + method: r.method as HttpMethod, + matcher: routeToRegex(`/api/${r.path}`), + })), +] + +function isAllowedApi(request: Request, pathname: string) { + const method = request.method as HttpMethod + return API_TOS_ALLOWLIST.some((rule) => { + if (rule.method !== '*' && rule.method !== method) return false + return rule.matcher.test(pathname) + }) +} + +export async function tosApiMiddleware( + { request }: { request: Request }, + next: () => Promise, +) { + const url = new URL(request.url) + + const jwtUser = await getUserFromJwt(request) + if (typeof jwtUser !== 'object') return next() + + if (isAllowedApi(request, url.pathname)) return next() + + const req = await getTosRequirementForUser(jwtUser.id) + if (req.mustBlock && req.tos) { + return json( + { + code: 'tos_required', + tosVersionId: req.tos.id, + effectiveFrom: req.tos.effectiveFrom, + acceptBy: req.tos.acceptBy, + }, + 428, + ) + } + + return next() +} \ No newline at end of file diff --git a/app/middleware/tos-ui.server.ts b/app/middleware/tos-ui.server.ts new file mode 100644 index 00000000..e947c708 --- /dev/null +++ b/app/middleware/tos-ui.server.ts @@ -0,0 +1,41 @@ +import { redirect } from "react-router"; +import { getTosRequirementForUser } from "~/models/tos.server"; +import { getUserId } from "~/utils/session.server"; + +function isAllowedUiPath(pathname: string) { + if (pathname.startsWith("/explore")) return true; + if (pathname === "/terms") return true; + if (pathname === "/settings/delete") return true; + if (pathname === "/logout") return true; + if (pathname === '/tos-required') return true; + if (pathname.startsWith("/profile")) return true; + + return false; +} + +export async function tosUiMiddleware( + { request }: { request: Request }, + next: () => Promise, +) { + const url = new URL(request.url); + + if (url.pathname.startsWith("/api")) { + // handled by tos-api middleware + return next(); + } + + if (isAllowedUiPath(url.pathname)) { + return next(); + } + + const userId = await getUserId(request); + if (!userId) return next(); + + const req = await getTosRequirementForUser(userId); + if (req.mustBlock && req.tos) { + const redirectTo = url.pathname + url.search; + throw redirect(`/tos-required?redirectTo=${encodeURIComponent(redirectTo)}`) + } + + return next(); +} \ No newline at end of file diff --git a/app/models/device.server.ts b/app/models/device.server.ts index 42039ed7..d25893ca 100644 --- a/app/models/device.server.ts +++ b/app/models/device.server.ts @@ -119,6 +119,28 @@ export function getDevice({ id }: Pick) { }) } +export function getUserDevice({ + id, + userId, +}: Pick) { + return drizzleClient.query.device.findFirst({ + where: (d, { and, eq }) => and(eq(d.id, id), eq(d.userId, userId)), + columns: { + id: true, + name: true, + description: true, + exposure: true, + image: true, + tags: true, + website: true, + updatedAt: true, + latitude: true, + longitude: true, + userId: true, + }, + }) +} + export function getLocations( { id }: Pick, fromDate: Date, diff --git a/app/models/tos.server.ts b/app/models/tos.server.ts new file mode 100644 index 00000000..6a686255 --- /dev/null +++ b/app/models/tos.server.ts @@ -0,0 +1,61 @@ +import { drizzleClient } from '~/db.server' +import { tosUserState } from '~/schema/tos' + +export async function getCurrentEffectiveTos(now = new Date()) { + return drizzleClient.query.tosVersion.findFirst({ + where: (t, { lte }) => lte(t.effectiveFrom, now), + orderBy: (t, { desc }) => [desc(t.effectiveFrom)], + }) +} + +async function getUserAcceptance(userId: string, tosVersionId: string) { + return drizzleClient.query.tosUserState.findFirst({ + where: (s, { and, eq }) => + and(eq(s.userId, userId), eq(s.tosVersionId, tosVersionId)), + columns: { acceptedAt: true }, + }) +} + +export async function markTosAccepted({ + userId, + tosId, + now = new Date(), +}: { + userId: string + tosId: string + now?: Date +}) { + await drizzleClient + .insert(tosUserState) + .values({ + userId, + tosVersionId: tosId, + acceptedAt: now, + }) + .onConflictDoUpdate({ + target: [tosUserState.userId, tosUserState.tosVersionId], + set: { acceptedAt: now }, + }) +} + +export async function getTosRequirementForUser(userId: string, now = new Date()) { + const current = await getCurrentEffectiveTos(now) + if (!current) { + return { + tos: null, + accepted: true, + inGrace: false, + mustBlock: false, + acceptBy: null as Date | null, + } + } + + const state = await getUserAcceptance(userId, current.id) + const accepted = !!state?.acceptedAt + + const acceptBy = new Date(current.acceptBy) + const inGrace = !accepted && now < acceptBy + const mustBlock = !accepted && now >= acceptBy + + return { tos: current, accepted, inGrace, mustBlock, acceptBy } +} \ No newline at end of file diff --git a/app/models/user.server.ts b/app/models/user.server.ts index 237a773a..8fff0426 100644 --- a/app/models/user.server.ts +++ b/app/models/user.server.ts @@ -9,6 +9,7 @@ import { type User, password as passwordTable, user, + tosUserState } from '~/schema' export async function getUserById(id: User['id']) { @@ -147,6 +148,7 @@ export async function createUser( email: User['email'], language: User['language'], password: string, + tosVersionId?: string ) { const hashedPassword = await bcrypt.hash(preparePasswordHash(password), 13) // make salt_factor configurable oSeM API uses 13 by default @@ -158,6 +160,8 @@ export async function createUser( email, language, unconfirmedEmail: email, + acceptedTosVersionId: tosVersionId, + acceptedTosAt: new Date(), }) .returning() await t.insert(passwordTable).values({ @@ -165,6 +169,14 @@ export async function createUser( userId: newUser[0].id, }) await createProfileWithTransaction(t, newUser[0].id, name) + if (tosVersionId) { + const now = new Date() + await t.insert(tosUserState).values({ + userId: newUser[0].id, + tosVersionId, + acceptedAt: new Date() + }).onConflictDoNothing() + } return newUser }) } diff --git a/app/root.tsx b/app/root.tsx index 85a8e691..fd8c7104 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -14,9 +14,11 @@ import { type MetaFunction, } from 'react-router' import { useChangeLanguage } from 'remix-i18next/react' +import { type Route } from './+types/root' import { Toaster } from './components/ui/toaster' import { i18nCookie } from './cookies' import i18next from './i18next.server' +import { tosUiMiddleware } from './middleware/tos-ui.server' import { getEnv } from './utils/env.server' import { getUser } from './utils/session.server' @@ -80,6 +82,8 @@ export async function loader({ request }: LoaderFunctionArgs) { ) } +export const middleware: Route.MiddlewareFunction[] = [tosUiMiddleware]; + export let handle = { // In the handle export, we can add a i18n key with namespaces our route // will need to load. This key can be a single string or an array of strings. diff --git a/app/routes/api.ts b/app/routes/api.ts index 802bff0d..5b089be7 100644 --- a/app/routes/api.ts +++ b/app/routes/api.ts @@ -1,32 +1,42 @@ import { type LoaderFunctionArgs } from 'react-router' +import { type Route } from '../+types/root' +import { tosApiMiddleware } from '~/middleware/tos-api.server' type RouteInfo = { path: string method: 'GET' | 'PUT' | 'POST' | 'DELETE' + skipTos: boolean deprecationNotice?: string } -const routes: { noauth: RouteInfo[]; auth: RouteInfo[] } = { +export const middleware: Route.MiddlewareFunction[] = [tosApiMiddleware]; + +export const routes: { noauth: RouteInfo[]; auth: RouteInfo[] } = { noauth: [ { path: '/', method: 'GET', + skipTos: true }, { path: '/stats', method: 'GET', + skipTos: true }, { path: '/tags', method: 'GET', + skipTos: true }, { path: `boxes`, method: 'GET', + skipTos: true }, { path: `boxes/data`, method: 'GET', + skipTos: true }, { path: `boxes/:boxId`, @@ -35,60 +45,74 @@ const routes: { noauth: RouteInfo[]; auth: RouteInfo[] } = { { path: `boxes/:boxId/sensors`, method: 'GET', + skipTos: true }, { path: `boxes/:boxId/sensors/:sensorId`, method: 'GET', + skipTos: true }, { path: `boxes/:boxId/data`, method: 'POST', + skipTos: true }, { path: `boxes/:boxId/:sensorId`, method: 'POST', + skipTos: true }, { path: `users/register`, method: 'POST', + skipTos: true }, { path: `users/request-password-reset`, method: 'POST', + skipTos: true }, { path: `users/password-reset`, method: 'POST', + skipTos: true }, { path: `users/confirm-email`, method: 'POST', + skipTos: true }, { path: `users/sign-in`, method: 'POST', + skipTos: true }, ], auth: [ { path: `users/refresh-auth`, method: 'POST', + skipTos: true }, { path: `users/me`, method: 'GET', + skipTos: true }, { path: `users/me`, method: 'PUT', + skipTos: false }, { path: `users/me/boxes`, method: 'GET', + skipTos: true }, { path: `users/me/boxes/:boxId`, method: 'GET', + skipTos: true }, // { // path: `boxes/:boxId/script`, @@ -97,50 +121,62 @@ const routes: { noauth: RouteInfo[]; auth: RouteInfo[] } = { { path: `boxes`, method: 'POST', + skipTos: false }, { path: `boxes/claim`, method: 'POST', + skipTos: false }, { path: `boxes/transfer`, method: 'POST', + skipTos: false }, { path: `boxes/transfer`, method: 'DELETE', + skipTos: false }, { path: `boxes/transfer/:boxId`, method: 'GET', + skipTos: true }, { path: `boxes/transfer/:boxId`, method: 'PUT', + skipTos: false }, { path: `boxes/:boxId`, method: 'PUT', + skipTos: false }, { path: `boxes/:boxId`, method: 'DELETE', + skipTos: false }, { path: `boxes/:boxId/:sensorId/measurements`, method: 'DELETE', + skipTos: false }, { path: `users/sign-out`, method: 'POST', + skipTos: true }, { path: `users/me`, method: 'DELETE', + skipTos: true }, { path: `users/me/resend-email-confirmation`, method: 'POST', + skipTos: false }, ], } diff --git a/app/routes/api.users.me.accept-tos.ts b/app/routes/api.users.me.accept-tos.ts new file mode 100644 index 00000000..a15fa131 --- /dev/null +++ b/app/routes/api.users.me.accept-tos.ts @@ -0,0 +1,34 @@ +import { type ActionFunctionArgs } from "react-router"; +import { drizzleClient } from "~/db.server"; +import { getUserFromJwt } from "~/lib/jwt"; +import { getCurrentEffectiveTos } from "~/models/tos.server"; +import { tosUserState } from "~/schema/tos"; + +export async function action({ request }: ActionFunctionArgs) { + if (request.method !== "POST") { + return new Response("Method Not Allowed", { status: 405 }); + } + + const jwtUser = await getUserFromJwt(request); + if (typeof jwtUser !== "object") { + return new Response( + JSON.stringify({ code: "invalid_jwt" }), + { status: 403, headers: { "content-type": "application/json; charset=utf-8" } }, + ); + } + + const tos = await getCurrentEffectiveTos(); + if (!tos) { + return new Response( + JSON.stringify({ code: "tos_missing" }), + { status: 500, headers: { "content-type": "application/json; charset=utf-8" } }, + ); + } + + await drizzleClient + .insert(tosUserState) + .values({ userId: jwtUser.id, tosVersionId: tos.id, acceptedAt: new Date(), graceUntil: new Date() }) + .onConflictDoNothing(); + + return new Response(null, { status: 204 }); +} \ No newline at end of file diff --git a/app/routes/api.users.register.ts b/app/routes/api.users.register.ts index 679b6b72..8e6481dc 100644 --- a/app/routes/api.users.register.ts +++ b/app/routes/api.users.register.ts @@ -2,6 +2,7 @@ import { type ActionFunction, type ActionFunctionArgs } from 'react-router' import { createToken } from '~/lib/jwt' import { parseUserRegistrationData } from '~/lib/request-parsing' import { + type TosValidation, type EmailValidation, type PasswordValidation, type UsernameValidation, @@ -56,6 +57,10 @@ import { StandardResponse } from '~/utils/response-utils' * - "en_US" * default: "en_US" * example: "en_US" + * tosAccepted: + * type: boolean + * description: Acceptance of Terms of service + * default: false * responses: * 201: * description: User successfully registered @@ -191,7 +196,8 @@ export const action: ActionFunction = async ({ const email = data.email const password = data.password const language = data.language as 'de_DE' | 'en_US' - const registration = await registerUser(username, email, password, language) + const tosAccepted = data.tosAccepted + const registration = await registerUser(username, email, password, language, tosAccepted) if (!registration) // null is returned when no new user profile was created because it already exists return StandardResponse.badRequest('User already exists.') @@ -221,6 +227,10 @@ export const action: ActionFunction = async ({ if (passwordValidation.length) msg = 'Password must be at least 8 characters long.' break + case 'tos': + const tosValidation = registration as TosValidation + if (tosValidation.required) msg = 'Terms of service must be accepted.' + break } return StandardResponse.badRequest(msg) } diff --git a/app/routes/device.$deviceId.edit.delete.tsx b/app/routes/device.$deviceId.edit.delete.tsx new file mode 100644 index 00000000..57c053d5 --- /dev/null +++ b/app/routes/device.$deviceId.edit.delete.tsx @@ -0,0 +1,131 @@ +import * as React from 'react' +import { Trans, useTranslation } from 'react-i18next' +import { + type ActionFunctionArgs, + type LoaderFunctionArgs, + Form, + data, + redirect, + useActionData, + useLoaderData, +} from 'react-router' +import invariant from 'tiny-invariant' +import { Button } from '~/components/ui/button' +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '~/components/ui/card' +import { Input } from '~/components/ui/input' +import { Label } from '~/components/ui/label' +import { deleteDevice, getDeviceWithoutSensors, getUserDevice } from '~/models/device.server' +import { verifyLogin } from '~/models/user.server' +import { deleteDeviceImage } from '~/utils/s3.server' +import { getUserEmail, getUserId } from '~/utils/session.server' + +export async function loader({ request, params }: LoaderFunctionArgs) { + const userId = await getUserId(request) + if (!userId) return redirect('/') + + const deviceId = params.deviceId + invariant(typeof deviceId === 'string', 'Device id not found.') + + const device = await getUserDevice({ id: deviceId, userId: userId }) + if (!device) return redirect('/profile/me') + + return data({ device }) +} + +export async function action({ request, params }: ActionFunctionArgs) { + const userId = await getUserId(request) + if (!userId) return redirect('/') + + const deviceId = params.deviceId + invariant(typeof deviceId === 'string', 'Device id not found.') + + const formData = await request.formData() + const passwordDelete = formData.get('passwordDelete') + invariant(typeof passwordDelete === 'string', 'password must be a string') + + const userEmail = await getUserEmail(request) + invariant(typeof userEmail === 'string', 'email not found') + + const user = await verifyLogin(userEmail, passwordDelete) + if (!user) { + return data( + { errors: { passwordDelete: 'Invalid password' } }, + { status: 400 }, + ) + } + + const device = await getDeviceWithoutSensors({ id: deviceId }) + if (device?.image) { + try { + await deleteDeviceImage(device.image) + } catch (err) { + console.error('Failed to delete device image:', err) + } + } + + await deleteDevice({ id: deviceId }) + + return redirect('/profile/me') +} + +export default function DeviceDeletePage() { + const { device } = useLoaderData() + const actionData = useActionData() + const passwordRef = React.useRef(null) + const [password, setPassword] = React.useState('') + + const { t } = useTranslation('delete-device') + + + React.useEffect(() => { + if (actionData?.errors?.passwordDelete) passwordRef.current?.focus() + }, [actionData]) + + return ( +
+ + + {t('delete_device')} + + }} + /> + + + + +
+ + setPassword(e.target.value)} + required + /> + {actionData?.errors?.passwordDelete && ( +
+ {actionData.errors.passwordDelete} +
+ )} +
+ + +
+
+
+ ) +} \ No newline at end of file diff --git a/app/routes/device.$deviceId.edit.tsx b/app/routes/device.$deviceId.edit.tsx index 1ac4a5c3..d4f3c513 100644 --- a/app/routes/device.$deviceId.edit.tsx +++ b/app/routes/device.$deviceId.edit.tsx @@ -12,11 +12,12 @@ import { ArrowLeft, UploadCloud, NotepadText, + Trash, } from "lucide-react"; import { useState } from "react"; import { redirect , Link, Outlet, useParams, type LoaderFunctionArgs, useLoaderData } from "react-router"; import ErrorMessage from "~/components/error-message"; -import { EditDviceSidebarNav } from "~/components/mydevices/edit-device/edit-device-sidebar-nav"; +import { EditDeviceSidebarNav } from "~/components/mydevices/edit-device/edit-device-sidebar-nav"; import { NavBar } from "~/components/nav-bar"; import { Separator } from "~/components/ui/separator"; import { getLucideIcon } from "~/lib/lucide-icon-map"; @@ -73,7 +74,7 @@ export default function EditBox() { title: "Transfer", href: `/device/${deviceId}/edit/transfer`, icon: ArrowRightLeft, - }, + } ]; return ( @@ -140,7 +141,7 @@ export default function EditBox() {
{/*
*/}
diff --git a/app/routes/explore.register.tsx b/app/routes/explore.register.tsx index 1ab1d68d..8485141f 100644 --- a/app/routes/explore.register.tsx +++ b/app/routes/explore.register.tsx @@ -27,6 +27,7 @@ import { CardHeader, CardTitle, } from '~/components/ui/card' +import { getCurrentEffectiveTos } from '~/models/tos.server' import { createUser, getUserByEmail, @@ -43,7 +44,7 @@ export async function loader({ request }: LoaderFunctionArgs) { export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData() - const { username, email, password } = Object.fromEntries(formData) + const { username, email, password, tosAccepted } = Object.fromEntries(formData) const redirectTo = safeRedirect(formData.get('redirectTo'), '/explore') if (!username || typeof username !== 'string') { @@ -53,6 +54,7 @@ export async function action({ request }: ActionFunctionArgs) { username: 'username_required', email: null, password: null, + tosAccepted: null, }, }, { status: 400 }, @@ -68,6 +70,7 @@ export async function action({ request }: ActionFunctionArgs) { username: validateUserName.errorMsg, password: null, email: null, + tosAccepted: null, }, }, { status: 400 }, @@ -82,6 +85,7 @@ export async function action({ request }: ActionFunctionArgs) { username: 'username_already_taken', email: null, password: null, + tosAccepted: null, }, }, { status: 400 }, @@ -89,7 +93,7 @@ export async function action({ request }: ActionFunctionArgs) { if (!validateEmail(email)) { return data( - { errors: { username: null, email: 'email_invalid', password: null } }, + { errors: { username: null, email: 'email_invalid', password: null, tosAccepted: null, } }, { status: 400 }, ) } @@ -101,6 +105,7 @@ export async function action({ request }: ActionFunctionArgs) { username: null, password: 'password_required', email: null, + tosAccepted: null, }, }, { status: 400 }, @@ -114,6 +119,7 @@ export async function action({ request }: ActionFunctionArgs) { username: null, password: 'password_too_short', email: null, + tosAccepted: null, }, }, { status: 400 }, @@ -129,19 +135,49 @@ export async function action({ request }: ActionFunctionArgs) { username: null, email: 'email_already_taken', password: null, + tosAccepted: null, }, }, { status: 400 }, ) } + if (tosAccepted !== 'on') { + return data( + { + errors: { + username: null, + email: null, + password: null, + tosAccepted: 'tos_must_accept', + }, + }, + { status: 400 }, + ) + } + + const tos = await getCurrentEffectiveTos() + if (!tos) { + return data( + { + errors: { + username: null, + email: null, + password: null, + tosAccepted: 'tos_unavailable', + }, + }, + { status: 500 }, + ) + } + invariant(typeof username === 'string', 'username must be a string') //* get current locale const locale = await i18next.getLocale(request) const language = locale === 'de' ? 'de_DE' : 'en_US' - const user = await createUser(username, email, language, password) + const user = await createUser(username, email, language, password, tos.id) return createUserSession({ request, @@ -252,6 +288,28 @@ export default function RegisterDialog() {
)}
+
+ + +
+ {actionData?.errors?.tosAccepted && ( +
+ {t(actionData.errors.tosAccepted)} +
+ )} diff --git a/app/routes/terms.tsx b/app/routes/terms.tsx new file mode 100644 index 00000000..0e3d02bc --- /dev/null +++ b/app/routes/terms.tsx @@ -0,0 +1,20 @@ +import { data, useLoaderData } from 'react-router' +import { getCurrentEffectiveTos } from '~/models/tos.server' + +export async function loader() { + const tos = await getCurrentEffectiveTos() + if (!tos) return data({ tos: null }, { status: 500 }) + return data({ tos }) +} + +export default function TermsPage() { + const { tos } = useLoaderData() + if (!tos) return
No ToS configured.
+ + return ( +
+

{tos.title}

+
{tos.body}
+
+ ) +} \ No newline at end of file diff --git a/app/routes/tos-required.tsx b/app/routes/tos-required.tsx new file mode 100644 index 00000000..8f389890 --- /dev/null +++ b/app/routes/tos-required.tsx @@ -0,0 +1,174 @@ +import * as React from 'react' +import { useTranslation, Trans } from 'react-i18next' +import { + Form, + Link, + data, + redirect, + useActionData, + useLoaderData, + type ActionFunctionArgs, + type LoaderFunctionArgs, +} from 'react-router' +import { Button } from '~/components/ui/button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '~/components/ui/dialog' +import { drizzleClient } from '~/db.server' +import { getCurrentEffectiveTos, getTosRequirementForUser } from '~/models/tos.server' +import { tosUserState } from '~/schema/tos' +import { requireUser } from '~/utils/session.server' + +function safeRedirectTo(value: string | null, fallback = '/') { + if (!value) return fallback + if (!value.startsWith('/')) return fallback + if (value.startsWith('//')) return fallback + return value +} + +export async function loader({ request }: LoaderFunctionArgs) { + const user = await requireUser(request) + const url = new URL(request.url) + const redirectTo = safeRedirectTo(url.searchParams.get('redirectTo'), '/') + + const req = await getTosRequirementForUser(user.id) + + if (!req.mustBlock) { + throw redirect(redirectTo) + } + + const tos = await getCurrentEffectiveTos() + if (!tos) return data({ tos: null, redirectTo }, { status: 500 }) + + return data({ tos, redirectTo }) +} + +export async function action({ request }: ActionFunctionArgs) { + const user = await requireUser(request) + const formData = await request.formData() + + const accepted = formData.get('accepted') + const tosVersionId = formData.get('tosVersionId') + const redirectTo = safeRedirectTo(formData.get('redirectTo') as string | null, '/') + + if (accepted !== 'on') { + return data({ error: 'tos_must_accept' }, { status: 400 }) + } + + if (!tosVersionId || typeof tosVersionId !== 'string') { + return data({ error: 'invalid_tos_version' }, { status: 400 }) + } + + const current = await getCurrentEffectiveTos() + if (!current || current.id !== tosVersionId) { + return data({ error: 'tos_not_current' }, { status: 400 }) + } + + const now = new Date() + + await drizzleClient + .insert(tosUserState) + .values({ + userId: user.id, + tosVersionId: current.id, + acceptedAt: now, + }) + .onConflictDoUpdate({ + target: [tosUserState.userId, tosUserState.tosVersionId], + set: { acceptedAt: now }, + }) + + throw redirect(redirectTo) +} + +export default function TosRequiredModal() { + const { tos, redirectTo } = useLoaderData() + const actionData = useActionData() + const [checked, setChecked] = React.useState(false) + + const { t } = useTranslation('tos') + + return ( + {}}> + e.preventDefault()} + onPointerDownOutside={(e) => e.preventDefault()} + onInteractOutside={(e) => e.preventDefault()} + className="sm:max-w-lg" + > + + {t('tos_update')} + {t('must_accept')} + + + {!tos ? ( +
+ {t('not_configured')} +
+ ) : ( +
+ + + +
+ setChecked(e.target.checked)} + /> + +
+ + {actionData?.error === 'tos_must_accept' && ( +
+ {t('must_accept')} +
+ )} + + +
+ + ), + }} + /> +
+ + +
+
+ )} +
+
+ ) +} \ No newline at end of file diff --git a/app/schema/device.ts b/app/schema/device.ts index db58f8a6..531e945a 100644 --- a/app/schema/device.ts +++ b/app/schema/device.ts @@ -49,8 +49,13 @@ export const device = pgTable('device', { expiresAt: date('expires_at', { mode: 'date' }), latitude: doublePrecision('latitude').notNull(), longitude: doublePrecision('longitude').notNull(), - userId: text('user_id').notNull(), sensorWikiModel: text('sensor_wiki_model'), + userId: text('user_id') + .notNull() + .references(() => user.id, { + onDelete: 'cascade', + onUpdate: 'cascade', + }), }) // Many-to-many relation between device - location diff --git a/app/schema/index.ts b/app/schema/index.ts index d929b7c3..0c2d4441 100644 --- a/app/schema/index.ts +++ b/app/schema/index.ts @@ -12,3 +12,4 @@ export * from './log-entry' export * from './refreshToken' export * from './claim' export * from './integration' +export * from './tos' diff --git a/app/schema/tos.ts b/app/schema/tos.ts new file mode 100644 index 00000000..62739f33 --- /dev/null +++ b/app/schema/tos.ts @@ -0,0 +1,44 @@ +import { createId } from '@paralleldrive/cuid2' +import { pgTable, text, timestamp, primaryKey, index, integer } from 'drizzle-orm/pg-core' +import { user } from './user' + +export const tosVersion = pgTable( + 'tos_version', + { + id: text('id').primaryKey().notNull().$defaultFn(() => createId()), + + version: text('version').notNull().unique(), + + title: text('title').notNull(), + body: text('body').notNull(), + + effectiveFrom: timestamp('effective_from', { withTimezone: true }).notNull(), + acceptBy: timestamp('accept_by', { withTimezone: true }).notNull(), + + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), + }, + (t) => ({ + effectiveFromIdx: index('tos_version_effective_from_idx').on(t.effectiveFrom), + acceptByIdx: index('tos_version_accept_by_idx').on(t.acceptBy), + }), +) + +export const tosUserState = pgTable( + 'tos_user_state', + { + userId: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + + tosVersionId: text('tos_version_id') + .notNull() + .references(() => tosVersion.id, { onDelete: 'cascade' }), + + acceptedAt: timestamp('accepted_at', { withTimezone: true }), + }, + (t) => ({ + pk: primaryKey({ columns: [t.userId, t.tosVersionId] }), + userIdx: index('tos_user_state_user_idx').on(t.userId), + }), +) \ No newline at end of file diff --git a/app/schema/user.ts b/app/schema/user.ts index 4cd3cbaf..458794cc 100644 --- a/app/schema/user.ts +++ b/app/schema/user.ts @@ -10,6 +10,7 @@ import { device } from './device' import { password, passwordResetRequest } from './password' import { profile } from './profile' import { refreshToken } from './refreshToken' +import { tosVersion } from './tos' /** * Table @@ -30,6 +31,8 @@ export const user = pgTable('user', { ), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), + acceptedTosVersionId: text('accepted_tos_version_id').references(() => tosVersion.id), + acceptedTosAt: timestamp('accepted_tos_at', { withTimezone: true }), }) /** diff --git a/drizzle/0034_striped_punisher.sql b/drizzle/0034_striped_punisher.sql new file mode 100644 index 00000000..3ded3a50 --- /dev/null +++ b/drizzle/0034_striped_punisher.sql @@ -0,0 +1 @@ +ALTER TABLE "device" ADD CONSTRAINT "device_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE cascade; \ No newline at end of file diff --git a/drizzle/0035_strong_alex_wilder.sql b/drizzle/0035_strong_alex_wilder.sql new file mode 100644 index 00000000..1e354429 --- /dev/null +++ b/drizzle/0035_strong_alex_wilder.sql @@ -0,0 +1,25 @@ +CREATE TABLE "tos_acceptance" ( + "user_id" text NOT NULL, + "tos_version_id" text NOT NULL, + "accepted_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "tos_acceptance_user_id_tos_version_id_pk" PRIMARY KEY("user_id","tos_version_id") +); +--> statement-breakpoint +CREATE TABLE "tos_version" ( + "id" text PRIMARY KEY NOT NULL, + "version" text NOT NULL, + "title" text NOT NULL, + "body" text NOT NULL, + "effective_from" timestamp with time zone NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "tos_version_version_unique" UNIQUE("version") +); +--> statement-breakpoint +ALTER TABLE "user" ADD COLUMN "accepted_tos_version_id" text;--> statement-breakpoint +ALTER TABLE "user" ADD COLUMN "accepted_tos_at" timestamp with time zone;--> statement-breakpoint +ALTER TABLE "tos_acceptance" ADD CONSTRAINT "tos_acceptance_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "tos_acceptance" ADD CONSTRAINT "tos_acceptance_tos_version_id_tos_version_id_fk" FOREIGN KEY ("tos_version_id") REFERENCES "public"."tos_version"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "tos_acceptance_user_idx" ON "tos_acceptance" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "tos_version_effective_from_idx" ON "tos_version" USING btree ("effective_from");--> statement-breakpoint +ALTER TABLE "user" ADD CONSTRAINT "user_accepted_tos_version_id_tos_version_id_fk" FOREIGN KEY ("accepted_tos_version_id") REFERENCES "public"."tos_version"("id") ON DELETE no action ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/0036_blue_stardust.sql b/drizzle/0036_blue_stardust.sql new file mode 100644 index 00000000..1ea50dd9 --- /dev/null +++ b/drizzle/0036_blue_stardust.sql @@ -0,0 +1,15 @@ +CREATE TABLE "tos_user_state" ( + "user_id" text NOT NULL, + "tos_version_id" text NOT NULL, + "first_seen_at" timestamp with time zone DEFAULT now() NOT NULL, + "grace_until" timestamp with time zone NOT NULL, + "accepted_at" timestamp with time zone, + CONSTRAINT "tos_user_state_user_id_tos_version_id_pk" PRIMARY KEY("user_id","tos_version_id") +); +--> statement-breakpoint +DROP TABLE "tos_acceptance" CASCADE;--> statement-breakpoint +ALTER TABLE "tos_version" ADD COLUMN "grace_days" integer DEFAULT 7 NOT NULL;--> statement-breakpoint +ALTER TABLE "tos_user_state" ADD CONSTRAINT "tos_user_state_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "tos_user_state" ADD CONSTRAINT "tos_user_state_tos_version_id_tos_version_id_fk" FOREIGN KEY ("tos_version_id") REFERENCES "public"."tos_version"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "tos_user_state_user_idx" ON "tos_user_state" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "tos_user_state_grace_until_idx" ON "tos_user_state" USING btree ("grace_until"); \ No newline at end of file diff --git a/drizzle/0037_clean_vermin.sql b/drizzle/0037_clean_vermin.sql new file mode 100644 index 00000000..6be472dc --- /dev/null +++ b/drizzle/0037_clean_vermin.sql @@ -0,0 +1,6 @@ +DROP INDEX "tos_user_state_grace_until_idx";--> statement-breakpoint +ALTER TABLE "tos_version" ADD COLUMN "accept_by" timestamp with time zone NOT NULL;--> statement-breakpoint +CREATE INDEX "tos_version_accept_by_idx" ON "tos_version" USING btree ("accept_by");--> statement-breakpoint +ALTER TABLE "tos_user_state" DROP COLUMN "first_seen_at";--> statement-breakpoint +ALTER TABLE "tos_user_state" DROP COLUMN "grace_until";--> statement-breakpoint +ALTER TABLE "tos_version" DROP COLUMN "grace_days"; \ No newline at end of file diff --git a/drizzle/meta/0033_snapshot.json b/drizzle/meta/0033_snapshot.json index c3d49cc0..5fc86266 100644 --- a/drizzle/meta/0033_snapshot.json +++ b/drizzle/meta/0033_snapshot.json @@ -1,5 +1,5 @@ { - "id": "2df45cfc-982c-4c7c-b2e7-6fe47039d121", + "id": "2b328f08-9cdd-4a4f-a611-cd519981dd08", "prevId": "d10e9d1c-dc82-4f0a-b93c-8810a746fe55", "version": "7", "dialect": "postgresql", @@ -175,28 +175,28 @@ "device_to_location_device_id_device_id_fk": { "name": "device_to_location_device_id_device_id_fk", "tableFrom": "device_to_location", + "tableTo": "device", "columnsFrom": [ "device_id" ], - "tableTo": "device", "columnsTo": [ "id" ], - "onUpdate": "cascade", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "cascade" }, "device_to_location_location_id_location_id_fk": { "name": "device_to_location_location_id_location_id_fk", "tableFrom": "device_to_location", + "tableTo": "location", "columnsFrom": [ "location_id" ], - "tableTo": "location", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "no action" + "onDelete": "no action", + "onUpdate": "no action" } }, "compositePrimaryKeys": { @@ -212,12 +212,12 @@ "uniqueConstraints": { "device_to_location_device_id_location_id_time_unique": { "name": "device_to_location_device_id_location_id_time_unique", + "nullsNotDistinct": false, "columns": [ "device_id", "location_id", "time" - ], - "nullsNotDistinct": false + ] } }, "policies": {}, @@ -259,26 +259,26 @@ "measurement_location_id_location_id_fk": { "name": "measurement_location_id_location_id_fk", "tableFrom": "measurement", + "tableTo": "location", "columnsFrom": [ "location_id" ], - "tableTo": "location", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "no action" + "onDelete": "no action", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, "uniqueConstraints": { "measurement_sensor_id_time_unique": { "name": "measurement_sensor_id_time_unique", + "nullsNotDistinct": false, "columns": [ "sensor_id", "time" - ], - "nullsNotDistinct": false + ] } }, "policies": {}, @@ -307,15 +307,15 @@ "password_user_id_user_id_fk": { "name": "password_user_id_user_id_fk", "tableFrom": "password", + "tableTo": "user", "columnsFrom": [ "user_id" ], - "tableTo": "user", "columnsTo": [ "id" ], - "onUpdate": "cascade", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "cascade" } }, "compositePrimaryKeys": {}, @@ -352,25 +352,25 @@ "password_reset_request_user_id_user_id_fk": { "name": "password_reset_request_user_id_user_id_fk", "tableFrom": "password_reset_request", + "tableTo": "user", "columnsFrom": [ "user_id" ], - "tableTo": "user", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, "uniqueConstraints": { "password_reset_request_user_id_unique": { "name": "password_reset_request_user_id_unique", + "nullsNotDistinct": false, "columns": [ "user_id" - ], - "nullsNotDistinct": false + ] } }, "policies": {}, @@ -412,25 +412,25 @@ "profile_user_id_user_id_fk": { "name": "profile_user_id_user_id_fk", "tableFrom": "profile", + "tableTo": "user", "columnsFrom": [ "user_id" ], - "tableTo": "user", "columnsTo": [ "id" ], - "onUpdate": "cascade", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "cascade" } }, "compositePrimaryKeys": {}, "uniqueConstraints": { "profile_username_unique": { "name": "profile_username_unique", + "nullsNotDistinct": false, "columns": [ "username" - ], - "nullsNotDistinct": false + ] } }, "policies": {}, @@ -491,15 +491,15 @@ "profile_image_profile_id_profile_id_fk": { "name": "profile_image_profile_id_profile_id_fk", "tableFrom": "profile_image", + "tableTo": "profile", "columnsFrom": [ "profile_id" ], - "tableTo": "profile", "columnsTo": [ "id" ], - "onUpdate": "cascade", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "cascade" } }, "compositePrimaryKeys": {}, @@ -606,15 +606,15 @@ "sensor_device_id_device_id_fk": { "name": "sensor_device_id_device_id_fk", "tableFrom": "sensor", + "tableTo": "device", "columnsFrom": [ "device_id" ], - "tableTo": "device", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -699,17 +699,17 @@ "uniqueConstraints": { "user_email_unique": { "name": "user_email_unique", + "nullsNotDistinct": false, "columns": [ "email" - ], - "nullsNotDistinct": false + ] }, "user_unconfirmed_email_unique": { "name": "user_unconfirmed_email_unique", + "nullsNotDistinct": false, "columns": [ "unconfirmed_email" - ], - "nullsNotDistinct": false + ] } }, "policies": {}, @@ -745,9 +745,9 @@ } ], "isUnique": false, - "with": {}, + "concurrently": false, "method": "gist", - "concurrently": false + "with": {} } }, "foreignKeys": {}, @@ -755,10 +755,10 @@ "uniqueConstraints": { "location_location_unique": { "name": "location_location_unique", + "nullsNotDistinct": false, "columns": [ "location" - ], - "nullsNotDistinct": false + ] } }, "policies": {}, @@ -838,15 +838,15 @@ "refresh_token_user_id_user_id_fk": { "name": "refresh_token_user_id_user_id_fk", "tableFrom": "refresh_token", + "tableTo": "user", "columnsFrom": [ "user_id" ], - "tableTo": "user", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -941,34 +941,34 @@ } ], "isUnique": false, - "with": {}, + "concurrently": false, "method": "btree", - "concurrently": false + "with": {} } }, "foreignKeys": { "claim_box_id_device_id_fk": { "name": "claim_box_id_device_id_fk", "tableFrom": "claim", + "tableTo": "device", "columnsFrom": [ "box_id" ], - "tableTo": "device", "columnsTo": [ "id" ], - "onUpdate": "no action", - "onDelete": "cascade" + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, "uniqueConstraints": { "unique_box_id": { "name": "unique_box_id", + "nullsNotDistinct": false, "columns": [ "box_id" - ], - "nullsNotDistinct": false + ] } }, "policies": {}, @@ -1042,10 +1042,10 @@ "uniqueConstraints": { "integration_slug_unique": { "name": "integration_slug_unique", + "nullsNotDistinct": false, "columns": [ "slug" - ], - "nullsNotDistinct": false + ] } }, "policies": {}, @@ -1097,10 +1097,11 @@ } }, "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, "views": { "public.measurement_10min": { - "name": "measurement_10min", - "schema": "public", "columns": { "sensor_id": { "name": "sensor_id", @@ -1139,12 +1140,12 @@ "notNull": false } }, - "materialized": true, - "isExisting": true + "name": "measurement_10min", + "schema": "public", + "isExisting": true, + "materialized": true }, "public.measurement_1day": { - "name": "measurement_1day", - "schema": "public", "columns": { "sensor_id": { "name": "sensor_id", @@ -1183,12 +1184,12 @@ "notNull": false } }, - "materialized": true, - "isExisting": true + "name": "measurement_1day", + "schema": "public", + "isExisting": true, + "materialized": true }, "public.measurement_1hour": { - "name": "measurement_1hour", - "schema": "public", "columns": { "sensor_id": { "name": "sensor_id", @@ -1227,12 +1228,12 @@ "notNull": false } }, - "materialized": true, - "isExisting": true + "name": "measurement_1hour", + "schema": "public", + "isExisting": true, + "materialized": true }, "public.measurement_1month": { - "name": "measurement_1month", - "schema": "public", "columns": { "sensor_id": { "name": "sensor_id", @@ -1271,12 +1272,12 @@ "notNull": false } }, - "materialized": true, - "isExisting": true + "name": "measurement_1month", + "schema": "public", + "isExisting": true, + "materialized": true }, "public.measurement_1year": { - "name": "measurement_1year", - "schema": "public", "columns": { "sensor_id": { "name": "sensor_id", @@ -1315,13 +1316,12 @@ "notNull": false } }, - "materialized": true, - "isExisting": true + "name": "measurement_1year", + "schema": "public", + "isExisting": true, + "materialized": true } }, - "sequences": {}, - "roles": {}, - "policies": {}, "_meta": { "columns": {}, "schemas": {}, diff --git a/drizzle/meta/0034_snapshot.json b/drizzle/meta/0034_snapshot.json new file mode 100644 index 00000000..81eb5c96 --- /dev/null +++ b/drizzle/meta/0034_snapshot.json @@ -0,0 +1,1344 @@ +{ + "id": "0b3c20ee-7221-4347-87f2-acfbc296d0d0", + "prevId": "2b328f08-9cdd-4a4f-a611-cd519981dd08", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.device": { + "name": "device", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "ARRAY[]::text[]" + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_auth": { + "name": "use_auth", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "apiKey": { + "name": "apiKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "exposure": { + "name": "exposure", + "type": "exposure", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'inactive'" + }, + "model": { + "name": "model", + "type": "model", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'custom'" + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "latitude": { + "name": "latitude", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "longitude": { + "name": "longitude", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "sensor_wiki_model": { + "name": "sensor_wiki_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "device_user_id_user_id_fk": { + "name": "device_user_id_user_id_fk", + "tableFrom": "device", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_to_location": { + "name": "device_to_location", + "schema": "", + "columns": { + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "location_id": { + "name": "location_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "time": { + "name": "time", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "device_to_location_device_id_device_id_fk": { + "name": "device_to_location_device_id_device_id_fk", + "tableFrom": "device_to_location", + "tableTo": "device", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "device_to_location_location_id_location_id_fk": { + "name": "device_to_location_location_id_location_id_fk", + "tableFrom": "device_to_location", + "tableTo": "location", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "device_to_location_device_id_location_id_time_pk": { + "name": "device_to_location_device_id_location_id_time_pk", + "columns": [ + "device_id", + "location_id", + "time" + ] + } + }, + "uniqueConstraints": { + "device_to_location_device_id_location_id_time_unique": { + "name": "device_to_location_device_id_location_id_time_unique", + "nullsNotDistinct": false, + "columns": [ + "device_id", + "location_id", + "time" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.measurement": { + "name": "measurement", + "schema": "", + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "value": { + "name": "value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "location_id": { + "name": "location_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "measurement_location_id_location_id_fk": { + "name": "measurement_location_id_location_id_fk", + "tableFrom": "measurement", + "tableTo": "location", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "measurement_sensor_id_time_unique": { + "name": "measurement_sensor_id_time_unique", + "nullsNotDistinct": false, + "columns": [ + "sensor_id", + "time" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.password": { + "name": "password", + "schema": "", + "columns": { + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "password_user_id_user_id_fk": { + "name": "password_user_id_user_id_fk", + "tableFrom": "password", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.password_reset_request": { + "name": "password_reset_request", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "password_reset_request_user_id_user_id_fk": { + "name": "password_reset_request_user_id_user_id_fk", + "tableFrom": "password_reset_request", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "password_reset_request_user_id_unique": { + "name": "password_reset_request_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile": { + "name": "profile", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "profile_user_id_user_id_fk": { + "name": "profile_user_id_user_id_fk", + "tableFrom": "profile", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "profile_username_unique": { + "name": "profile_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile_image": { + "name": "profile_image", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "alt_text": { + "name": "alt_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "blob": { + "name": "blob", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "profile_image_profile_id_profile_id_fk": { + "name": "profile_image_profile_id_profile_id_fk", + "tableFrom": "profile_image", + "tableTo": "profile", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sensor": { + "name": "sensor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sensor_type": { + "name": "sensor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'inactive'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sensor_wiki_type": { + "name": "sensor_wiki_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sensor_wiki_phenomenon": { + "name": "sensor_wiki_phenomenon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sensor_wiki_unit": { + "name": "sensor_wiki_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lastMeasurement": { + "name": "lastMeasurement", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "sensor_device_id_device_id_fk": { + "name": "sensor_device_id_device_id_fk", + "tableFrom": "sensor", + "tableTo": "device", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "unconfirmed_email": { + "name": "unconfirmed_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'en_US'" + }, + "email_is_confirmed": { + "name": "email_is_confirmed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "email_confirmation_token": { + "name": "email_confirmation_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_unconfirmed_email_unique": { + "name": "user_unconfirmed_email_unique", + "nullsNotDistinct": false, + "columns": [ + "unconfirmed_email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.location": { + "name": "location", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "location": { + "name": "location", + "type": "geometry(point)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "location_index": { + "name": "location_index", + "columns": [ + { + "expression": "location", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gist", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "location_location_unique": { + "name": "location_location_unique", + "nullsNotDistinct": false, + "columns": [ + "location" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.log_entry": { + "name": "log_entry", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.refresh_token": { + "name": "refresh_token", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "refresh_token_user_id_user_id_fk": { + "name": "refresh_token_user_id_user_id_fk", + "tableFrom": "refresh_token", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.token_revocation": { + "name": "token_revocation", + "schema": "", + "columns": { + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.claim": { + "name": "claim", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "box_id": { + "name": "box_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "claim_expires_at_idx": { + "name": "claim_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "claim_box_id_device_id_fk": { + "name": "claim_box_id_device_id_fk", + "tableFrom": "claim", + "tableTo": "device", + "columnsFrom": [ + "box_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_box_id": { + "name": "unique_box_id", + "nullsNotDistinct": false, + "columns": [ + "box_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration": { + "name": "integration", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_url": { + "name": "service_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_key": { + "name": "service_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "integration_slug_unique": { + "name": "integration_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.exposure": { + "name": "exposure", + "schema": "public", + "values": [ + "indoor", + "outdoor", + "mobile", + "unknown" + ] + }, + "public.model": { + "name": "model", + "schema": "public", + "values": [ + "homeV2Lora", + "homeV2Ethernet", + "homeV2Wifi", + "homeEthernet", + "homeWifi", + "homeEthernetFeinstaub", + "homeWifiFeinstaub", + "luftdaten_sds011", + "luftdaten_sds011_dht11", + "luftdaten_sds011_dht22", + "luftdaten_sds011_bmp180", + "luftdaten_sds011_bme280", + "hackair_home_v2", + "senseBox:Edu", + "luftdaten.info", + "custom" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": [ + "active", + "inactive", + "old" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": { + "public.measurement_10min": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_10min", + "schema": "public", + "isExisting": true, + "materialized": true + }, + "public.measurement_1day": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_1day", + "schema": "public", + "isExisting": true, + "materialized": true + }, + "public.measurement_1hour": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_1hour", + "schema": "public", + "isExisting": true, + "materialized": true + }, + "public.measurement_1month": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_1month", + "schema": "public", + "isExisting": true, + "materialized": true + }, + "public.measurement_1year": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_1year", + "schema": "public", + "isExisting": true, + "materialized": true + } + }, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0035_snapshot.json b/drizzle/meta/0035_snapshot.json new file mode 100644 index 00000000..ed5d4127 --- /dev/null +++ b/drizzle/meta/0035_snapshot.json @@ -0,0 +1,1534 @@ +{ + "id": "0c229292-a4d0-4722-952c-8e92e9fe6b7a", + "prevId": "0b3c20ee-7221-4347-87f2-acfbc296d0d0", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.device": { + "name": "device", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "ARRAY[]::text[]" + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_auth": { + "name": "use_auth", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "apiKey": { + "name": "apiKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "exposure": { + "name": "exposure", + "type": "exposure", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'inactive'" + }, + "model": { + "name": "model", + "type": "model", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'custom'" + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "latitude": { + "name": "latitude", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "longitude": { + "name": "longitude", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "sensor_wiki_model": { + "name": "sensor_wiki_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "device_user_id_user_id_fk": { + "name": "device_user_id_user_id_fk", + "tableFrom": "device", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_to_location": { + "name": "device_to_location", + "schema": "", + "columns": { + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "location_id": { + "name": "location_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "time": { + "name": "time", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "device_to_location_device_id_device_id_fk": { + "name": "device_to_location_device_id_device_id_fk", + "tableFrom": "device_to_location", + "tableTo": "device", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "device_to_location_location_id_location_id_fk": { + "name": "device_to_location_location_id_location_id_fk", + "tableFrom": "device_to_location", + "tableTo": "location", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "device_to_location_device_id_location_id_time_pk": { + "name": "device_to_location_device_id_location_id_time_pk", + "columns": [ + "device_id", + "location_id", + "time" + ] + } + }, + "uniqueConstraints": { + "device_to_location_device_id_location_id_time_unique": { + "name": "device_to_location_device_id_location_id_time_unique", + "nullsNotDistinct": false, + "columns": [ + "device_id", + "location_id", + "time" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.measurement": { + "name": "measurement", + "schema": "", + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "value": { + "name": "value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "location_id": { + "name": "location_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "measurement_location_id_location_id_fk": { + "name": "measurement_location_id_location_id_fk", + "tableFrom": "measurement", + "tableTo": "location", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "measurement_sensor_id_time_unique": { + "name": "measurement_sensor_id_time_unique", + "nullsNotDistinct": false, + "columns": [ + "sensor_id", + "time" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.password": { + "name": "password", + "schema": "", + "columns": { + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "password_user_id_user_id_fk": { + "name": "password_user_id_user_id_fk", + "tableFrom": "password", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.password_reset_request": { + "name": "password_reset_request", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "password_reset_request_user_id_user_id_fk": { + "name": "password_reset_request_user_id_user_id_fk", + "tableFrom": "password_reset_request", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "password_reset_request_user_id_unique": { + "name": "password_reset_request_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile": { + "name": "profile", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "profile_user_id_user_id_fk": { + "name": "profile_user_id_user_id_fk", + "tableFrom": "profile", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "profile_username_unique": { + "name": "profile_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile_image": { + "name": "profile_image", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "alt_text": { + "name": "alt_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "blob": { + "name": "blob", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "profile_image_profile_id_profile_id_fk": { + "name": "profile_image_profile_id_profile_id_fk", + "tableFrom": "profile_image", + "tableTo": "profile", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sensor": { + "name": "sensor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sensor_type": { + "name": "sensor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'inactive'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sensor_wiki_type": { + "name": "sensor_wiki_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sensor_wiki_phenomenon": { + "name": "sensor_wiki_phenomenon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sensor_wiki_unit": { + "name": "sensor_wiki_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lastMeasurement": { + "name": "lastMeasurement", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "sensor_device_id_device_id_fk": { + "name": "sensor_device_id_device_id_fk", + "tableFrom": "sensor", + "tableTo": "device", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "unconfirmed_email": { + "name": "unconfirmed_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'en_US'" + }, + "email_is_confirmed": { + "name": "email_is_confirmed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "email_confirmation_token": { + "name": "email_confirmation_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "accepted_tos_version_id": { + "name": "accepted_tos_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accepted_tos_at": { + "name": "accepted_tos_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_accepted_tos_version_id_tos_version_id_fk": { + "name": "user_accepted_tos_version_id_tos_version_id_fk", + "tableFrom": "user", + "tableTo": "tos_version", + "columnsFrom": [ + "accepted_tos_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_unconfirmed_email_unique": { + "name": "user_unconfirmed_email_unique", + "nullsNotDistinct": false, + "columns": [ + "unconfirmed_email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.location": { + "name": "location", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "location": { + "name": "location", + "type": "geometry(point)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "location_index": { + "name": "location_index", + "columns": [ + { + "expression": "location", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gist", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "location_location_unique": { + "name": "location_location_unique", + "nullsNotDistinct": false, + "columns": [ + "location" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.log_entry": { + "name": "log_entry", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.refresh_token": { + "name": "refresh_token", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "refresh_token_user_id_user_id_fk": { + "name": "refresh_token_user_id_user_id_fk", + "tableFrom": "refresh_token", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.token_revocation": { + "name": "token_revocation", + "schema": "", + "columns": { + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.claim": { + "name": "claim", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "box_id": { + "name": "box_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "claim_expires_at_idx": { + "name": "claim_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "claim_box_id_device_id_fk": { + "name": "claim_box_id_device_id_fk", + "tableFrom": "claim", + "tableTo": "device", + "columnsFrom": [ + "box_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_box_id": { + "name": "unique_box_id", + "nullsNotDistinct": false, + "columns": [ + "box_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration": { + "name": "integration", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_url": { + "name": "service_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_key": { + "name": "service_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "integration_slug_unique": { + "name": "integration_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tos_acceptance": { + "name": "tos_acceptance", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tos_version_id": { + "name": "tos_version_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tos_acceptance_user_idx": { + "name": "tos_acceptance_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tos_acceptance_user_id_user_id_fk": { + "name": "tos_acceptance_user_id_user_id_fk", + "tableFrom": "tos_acceptance", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tos_acceptance_tos_version_id_tos_version_id_fk": { + "name": "tos_acceptance_tos_version_id_tos_version_id_fk", + "tableFrom": "tos_acceptance", + "tableTo": "tos_version", + "columnsFrom": [ + "tos_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tos_acceptance_user_id_tos_version_id_pk": { + "name": "tos_acceptance_user_id_tos_version_id_pk", + "columns": [ + "user_id", + "tos_version_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tos_version": { + "name": "tos_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effective_from": { + "name": "effective_from", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tos_version_effective_from_idx": { + "name": "tos_version_effective_from_idx", + "columns": [ + { + "expression": "effective_from", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tos_version_version_unique": { + "name": "tos_version_version_unique", + "nullsNotDistinct": false, + "columns": [ + "version" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.exposure": { + "name": "exposure", + "schema": "public", + "values": [ + "indoor", + "outdoor", + "mobile", + "unknown" + ] + }, + "public.model": { + "name": "model", + "schema": "public", + "values": [ + "homeV2Lora", + "homeV2Ethernet", + "homeV2Wifi", + "homeEthernet", + "homeWifi", + "homeEthernetFeinstaub", + "homeWifiFeinstaub", + "luftdaten_sds011", + "luftdaten_sds011_dht11", + "luftdaten_sds011_dht22", + "luftdaten_sds011_bmp180", + "luftdaten_sds011_bme280", + "hackair_home_v2", + "senseBox:Edu", + "luftdaten.info", + "custom" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": [ + "active", + "inactive", + "old" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": { + "public.measurement_10min": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_10min", + "schema": "public", + "isExisting": true, + "materialized": true + }, + "public.measurement_1day": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_1day", + "schema": "public", + "isExisting": true, + "materialized": true + }, + "public.measurement_1hour": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_1hour", + "schema": "public", + "isExisting": true, + "materialized": true + }, + "public.measurement_1month": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_1month", + "schema": "public", + "isExisting": true, + "materialized": true + }, + "public.measurement_1year": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_1year", + "schema": "public", + "isExisting": true, + "materialized": true + } + }, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0036_snapshot.json b/drizzle/meta/0036_snapshot.json new file mode 100644 index 00000000..c98ec824 --- /dev/null +++ b/drizzle/meta/0036_snapshot.json @@ -0,0 +1,1568 @@ +{ + "id": "9bfbaaa3-6bf2-419d-abdc-635627ef272b", + "prevId": "0c229292-a4d0-4722-952c-8e92e9fe6b7a", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.device": { + "name": "device", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "ARRAY[]::text[]" + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_auth": { + "name": "use_auth", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "apiKey": { + "name": "apiKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "exposure": { + "name": "exposure", + "type": "exposure", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'inactive'" + }, + "model": { + "name": "model", + "type": "model", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'custom'" + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "latitude": { + "name": "latitude", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "longitude": { + "name": "longitude", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "sensor_wiki_model": { + "name": "sensor_wiki_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "device_user_id_user_id_fk": { + "name": "device_user_id_user_id_fk", + "tableFrom": "device", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_to_location": { + "name": "device_to_location", + "schema": "", + "columns": { + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "location_id": { + "name": "location_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "time": { + "name": "time", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "device_to_location_device_id_device_id_fk": { + "name": "device_to_location_device_id_device_id_fk", + "tableFrom": "device_to_location", + "tableTo": "device", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "device_to_location_location_id_location_id_fk": { + "name": "device_to_location_location_id_location_id_fk", + "tableFrom": "device_to_location", + "tableTo": "location", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "device_to_location_device_id_location_id_time_pk": { + "name": "device_to_location_device_id_location_id_time_pk", + "columns": [ + "device_id", + "location_id", + "time" + ] + } + }, + "uniqueConstraints": { + "device_to_location_device_id_location_id_time_unique": { + "name": "device_to_location_device_id_location_id_time_unique", + "nullsNotDistinct": false, + "columns": [ + "device_id", + "location_id", + "time" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.measurement": { + "name": "measurement", + "schema": "", + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "value": { + "name": "value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "location_id": { + "name": "location_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "measurement_location_id_location_id_fk": { + "name": "measurement_location_id_location_id_fk", + "tableFrom": "measurement", + "tableTo": "location", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "measurement_sensor_id_time_unique": { + "name": "measurement_sensor_id_time_unique", + "nullsNotDistinct": false, + "columns": [ + "sensor_id", + "time" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.password": { + "name": "password", + "schema": "", + "columns": { + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "password_user_id_user_id_fk": { + "name": "password_user_id_user_id_fk", + "tableFrom": "password", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.password_reset_request": { + "name": "password_reset_request", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "password_reset_request_user_id_user_id_fk": { + "name": "password_reset_request_user_id_user_id_fk", + "tableFrom": "password_reset_request", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "password_reset_request_user_id_unique": { + "name": "password_reset_request_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile": { + "name": "profile", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "profile_user_id_user_id_fk": { + "name": "profile_user_id_user_id_fk", + "tableFrom": "profile", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "profile_username_unique": { + "name": "profile_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile_image": { + "name": "profile_image", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "alt_text": { + "name": "alt_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "blob": { + "name": "blob", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "profile_image_profile_id_profile_id_fk": { + "name": "profile_image_profile_id_profile_id_fk", + "tableFrom": "profile_image", + "tableTo": "profile", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sensor": { + "name": "sensor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sensor_type": { + "name": "sensor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'inactive'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sensor_wiki_type": { + "name": "sensor_wiki_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sensor_wiki_phenomenon": { + "name": "sensor_wiki_phenomenon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sensor_wiki_unit": { + "name": "sensor_wiki_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lastMeasurement": { + "name": "lastMeasurement", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "sensor_device_id_device_id_fk": { + "name": "sensor_device_id_device_id_fk", + "tableFrom": "sensor", + "tableTo": "device", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "unconfirmed_email": { + "name": "unconfirmed_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'en_US'" + }, + "email_is_confirmed": { + "name": "email_is_confirmed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "email_confirmation_token": { + "name": "email_confirmation_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "accepted_tos_version_id": { + "name": "accepted_tos_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accepted_tos_at": { + "name": "accepted_tos_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_accepted_tos_version_id_tos_version_id_fk": { + "name": "user_accepted_tos_version_id_tos_version_id_fk", + "tableFrom": "user", + "tableTo": "tos_version", + "columnsFrom": [ + "accepted_tos_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_unconfirmed_email_unique": { + "name": "user_unconfirmed_email_unique", + "nullsNotDistinct": false, + "columns": [ + "unconfirmed_email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.location": { + "name": "location", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "location": { + "name": "location", + "type": "geometry(point)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "location_index": { + "name": "location_index", + "columns": [ + { + "expression": "location", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gist", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "location_location_unique": { + "name": "location_location_unique", + "nullsNotDistinct": false, + "columns": [ + "location" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.log_entry": { + "name": "log_entry", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.refresh_token": { + "name": "refresh_token", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "refresh_token_user_id_user_id_fk": { + "name": "refresh_token_user_id_user_id_fk", + "tableFrom": "refresh_token", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.token_revocation": { + "name": "token_revocation", + "schema": "", + "columns": { + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.claim": { + "name": "claim", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "box_id": { + "name": "box_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "claim_expires_at_idx": { + "name": "claim_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "claim_box_id_device_id_fk": { + "name": "claim_box_id_device_id_fk", + "tableFrom": "claim", + "tableTo": "device", + "columnsFrom": [ + "box_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_box_id": { + "name": "unique_box_id", + "nullsNotDistinct": false, + "columns": [ + "box_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration": { + "name": "integration", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_url": { + "name": "service_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_key": { + "name": "service_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "integration_slug_unique": { + "name": "integration_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tos_user_state": { + "name": "tos_user_state", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tos_version_id": { + "name": "tos_version_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "grace_until": { + "name": "grace_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "tos_user_state_user_idx": { + "name": "tos_user_state_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tos_user_state_grace_until_idx": { + "name": "tos_user_state_grace_until_idx", + "columns": [ + { + "expression": "grace_until", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tos_user_state_user_id_user_id_fk": { + "name": "tos_user_state_user_id_user_id_fk", + "tableFrom": "tos_user_state", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tos_user_state_tos_version_id_tos_version_id_fk": { + "name": "tos_user_state_tos_version_id_tos_version_id_fk", + "tableFrom": "tos_user_state", + "tableTo": "tos_version", + "columnsFrom": [ + "tos_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tos_user_state_user_id_tos_version_id_pk": { + "name": "tos_user_state_user_id_tos_version_id_pk", + "columns": [ + "user_id", + "tos_version_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tos_version": { + "name": "tos_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effective_from": { + "name": "effective_from", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "grace_days": { + "name": "grace_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 7 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tos_version_effective_from_idx": { + "name": "tos_version_effective_from_idx", + "columns": [ + { + "expression": "effective_from", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tos_version_version_unique": { + "name": "tos_version_version_unique", + "nullsNotDistinct": false, + "columns": [ + "version" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.exposure": { + "name": "exposure", + "schema": "public", + "values": [ + "indoor", + "outdoor", + "mobile", + "unknown" + ] + }, + "public.model": { + "name": "model", + "schema": "public", + "values": [ + "homeV2Lora", + "homeV2Ethernet", + "homeV2Wifi", + "homeEthernet", + "homeWifi", + "homeEthernetFeinstaub", + "homeWifiFeinstaub", + "luftdaten_sds011", + "luftdaten_sds011_dht11", + "luftdaten_sds011_dht22", + "luftdaten_sds011_bmp180", + "luftdaten_sds011_bme280", + "hackair_home_v2", + "senseBox:Edu", + "luftdaten.info", + "custom" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": [ + "active", + "inactive", + "old" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": { + "public.measurement_10min": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_10min", + "schema": "public", + "isExisting": true, + "materialized": true + }, + "public.measurement_1day": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_1day", + "schema": "public", + "isExisting": true, + "materialized": true + }, + "public.measurement_1hour": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_1hour", + "schema": "public", + "isExisting": true, + "materialized": true + }, + "public.measurement_1month": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_1month", + "schema": "public", + "isExisting": true, + "materialized": true + }, + "public.measurement_1year": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_1year", + "schema": "public", + "isExisting": true, + "materialized": true + } + }, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0037_snapshot.json b/drizzle/meta/0037_snapshot.json new file mode 100644 index 00000000..74016e43 --- /dev/null +++ b/drizzle/meta/0037_snapshot.json @@ -0,0 +1,1554 @@ +{ + "id": "0613a712-e3b2-4c4d-8559-2c44e54cbe36", + "prevId": "9bfbaaa3-6bf2-419d-abdc-635627ef272b", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.device": { + "name": "device", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "ARRAY[]::text[]" + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_auth": { + "name": "use_auth", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "apiKey": { + "name": "apiKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "exposure": { + "name": "exposure", + "type": "exposure", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'inactive'" + }, + "model": { + "name": "model", + "type": "model", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'custom'" + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "latitude": { + "name": "latitude", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "longitude": { + "name": "longitude", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "sensor_wiki_model": { + "name": "sensor_wiki_model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "device_user_id_user_id_fk": { + "name": "device_user_id_user_id_fk", + "tableFrom": "device", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_to_location": { + "name": "device_to_location", + "schema": "", + "columns": { + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "location_id": { + "name": "location_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "time": { + "name": "time", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "device_to_location_device_id_device_id_fk": { + "name": "device_to_location_device_id_device_id_fk", + "tableFrom": "device_to_location", + "tableTo": "device", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "device_to_location_location_id_location_id_fk": { + "name": "device_to_location_location_id_location_id_fk", + "tableFrom": "device_to_location", + "tableTo": "location", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "device_to_location_device_id_location_id_time_pk": { + "name": "device_to_location_device_id_location_id_time_pk", + "columns": [ + "device_id", + "location_id", + "time" + ] + } + }, + "uniqueConstraints": { + "device_to_location_device_id_location_id_time_unique": { + "name": "device_to_location_device_id_location_id_time_unique", + "nullsNotDistinct": false, + "columns": [ + "device_id", + "location_id", + "time" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.measurement": { + "name": "measurement", + "schema": "", + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "value": { + "name": "value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "location_id": { + "name": "location_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "measurement_location_id_location_id_fk": { + "name": "measurement_location_id_location_id_fk", + "tableFrom": "measurement", + "tableTo": "location", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "measurement_sensor_id_time_unique": { + "name": "measurement_sensor_id_time_unique", + "nullsNotDistinct": false, + "columns": [ + "sensor_id", + "time" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.password": { + "name": "password", + "schema": "", + "columns": { + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "password_user_id_user_id_fk": { + "name": "password_user_id_user_id_fk", + "tableFrom": "password", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.password_reset_request": { + "name": "password_reset_request", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "password_reset_request_user_id_user_id_fk": { + "name": "password_reset_request_user_id_user_id_fk", + "tableFrom": "password_reset_request", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "password_reset_request_user_id_unique": { + "name": "password_reset_request_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile": { + "name": "profile", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "profile_user_id_user_id_fk": { + "name": "profile_user_id_user_id_fk", + "tableFrom": "profile", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "profile_username_unique": { + "name": "profile_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile_image": { + "name": "profile_image", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "alt_text": { + "name": "alt_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "blob": { + "name": "blob", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "profile_image_profile_id_profile_id_fk": { + "name": "profile_image_profile_id_profile_id_fk", + "tableFrom": "profile_image", + "tableTo": "profile", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sensor": { + "name": "sensor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sensor_type": { + "name": "sensor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'inactive'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sensor_wiki_type": { + "name": "sensor_wiki_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sensor_wiki_phenomenon": { + "name": "sensor_wiki_phenomenon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sensor_wiki_unit": { + "name": "sensor_wiki_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lastMeasurement": { + "name": "lastMeasurement", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "sensor_device_id_device_id_fk": { + "name": "sensor_device_id_device_id_fk", + "tableFrom": "sensor", + "tableTo": "device", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "unconfirmed_email": { + "name": "unconfirmed_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'en_US'" + }, + "email_is_confirmed": { + "name": "email_is_confirmed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "email_confirmation_token": { + "name": "email_confirmation_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "accepted_tos_version_id": { + "name": "accepted_tos_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accepted_tos_at": { + "name": "accepted_tos_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_accepted_tos_version_id_tos_version_id_fk": { + "name": "user_accepted_tos_version_id_tos_version_id_fk", + "tableFrom": "user", + "tableTo": "tos_version", + "columnsFrom": [ + "accepted_tos_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_unconfirmed_email_unique": { + "name": "user_unconfirmed_email_unique", + "nullsNotDistinct": false, + "columns": [ + "unconfirmed_email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.location": { + "name": "location", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "location": { + "name": "location", + "type": "geometry(point)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "location_index": { + "name": "location_index", + "columns": [ + { + "expression": "location", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gist", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "location_location_unique": { + "name": "location_location_unique", + "nullsNotDistinct": false, + "columns": [ + "location" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.log_entry": { + "name": "log_entry", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.refresh_token": { + "name": "refresh_token", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "refresh_token_user_id_user_id_fk": { + "name": "refresh_token_user_id_user_id_fk", + "tableFrom": "refresh_token", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.token_revocation": { + "name": "token_revocation", + "schema": "", + "columns": { + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.claim": { + "name": "claim", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "box_id": { + "name": "box_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "claim_expires_at_idx": { + "name": "claim_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "claim_box_id_device_id_fk": { + "name": "claim_box_id_device_id_fk", + "tableFrom": "claim", + "tableTo": "device", + "columnsFrom": [ + "box_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_box_id": { + "name": "unique_box_id", + "nullsNotDistinct": false, + "columns": [ + "box_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration": { + "name": "integration", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_url": { + "name": "service_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_key": { + "name": "service_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "integration_slug_unique": { + "name": "integration_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tos_user_state": { + "name": "tos_user_state", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tos_version_id": { + "name": "tos_version_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "tos_user_state_user_idx": { + "name": "tos_user_state_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tos_user_state_user_id_user_id_fk": { + "name": "tos_user_state_user_id_user_id_fk", + "tableFrom": "tos_user_state", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tos_user_state_tos_version_id_tos_version_id_fk": { + "name": "tos_user_state_tos_version_id_tos_version_id_fk", + "tableFrom": "tos_user_state", + "tableTo": "tos_version", + "columnsFrom": [ + "tos_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tos_user_state_user_id_tos_version_id_pk": { + "name": "tos_user_state_user_id_tos_version_id_pk", + "columns": [ + "user_id", + "tos_version_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tos_version": { + "name": "tos_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effective_from": { + "name": "effective_from", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "accept_by": { + "name": "accept_by", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tos_version_effective_from_idx": { + "name": "tos_version_effective_from_idx", + "columns": [ + { + "expression": "effective_from", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tos_version_accept_by_idx": { + "name": "tos_version_accept_by_idx", + "columns": [ + { + "expression": "accept_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tos_version_version_unique": { + "name": "tos_version_version_unique", + "nullsNotDistinct": false, + "columns": [ + "version" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.exposure": { + "name": "exposure", + "schema": "public", + "values": [ + "indoor", + "outdoor", + "mobile", + "unknown" + ] + }, + "public.model": { + "name": "model", + "schema": "public", + "values": [ + "homeV2Lora", + "homeV2Ethernet", + "homeV2Wifi", + "homeEthernet", + "homeWifi", + "homeEthernetFeinstaub", + "homeWifiFeinstaub", + "luftdaten_sds011", + "luftdaten_sds011_dht11", + "luftdaten_sds011_dht22", + "luftdaten_sds011_bmp180", + "luftdaten_sds011_bme280", + "hackair_home_v2", + "senseBox:Edu", + "luftdaten.info", + "custom" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": [ + "active", + "inactive", + "old" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": { + "public.measurement_10min": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_10min", + "schema": "public", + "isExisting": true, + "materialized": true + }, + "public.measurement_1day": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_1day", + "schema": "public", + "isExisting": true, + "materialized": true + }, + "public.measurement_1hour": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_1hour", + "schema": "public", + "isExisting": true, + "materialized": true + }, + "public.measurement_1month": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_1month", + "schema": "public", + "isExisting": true, + "materialized": true + }, + "public.measurement_1year": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_1year", + "schema": "public", + "isExisting": true, + "materialized": true + } + }, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 07a2283d..fb652724 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -239,6 +239,34 @@ "when": 1772009671134, "tag": "0033_caag-retention-policies", "breakpoints": true + }, + { + "idx": 34, + "version": "7", + "when": 1773047162857, + "tag": "0034_striped_punisher", + "breakpoints": true + }, + { + "idx": 35, + "version": "7", + "when": 1773047232187, + "tag": "0035_strong_alex_wilder", + "breakpoints": true + }, + { + "idx": 36, + "version": "7", + "when": 1773052746270, + "tag": "0036_blue_stardust", + "breakpoints": true + }, + { + "idx": 37, + "version": "7", + "when": 1773135331164, + "tag": "0037_clean_vermin", + "breakpoints": true } ] } \ No newline at end of file diff --git a/public/locales/de/data-table.json b/public/locales/de/data-table.json index e2b9e788..70a597c4 100644 --- a/public/locales/de/data-table.json +++ b/public/locales/de/data-table.json @@ -20,5 +20,6 @@ "edit": "Bearbeiten", "data_upload": "Daten hochladen", "support": "Unterstützung", - "copy_id": "ID kopieren" + "copy_id": "ID kopieren", + "delete": "Löschen" } diff --git a/public/locales/de/delete-device.json b/public/locales/de/delete-device.json new file mode 100644 index 00000000..213b6647 --- /dev/null +++ b/public/locales/de/delete-device.json @@ -0,0 +1,5 @@ +{ + "delete_device": "Gerät löschen", + "confirm_permanent_deletion": "Dadurch werden {{device}} und alle zugehörigen Daten dauerhaft gelöscht. Bitte bestätigen Sie mit Ihrem Passwort.", + "password": "Passwort" +} \ No newline at end of file diff --git a/public/locales/de/menu.json b/public/locales/de/menu.json index 3d425a2a..a11c737e 100644 --- a/public/locales/de/menu.json +++ b/public/locales/de/menu.json @@ -13,6 +13,7 @@ "imprint_label": "Impressum", "data_protection_label": "Datenschutz", "donate_label": "Spenden", + "tos": "Nutzungsbedingungen", "promotion_label": "Förderung", "login_label": "Einloggen", "logout_label": "Ausloggen", diff --git a/public/locales/de/register.json b/public/locales/de/register.json index a6eb6266..b4521472 100644 --- a/public/locales/de/register.json +++ b/public/locales/de/register.json @@ -18,5 +18,10 @@ "password_required": "Passwort ist ein Pflichtfeld", "password_too_short": "Passwort ist zu kurz", "email_already_taken": "Die E-Mail Adresse wird bereits verwendet", - "email_invalid": "Ungültige E-Mail Adresse" + "email_invalid": "Ungültige E-Mail Adresse", + + "agree_tos_prefix": "Ich stimme den", + "terms_of_service": "Nutzungsbedingungen", + "agree_tos_suffix": "zu.", + "tos_must_accept": "Den Nutzungsbedingungen muss zugestimmt werden." } diff --git a/public/locales/de/tos.json b/public/locales/de/tos.json new file mode 100644 index 00000000..1d38fcb9 --- /dev/null +++ b/public/locales/de/tos.json @@ -0,0 +1,8 @@ +{ + "tos_update": "Änderung der Nutzungsbedingungen", + "must_accept": "Sie müssen die aktuellen Nutzungsbedingungen akzeptieren, um die App weiterhin nutzen zu können.", + "tos_agree": "Ich stimme den Nutzungsbedingungen zu.", + "delete_account": "Sie können ihr Konto weiterhin löschen .", + "continue": "Weiter", + "not_configured": "Nutzungsbedingungen wurden nicht konfiguriert." +} \ No newline at end of file diff --git a/public/locales/en/data-table.json b/public/locales/en/data-table.json index 24822ff2..e2b73dfe 100644 --- a/public/locales/en/data-table.json +++ b/public/locales/en/data-table.json @@ -20,5 +20,6 @@ "edit": "Edit", "data_upload": "Data upload", "support": "Support", - "copy_id": "Copy ID" + "copy_id": "Copy ID", + "delete": "Delete" } diff --git a/public/locales/en/delete-device.json b/public/locales/en/delete-device.json new file mode 100644 index 00000000..759e82a1 --- /dev/null +++ b/public/locales/en/delete-device.json @@ -0,0 +1,5 @@ +{ + "delete_device": "Delete device", + "confirm_permanent_deletion": "This will permanently delete {{device}} and all associated data. Please confirm with your password.", + "password": "Password" +} \ No newline at end of file diff --git a/public/locales/en/menu.json b/public/locales/en/menu.json index 9f7115d3..0bd3bcf1 100644 --- a/public/locales/en/menu.json +++ b/public/locales/en/menu.json @@ -13,6 +13,7 @@ "imprint_label": "Imprint", "data_protection_label": "Data Protection", "donate_label": "Donate", + "tos": "Terms of Service", "promotion_label": "Promotion", "login_label": "Log in", "logout_label": "Log out", diff --git a/public/locales/en/register.json b/public/locales/en/register.json index f516cccb..e43d9eff 100644 --- a/public/locales/en/register.json +++ b/public/locales/en/register.json @@ -18,5 +18,10 @@ "password_required": "Password is required", "password_too_short": "Password is too short", "email_already_taken": "A user with this email already exists", - "email_invalid": "Invalid email address" + "email_invalid": "Invalid email address", + + "agree_tos_prefix": "I agree to the", + "terms_of_service": "Terms of Service", + "agree_tos_suffix": ".", + "tos_must_accept": "The terms of service must be accepted." } diff --git a/public/locales/en/tos.json b/public/locales/en/tos.json new file mode 100644 index 00000000..fd053f13 --- /dev/null +++ b/public/locales/en/tos.json @@ -0,0 +1,9 @@ +{ + "tos_update": "Terms of service update", + "must_accept": "You need to accept the latest Terms of Service to continue using the app.", + "tos_agree": "I agree to the Terms of Service.", + "delete_account": "You can still delete your account.", + "continue": "Continue", + "not_configured": "No effective Terms of Service are configured." + +} \ No newline at end of file diff --git a/react-router.config.ts b/react-router.config.ts index 91861f0d..52fa60eb 100644 --- a/react-router.config.ts +++ b/react-router.config.ts @@ -1,4 +1,7 @@ import { type Config } from '@react-router/dev/config' export default { ssr: true, + future: { + v8_middleware: true, + }, } satisfies Config diff --git a/tests/models/device.server.spec.ts b/tests/models/device.server.spec.ts index 1553ac04..ddfdd56e 100644 --- a/tests/models/device.server.spec.ts +++ b/tests/models/device.server.spec.ts @@ -19,6 +19,7 @@ describe('Device Model: createDevice', () => { DEVICE_MODEL_TEST_USER.email, DEVICE_MODEL_TEST_USER.password, 'en_US', + true ) userId = (user as User).id }) diff --git a/tests/request-parsing.spec.ts b/tests/request-parsing.spec.ts index eb98c95c..10b9e3b6 100644 --- a/tests/request-parsing.spec.ts +++ b/tests/request-parsing.spec.ts @@ -64,6 +64,7 @@ describe('parseUserRegistrationData', () => { email: 'john@example.com', password: 'password123', language: 'de_DE', + tosAccepted: true } const request = new Request('http://localhost', { method: 'POST', @@ -77,6 +78,7 @@ describe('parseUserRegistrationData', () => { email: 'john@example.com', password: 'password123', language: 'de_DE', + tosAccepted: true }) }) @@ -85,6 +87,7 @@ describe('parseUserRegistrationData', () => { name: 'john_doe', email: 'john@example.com', password: 'password123', + tosAccepted: true } const request = new Request('http://localhost', { method: 'POST', @@ -98,6 +101,7 @@ describe('parseUserRegistrationData', () => { email: 'john@example.com', password: 'password123', language: 'en_US', + tosAccepted: true }) }) }) diff --git a/tests/routes/api.boxes.$deviceId.$sensorId.measurement.spec.ts b/tests/routes/api.boxes.$deviceId.$sensorId.measurement.spec.ts index c38e65f6..6129d8ce 100644 --- a/tests/routes/api.boxes.$deviceId.$sensorId.measurement.spec.ts +++ b/tests/routes/api.boxes.$deviceId.$sensorId.measurement.spec.ts @@ -1,4 +1,4 @@ -import { ActionFunctionArgs, Params } from 'react-router' +import { type ActionFunctionArgs, type Params } from 'react-router' import { generateTestUserCredentials } from 'tests/data/generate_test_user' import { BASE_URL } from 'vitest.setup' import { createToken } from '~/lib/jwt' @@ -8,7 +8,7 @@ import { insertMeasurements } from '~/models/measurement.server' import { getSensors } from '~/models/sensor.server' import { deleteUserByEmail } from '~/models/user.server' import { action } from '~/routes/api.boxes.$deviceId.$sensorId.measurements' -import { sensor, type User } from '~/schema' +import { type User } from '~/schema' const USER = generateTestUserCredentials() const USER2 = generateTestUserCredentials() @@ -56,12 +56,13 @@ describe('openSenseMap API Routes: /boxes/:deviceId/:sensorId/measurement', () = let jwt2: string beforeAll(async () => { - const u = await registerUser(USER.name, USER.email, USER.password, 'en_US') + const u = await registerUser(USER.name, USER.email, USER.password, 'en_US', true) const u2 = await registerUser( USER2.name, USER2.email, USER2.password, 'en_US', + true ) const d = await createDevice(DEVICE, (u as User).id) const s = await getSensors(d.id) diff --git a/tests/routes/api.boxes.$deviceId.data.$sensorId.spec.ts b/tests/routes/api.boxes.$deviceId.data.$sensorId.spec.ts index 287dc47b..932b3303 100644 --- a/tests/routes/api.boxes.$deviceId.data.$sensorId.spec.ts +++ b/tests/routes/api.boxes.$deviceId.data.$sensorId.spec.ts @@ -68,6 +68,7 @@ describe('openSenseMap API Routes: /api/boxes/:deviceId/data/:sensorId', () => { DEVICE_SENSORS_ID_USER.email, DEVICE_SENSORS_ID_USER.password, 'en_US', + true ) device = await createDevice(DEVICE_SENSOR_ID_BOX, (user as User).id) diff --git a/tests/routes/api.boxes.$deviceId.locations.spec.ts b/tests/routes/api.boxes.$deviceId.locations.spec.ts index dd0a77e8..ff156b57 100644 --- a/tests/routes/api.boxes.$deviceId.locations.spec.ts +++ b/tests/routes/api.boxes.$deviceId.locations.spec.ts @@ -88,6 +88,7 @@ describe('openSenseMap API Routes: /api/boxes/:deviceId/locations', () => { DEVICE_SENSORS_ID_USER.email, DEVICE_SENSORS_ID_USER.password, 'en_US', + true ) device = await createDevice(DEVICE_SENSOR_ID_BOX, (user as User).id) diff --git a/tests/routes/api.boxes.$deviceId.sensors.$sensorId.spec.ts b/tests/routes/api.boxes.$deviceId.sensors.$sensorId.spec.ts index 120e120e..ba929061 100644 --- a/tests/routes/api.boxes.$deviceId.sensors.$sensorId.spec.ts +++ b/tests/routes/api.boxes.$deviceId.sensors.$sensorId.spec.ts @@ -50,6 +50,7 @@ describe('openSenseMap API Routes: /boxes/:deviceId/sensors/:sensorId', () => { DEVICE_SENSORS_ID_USER.email, DEVICE_SENSORS_ID_USER.password, 'en_US', + true ) device = await createDevice(DEVICE_SENSOR_ID_BOX, (user as User).id) diff --git a/tests/routes/api.boxes.$deviceId.sensors.spec.ts b/tests/routes/api.boxes.$deviceId.sensors.spec.ts index 82280f19..8168a793 100644 --- a/tests/routes/api.boxes.$deviceId.sensors.spec.ts +++ b/tests/routes/api.boxes.$deviceId.sensors.spec.ts @@ -47,6 +47,7 @@ describe('openSenseMap API Routes: /boxes/:deviceId/sensors', () => { DEVICE_SENSORS_USER.email, DEVICE_SENSORS_USER.password, 'en_US', + true ) const device = await createDevice(DEVICE_SENSOR_BOX, (user as User).id) diff --git a/tests/routes/api.boxes.data.spec.ts b/tests/routes/api.boxes.data.spec.ts index e2b35427..9b46be88 100644 --- a/tests/routes/api.boxes.data.spec.ts +++ b/tests/routes/api.boxes.data.spec.ts @@ -45,6 +45,7 @@ describe('openSenseMap API: /boxes/data', () => { BOXES_DATA_TEST_USER.email, BOXES_DATA_TEST_USER.password, 'en_US', + true ) user = testUser as User const t = await createToken(user) diff --git a/tests/routes/api.boxes.spec.ts b/tests/routes/api.boxes.spec.ts index 2d2785f9..846f6a48 100644 --- a/tests/routes/api.boxes.spec.ts +++ b/tests/routes/api.boxes.spec.ts @@ -33,6 +33,7 @@ describe('openSenseMap API Routes: /boxes', () => { BOXES_TEST_USER.email, BOXES_TEST_USER.password, 'en_US', + true ) user = testUser as User const { token } = await createToken(testUser as User) diff --git a/tests/routes/api.claim.spec.ts b/tests/routes/api.claim.spec.ts index 409d43fc..b2be722d 100644 --- a/tests/routes/api.claim.spec.ts +++ b/tests/routes/api.claim.spec.ts @@ -13,7 +13,7 @@ const CLAIM_TEST_USER = generateTestUserCredentials() const createTestUser = async (suffix: string): Promise => { const u = generateTestUserCredentials() - const result = await registerUser(u.name, u.email, u.password, 'en_US') + const result = await registerUser(u.name, u.email, u.password, 'en_US', true) if (!result || (typeof result === 'object' && 'isValid' in result)) { throw new Error('Failed to create test user') @@ -44,6 +44,7 @@ describe('openSenseMap API Routes: /boxes/claim', () => { CLAIM_TEST_USER.email, CLAIM_TEST_USER.password, 'en_US', + true ) user = testUser as User const { token: t } = await createToken(testUser as User) @@ -219,6 +220,7 @@ describe('openSenseMap API Routes: /boxes/claim', () => { `claimer${Date.now()}@test.com`, 'password123', 'en_US', + true ) const { token: newUserJwt } = await createToken(newUser as User) diff --git a/tests/routes/api.device.feinstaub.spec.ts b/tests/routes/api.device.feinstaub.spec.ts index cc531c83..fefa4e05 100644 --- a/tests/routes/api.device.feinstaub.spec.ts +++ b/tests/routes/api.device.feinstaub.spec.ts @@ -34,6 +34,7 @@ describe('Device API: Feinstaub Addon behavior', () => { TEST_USER.email, TEST_USER.password, 'en_US', + true ) user = testUser as User const { token: t } = await createToken(testUser as User) diff --git a/tests/routes/api.device.sensors.spec.ts b/tests/routes/api.device.sensors.spec.ts index 16d028f0..bc3dbd32 100644 --- a/tests/routes/api.device.sensors.spec.ts +++ b/tests/routes/api.device.sensors.spec.ts @@ -30,6 +30,7 @@ describe('Device Sensors API: updating sensors', () => { DEVICE_TEST_USER.email, DEVICE_TEST_USER.password, 'en_US', + true ) user = testUser as User const { token } = await createToken(user) diff --git a/tests/routes/api.location.spec.ts b/tests/routes/api.location.spec.ts index b2da69ee..416eab61 100644 --- a/tests/routes/api.location.spec.ts +++ b/tests/routes/api.location.spec.ts @@ -116,6 +116,7 @@ describe('openSenseMap API Routes: Location Measurements', () => { TEST_USER.email, TEST_USER.password, 'en_US', + true ) userId = (user as User).id const device = await createDevice(TEST_BOX, userId) diff --git a/tests/routes/api.measurements.spec.ts b/tests/routes/api.measurements.spec.ts index 98b6bd58..5165cf8f 100644 --- a/tests/routes/api.measurements.spec.ts +++ b/tests/routes/api.measurements.spec.ts @@ -41,6 +41,7 @@ describe('openSenseMap API Routes: /boxes', () => { TEST_USER.email, TEST_USER.password, 'en_US', + true ) userId = (user as User).id const device = await createDevice(TEST_BOX, userId) diff --git a/tests/routes/api.sign-out.spec.ts b/tests/routes/api.sign-out.spec.ts index 1d03875c..1632ca8b 100644 --- a/tests/routes/api.sign-out.spec.ts +++ b/tests/routes/api.sign-out.spec.ts @@ -18,6 +18,7 @@ describe('openSenseMap API Routes: /users', () => { VALID_SIGN_OUT_TEST_USER.email, VALID_SIGN_OUT_TEST_USER.password, 'en_US', + true ) ;({ token: jwt } = await createToken(user as User)) }) diff --git a/tests/routes/api.tags.spec.ts b/tests/routes/api.tags.spec.ts index 5e592385..4bb0a239 100644 --- a/tests/routes/api.tags.spec.ts +++ b/tests/routes/api.tags.spec.ts @@ -30,6 +30,7 @@ describe('openSenseMap API Routes: /tags', () => { TAGS_TEST_USER.email, TAGS_TEST_USER.password, 'en_US', + true ) userId = (user as User).id }) diff --git a/tests/routes/api.transfers.spec.ts b/tests/routes/api.transfers.spec.ts index dbe7c0db..4560b0e5 100644 --- a/tests/routes/api.transfers.spec.ts +++ b/tests/routes/api.transfers.spec.ts @@ -42,6 +42,7 @@ describe('openSenseMap API Routes: /boxes/transfer and /boxes/claim', () => { TRANSFER_TEST_USER.email, TRANSFER_TEST_USER.password, 'en_US', + true ) user = testUser as User const { token: t } = await createToken(testUser as User) @@ -139,6 +140,7 @@ describe('openSenseMap API Routes: /boxes/transfer and /boxes/claim', () => { `other${Date.now()}@test.com`, 'password123', 'en_US', + true ) const { token: otherJwt } = await createToken(otherUser as User) @@ -196,6 +198,7 @@ describe('openSenseMap API Routes: /boxes/transfer and /boxes/claim', () => { `other${Date.now()}@test.com`, 'password123', 'en_US', + true ) const { token: otherJwt } = await createToken(otherUser as User) diff --git a/tests/routes/api.users.me.boxes.$deviceId.spec.ts b/tests/routes/api.users.me.boxes.$deviceId.spec.ts index 84e1ff90..31e07dff 100644 --- a/tests/routes/api.users.me.boxes.$deviceId.spec.ts +++ b/tests/routes/api.users.me.boxes.$deviceId.spec.ts @@ -36,6 +36,7 @@ describe('openSenseMap API Routes: /users', () => { BOX_TEST_USER.email, BOX_TEST_USER.password, 'en_US', + true ) const { token: t } = await createToken(user as User) jwt = t @@ -45,6 +46,7 @@ describe('openSenseMap API Routes: /users', () => { OTHER_TEST_USER.email, OTHER_TEST_USER.password, 'en_US', + true ) const { token: t2 } = await createToken(otherUser as User) otherJwt = t2 diff --git a/tests/routes/api.users.me.boxes.spec.ts b/tests/routes/api.users.me.boxes.spec.ts index 7b0f86dd..f5790a05 100644 --- a/tests/routes/api.users.me.boxes.spec.ts +++ b/tests/routes/api.users.me.boxes.spec.ts @@ -33,6 +33,7 @@ describe('openSenseMap API Routes: /users', () => { BOXES_TEST_USER.email, BOXES_TEST_USER.password, 'en_US', + true ) const { token } = await createToken(user as User) jwt = token @@ -108,6 +109,7 @@ describe('openSenseMap API Routes: /users', () => { 'nodevices@test.com', 'password123', 'en_US', + true ) const { token: noDevicesJwt } = await createToken( userWithNoDevices as User, diff --git a/tests/routes/api.users.me.resend-email-confirmation.spec.ts b/tests/routes/api.users.me.resend-email-confirmation.spec.ts index c9e13aec..20a1a2b4 100644 --- a/tests/routes/api.users.me.resend-email-confirmation.spec.ts +++ b/tests/routes/api.users.me.resend-email-confirmation.spec.ts @@ -20,6 +20,7 @@ describe('openSenseMap API Routes: /users', () => { RESEND_EMAIL_USER.email, RESEND_EMAIL_USER.password, 'en_US', + true ) const { token: t } = await createToken(user as User) jwt = t diff --git a/tests/routes/api.users.me.spec.ts b/tests/routes/api.users.me.spec.ts index 4f2f6468..003e264b 100644 --- a/tests/routes/api.users.me.spec.ts +++ b/tests/routes/api.users.me.spec.ts @@ -21,6 +21,7 @@ describe('openSenseMap API Routes: /users', () => { ME_TEST_USER.email, ME_TEST_USER.password, 'en_US', + true ) const { token: t } = await createToken(user as User) jwt = t diff --git a/tests/routes/api.users.refresh-auth.spec.ts b/tests/routes/api.users.refresh-auth.spec.ts index af54b0fb..d32917a2 100644 --- a/tests/routes/api.users.refresh-auth.spec.ts +++ b/tests/routes/api.users.refresh-auth.spec.ts @@ -24,6 +24,7 @@ describe('openSenseMap API Routes: /users', () => { VALID_REFRESH_AUTH_TEST_USER.email, VALID_REFRESH_AUTH_TEST_USER.password, 'en_US', + true ) ;({ token: jwt, refreshToken } = await createToken(user as User)) }) diff --git a/tests/routes/api.users.register.spec.ts b/tests/routes/api.users.register.spec.ts index 9b399725..979f2730 100644 --- a/tests/routes/api.users.register.spec.ts +++ b/tests/routes/api.users.register.spec.ts @@ -9,11 +9,12 @@ const VALID_SECOND_USER = generateTestUserCredentials() describe('openSenseMap API Routes: /users/register', () => { describe('/POST', () => { - it('should allow to register an user via POST', async () => { + it('should allow to register a user via POST', async () => { // Arrange const params = new URLSearchParams() for (const [key, value] of Object.entries(VALID_USER)) params.append(key, value) + params.append('tosAccepted', 'true') const request = new Request(`${BASE_URL}/users/register`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, @@ -43,6 +44,7 @@ describe('openSenseMap API Routes: /users/register', () => { const params = new URLSearchParams() for (const [key, value] of Object.entries(VALID_USER)) params.append(key, value) + params.append('tosAccepted', 'true') const request = new Request(`${BASE_URL}/users/register`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, @@ -274,6 +276,7 @@ describe('openSenseMap API Routes: /users/register', () => { const params = new URLSearchParams() for (const [key, value] of Object.entries(VALID_SECOND_USER)) params.append(key, value) + params.append('tosAccepted', 'true') const request = new Request(`${BASE_URL}/users/register`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, diff --git a/tests/routes/api.users.request-password-reset.spec.ts b/tests/routes/api.users.request-password-reset.spec.ts index a4b93ba4..6ca292c4 100644 --- a/tests/routes/api.users.request-password-reset.spec.ts +++ b/tests/routes/api.users.request-password-reset.spec.ts @@ -15,6 +15,7 @@ describe('openSenseMap API Routes: /users', () => { VALID_USER.email, VALID_USER.password, 'en_US', + true ) }) diff --git a/tests/routes/api.users.sign-in.spec.ts b/tests/routes/api.users.sign-in.spec.ts index dcf3f0ad..4fee6ac5 100644 --- a/tests/routes/api.users.sign-in.spec.ts +++ b/tests/routes/api.users.sign-in.spec.ts @@ -18,6 +18,7 @@ describe('openSenseMap API Routes: /users', () => { VALID_SIGN_IN_TEST_USER.email, VALID_SIGN_IN_TEST_USER.password, 'en_US', + true ) }) diff --git a/tests/utils/measurement-server-helper.spec.ts b/tests/utils/measurement-server-helper.spec.ts index de81788f..567411a5 100644 --- a/tests/utils/measurement-server-helper.spec.ts +++ b/tests/utils/measurement-server-helper.spec.ts @@ -126,6 +126,7 @@ describe('measurement server helper', () => { DEVICE_SENSORS_ID_USER.email, DEVICE_SENSORS_ID_USER.password, 'en_US', + true ) device = await createDevice(DEVICE_SENSOR_ID_BOX, (user as User).id)