diff --git a/app/components/ui/alert.tsx b/app/components/ui/alert.tsx index d9a5d137..7268117d 100644 --- a/app/components/ui/alert.tsx +++ b/app/components/ui/alert.tsx @@ -1,60 +1,137 @@ -import { cva, type VariantProps } from "class-variance-authority"; -import * as React from "react"; - -import { cn } from "@/lib/utils"; +import { cva, type VariantProps } from 'class-variance-authority' +import { + type LucideIcon, + LucideInfo, + LucideLightbulb, + LucideMessageCircleWarning, + LucideTriangleAlert, + LucideOctagonAlert, +} from 'lucide-react' +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { cn } from '@/lib/utils' const alertVariants = cva( - "relative w-full rounded-lg border border-slate-200 p-4 [&:has(svg)]:pl-11 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-slate-950 dark:border-slate-800 dark:[&>svg]:text-slate-50", - { - variants: { - variant: { - default: - "bg-white text-slate-950 dark:bg-dark-boxes dark:text-dark-text ", - destructive: - "border-red-500/50 text-red-500 dark:border-red-500 [&>svg]:text-red-500 dark:border-red-900/50 dark:text-red-900 dark:dark:border-red-900 dark:[&>svg]:text-red-900", - }, - }, - defaultVariants: { - variant: "default", - }, - }, -); + 'relative w-full rounded-md border border-slate-200 p-4 dark:border-slate-800 [&:has(svg)]:pl-12 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-slate-950 dark:[&>svg]:text-slate-50', + { + variants: { + variant: { + default: + 'bg-white text-slate-950 dark:bg-dark-boxes dark:text-dark-text', + destructive: + 'border-red-500/50 dark:border-red-900/50 dark:text-red-900 dark:dark:border-red-900 dark:[&>svg]:text-red-900 text-red-500 dark:border-red-500 [&>svg]:text-red-500', + note: 'border-blue-500/50 dark:border-blue-900/50 dark:bg-blue-900/20 bg-blue-50 dark:text-blue-200 dark:[&>svg]:text-blue-400 text-blue-900 [&>svg]:text-blue-500', + tip: 'border-green-500/50 dark:border-green-900/50 dark:bg-green-900/20 bg-green-50 text-green-900 dark:text-green-200 [&>svg]:text-green-500 dark:[&>svg]:text-green-400', + important: + 'border-violet-500/50 bg-violet-50 text-violet-900 dark:border-violet-900/50 dark:bg-violet-900/20 dark:text-violet-200 dark:[&>svg]:text-violet-400 [&>svg]:text-violet-500', + warning: + 'border-yellow-500/50 bg-yellow-50 text-yellow-900 dark:border-yellow-900/50 dark:bg-yellow-900/20 dark:text-yellow-200 [&>svg]:text-yellow-500 dark:[&>svg]:text-yellow-400', + caution: + 'border-red-500/50 dark:border-red-900/50 dark:bg-red-900/20 bg-red-50 text-red-900 dark:text-red-200 dark:[&>svg]:text-red-400 [&>svg]:text-red-500', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +) const Alert = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes & VariantProps + HTMLDivElement, + React.HTMLAttributes & VariantProps >(({ className, variant, ...props }, ref) => ( -
-)); -Alert.displayName = "Alert"; +
+)) +Alert.displayName = 'Alert' const AlertTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes + HTMLParagraphElement, + React.HTMLAttributes >(({ className, ...props }, ref) => ( -
-)); -AlertTitle.displayName = "AlertTitle"; +
+)) +AlertTitle.displayName = 'AlertTitle' const AlertDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes + HTMLParagraphElement, + React.HTMLAttributes >(({ className, ...props }, ref) => ( -
-)); -AlertDescription.displayName = "AlertDescription"; - -export { Alert, AlertTitle, AlertDescription }; +
+)) +AlertDescription.displayName = 'AlertDescription' + +interface CalloutProps { + variant: Exclude< + VariantProps['variant'], + 'default' | 'destructive' | undefined | null + > +} + +const VARIANT_MAPPING: { + [key in CalloutProps['variant']]: { + icon: LucideIcon + translationResource: string + } +} = { + note: { + icon: LucideInfo, + translationResource: 'callout_title_note', + }, + tip: { + icon: LucideLightbulb, + translationResource: 'callout_title_tip', + }, + important: { + icon: LucideMessageCircleWarning, + translationResource: 'callout_title_important', + }, + warning: { + icon: LucideTriangleAlert, + translationResource: 'callout_title_warning', + }, + caution: { + icon: LucideOctagonAlert, + translationResource: 'callout_title_caution', + }, +} + +/** + * A convenience wrapper for {@link Alert} that predefines icons + * and titles for different types of callouts (notes, tips, warnings, etc.) + */ +const Callout = ( + props: React.PropsWithChildren = { + variant: 'note', + }, +) => { + const { t } = useTranslation('ui-components') + const map = VARIANT_MAPPING[props.variant] + const Icon = map.icon + const title = t(`callout.${map.translationResource}`) + + return ( + + + {title} + {props.children} + + ) +} + +export { Alert, AlertTitle, AlertDescription, Callout } diff --git a/app/lib/device-transform.ts b/app/lib/device-transform.ts index 399c3a17..dcdc6be0 100644 --- a/app/lib/device-transform.ts +++ b/app/lib/device-transform.ts @@ -17,6 +17,7 @@ export type TransformedDevice = { latitude: number longitude: number useAuth: boolean | null + access_token: string | null public: boolean | null status: string | null createdAt: Date @@ -65,7 +66,7 @@ export type TransformedDevice = { export function transformDeviceToApiFormat( box: DeviceWithSensors, ): TransformedDevice { - const { id, tags, sensors, ...rest } = box + const { id, tags, sensors, apiKey, ...rest } = box const timestamp = box.updatedAt.toISOString() const coordinates = [box.longitude, box.latitude] @@ -86,6 +87,7 @@ export function transformDeviceToApiFormat( }, ], integrations: { mqtt: { enabled: false } }, + access_token: apiKey, sensors: sensors?.map((sensor) => ({ _id: sensor.id, diff --git a/app/lib/jwt.ts b/app/lib/jwt.ts index cbe28e07..4058e138 100644 --- a/app/lib/jwt.ts +++ b/app/lib/jwt.ts @@ -1,37 +1,37 @@ -import { createHmac } from "node:crypto"; -import { eq } from "drizzle-orm"; -import jsonwebtoken, { type JwtPayload, type Algorithm } from "jsonwebtoken"; -import invariant from "tiny-invariant"; -import { v4 as uuidv4 } from "uuid"; -import { drizzleClient } from "~/db.server"; -import { getUserByEmail } from "~/models/user.server"; -import { type User } from "~/schema"; -import { refreshToken, tokenRevocation } from "~/schema/refreshToken"; - -const { sign, verify } = jsonwebtoken; +import { createHmac } from 'node:crypto' +import { eq } from 'drizzle-orm' +import jsonwebtoken, { type JwtPayload, type Algorithm } from 'jsonwebtoken' +import invariant from 'tiny-invariant' +import { v4 as uuidv4 } from 'uuid' +import { drizzleClient } from '~/db.server' +import { getUserByEmail } from '~/models/user.server' +import { device, Device, type User } from '~/schema' +import { refreshToken, tokenRevocation } from '~/schema/refreshToken' + +const { sign, verify } = jsonwebtoken const { - JWT_ALGORITHM, - JWT_ISSUER, - JWT_VALIDITY_MS, - JWT_SECRET, - REFRESH_TOKEN_ALGORITHM, - REFRESH_TOKEN_SECRET, - REFRESH_TOKEN_VALIDITY_MS, -} = process.env; + JWT_ALGORITHM, + JWT_ISSUER, + JWT_VALIDITY_MS, + JWT_SECRET, + REFRESH_TOKEN_ALGORITHM, + REFRESH_TOKEN_SECRET, + REFRESH_TOKEN_VALIDITY_MS, +} = process.env const jwtSignOptions = { - algorithm: JWT_ALGORITHM as Algorithm, - issuer: JWT_ISSUER, - expiresIn: Math.round(Number(JWT_VALIDITY_MS) / 1000), -}; + algorithm: JWT_ALGORITHM as Algorithm, + issuer: JWT_ISSUER, + expiresIn: Math.round(Number(JWT_VALIDITY_MS) / 1000), +} const jwtVerifyOptions = { - algorithm: [JWT_ALGORITHM as Algorithm], - issuer: JWT_ISSUER, -}; + algorithm: [JWT_ALGORITHM as Algorithm], + issuer: JWT_ISSUER, +} -const ONE_DAY_IN_MS = 1000 * 60 * 60 * 24; +const ONE_DAY_IN_MS = 1000 * 60 * 60 * 24 /** * @@ -39,76 +39,76 @@ const ONE_DAY_IN_MS = 1000 * 60 * 60 * 24; * @returns */ export const createToken = ( - user: User, + user: User, ): Promise<{ - token: string; - refreshToken: string; + token: string + refreshToken: string }> => { - invariant(typeof JWT_ALGORITHM === "string"); - invariant(typeof JWT_ISSUER === "string"); - invariant(typeof JWT_VALIDITY_MS === "string"); - invariant(typeof JWT_SECRET === "string"); - - invariant(typeof REFRESH_TOKEN_VALIDITY_MS === "string"); - const payload = { role: user.role }; - const signOptions = Object.assign( - { subject: user.email, jwtid: uuidv4() }, - jwtSignOptions, - ); - - return new Promise(function (resolve, reject) { - sign( - payload, - JWT_SECRET, - signOptions, - async (err: Error | null, token: string | undefined) => { - if (err) return reject(err); - if (typeof token === "undefined") - return reject("Generated token was undefined and thus not valid"); - - // JWT generation was successful - // we now create the refreshToken. - // and set the refreshTokenExpires to 1 week - // it is a HMAC of the jwt string - const refreshToken = hashJwt(token); - const refreshTokenExpiresAt: Date = new Date( - Date.now() + Number(REFRESH_TOKEN_VALIDITY_MS), - ); - try { - await addRefreshToken(user.id, refreshToken, refreshTokenExpiresAt); - return resolve({ token, refreshToken }); - } catch (err) { - return reject(err); - } - }, - ); - }); -}; + invariant(typeof JWT_ALGORITHM === 'string') + invariant(typeof JWT_ISSUER === 'string') + invariant(typeof JWT_VALIDITY_MS === 'string') + invariant(typeof JWT_SECRET === 'string') + + invariant(typeof REFRESH_TOKEN_VALIDITY_MS === 'string') + const payload = { role: user.role } + const signOptions = Object.assign( + { subject: user.email, jwtid: uuidv4() }, + jwtSignOptions, + ) + + return new Promise(function (resolve, reject) { + sign( + payload, + JWT_SECRET, + signOptions, + async (err: Error | null, token: string | undefined) => { + if (err) return reject(err) + if (typeof token === 'undefined') + return reject('Generated token was undefined and thus not valid') + + // JWT generation was successful + // we now create the refreshToken. + // and set the refreshTokenExpires to 1 week + // it is a HMAC of the jwt string + const refreshToken = hashJwt(token) + const refreshTokenExpiresAt: Date = new Date( + Date.now() + Number(REFRESH_TOKEN_VALIDITY_MS), + ) + try { + await addRefreshToken(user.id, refreshToken, refreshTokenExpiresAt) + return resolve({ token, refreshToken }) + } catch (err) { + return reject(err) + } + }, + ) + }) +} export const revokeToken = async (user: User, jwtString: string) => { - invariant(typeof JWT_ALGORITHM === "string"); - invariant(typeof JWT_ISSUER === "string"); - invariant(typeof JWT_SECRET === "string"); - - const hash = hashJwt(jwtString); - await deleteRefreshToken(hash); - const jwt = await decodeJwtString(jwtString, JWT_SECRET, jwtVerifyOptions); - - if (jwt.jti) - await drizzleClient.insert(tokenRevocation).values({ - hash, - token: jwt, - expiresAt: jwt.exp === undefined ? new Date() : new Date(jwt.exp), - }); -}; + invariant(typeof JWT_ALGORITHM === 'string') + invariant(typeof JWT_ISSUER === 'string') + invariant(typeof JWT_SECRET === 'string') + + const hash = hashJwt(jwtString) + await deleteRefreshToken(hash) + const jwt = await decodeJwtString(jwtString, JWT_SECRET, jwtVerifyOptions) + + if (jwt.jti) + await drizzleClient.insert(tokenRevocation).values({ + hash, + token: jwt, + expiresAt: jwt.exp === undefined ? new Date() : new Date(jwt.exp), + }) +} export const revokeRefreshToken = async (refreshToken: string) => { - await drizzleClient.insert(tokenRevocation).values({ - hash: refreshToken, - token: "", - expiresAt: new Date(Date.now() + ONE_DAY_IN_MS * 7), - }); -}; + await drizzleClient.insert(tokenRevocation).values({ + hash: refreshToken, + token: '', + expiresAt: new Date(Date.now() + ONE_DAY_IN_MS * 7), + }) +} /** * @@ -116,135 +116,171 @@ export const revokeRefreshToken = async (refreshToken: string) => { * @returns */ export const getUserFromJwt = async ( - r: Request, -): Promise => { - invariant(typeof JWT_ALGORITHM === "string"); - invariant(typeof JWT_ISSUER === "string"); - invariant(typeof JWT_SECRET === "string"); - - // check if Authorization header is present - const rawAuthorizationHeader = r.headers.get("authorization"); - if (!rawAuthorizationHeader) return "no_token"; - - const [bearer, jwtString] = rawAuthorizationHeader.split(" "); - if (bearer !== "Bearer") return "invalid_token_type"; - - let decodedJwt: JwtPayload | undefined = undefined; - try { - decodedJwt = await decodeJwtString(jwtString, JWT_SECRET, { - ...jwtVerifyOptions, - ignoreExpiration: r.url.endsWith("/users/refresh-auth"), // ignore expiration for refresh endpoint - }); - } catch (err: any) { - if (typeof err === "string") return err as "verification_error"; - } - - invariant(decodedJwt !== undefined); - invariant(decodedJwt.sub !== undefined); - const user = await getUserByEmail(decodedJwt.sub); - if (!user) - throw new Error("User was not found despite a verified jwt provided"); - return user; -}; + r: Request, +): Promise => { + invariant(typeof JWT_ALGORITHM === 'string') + invariant(typeof JWT_ISSUER === 'string') + invariant(typeof JWT_SECRET === 'string') + + // check if Authorization header is present + const rawAuthorizationHeader = r.headers.get('authorization') + if (!rawAuthorizationHeader) return 'no_token' + + const [bearer, jwtString] = rawAuthorizationHeader.split(' ') + if (bearer !== 'Bearer') return 'invalid_token_type' + + let decodedJwt: JwtPayload | undefined = undefined + try { + decodedJwt = await decodeJwtString(jwtString, JWT_SECRET, { + ...jwtVerifyOptions, + ignoreExpiration: r.url.endsWith('/users/refresh-auth'), // ignore expiration for refresh endpoint + }) + } catch (err: any) { + if (typeof err === 'string') return err as 'verification_error' + } + + invariant(decodedJwt !== undefined) + invariant(decodedJwt.sub !== undefined) + const user = await getUserByEmail(decodedJwt.sub) + if (!user) + throw new Error('User was not found despite a verified jwt provided') + return user +} const decodeJwtString = ( - jwtString: string, - jwtSecret: jsonwebtoken.Secret, - options: jsonwebtoken.VerifyOptions & { complete?: false }, + jwtString: string, + jwtSecret: jsonwebtoken.Secret, + options: jsonwebtoken.VerifyOptions & { complete?: false }, ): Promise => { - return new Promise((resolve, reject) => { - verify(jwtString, jwtSecret, options, async (err, decodedJwt) => { - if (err) reject("verification_error"); - if (decodedJwt === undefined) { - reject("verification_error"); - return; - } - - // Our tokens are signed with an object, therefore tokens that - // verify and decode to a string cannot be valid - if (typeof decodedJwt === "string") { - reject("verification_error"); - return; - } - - // We sign our jwt with the user email as the subject, so if there is - // no subject, the jwt cannot be valid as well. - if (decodedJwt.sub === undefined) { - reject("verification_error"); - return; - } - - // check if the token is blacklisted by performing a hmac digest - // on the string representation of the jwt. - // also checks the existence of the jti claim - if (await isTokenRevoked(decodedJwt, jwtString)) { - reject("verification_error"); - return; - } - - resolve(decodedJwt); - return; - }); - }); -}; + return new Promise((resolve, reject) => { + verify(jwtString, jwtSecret, options, async (err, decodedJwt) => { + if (err) reject('verification_error') + if (decodedJwt === undefined) { + reject('verification_error') + return + } + + // Our tokens are signed with an object, therefore tokens that + // verify and decode to a string cannot be valid + if (typeof decodedJwt === 'string') { + reject('verification_error') + return + } + + // We sign our jwt with the user email as the subject, so if there is + // no subject, the jwt cannot be valid as well. + if (decodedJwt.sub === undefined) { + reject('verification_error') + return + } + + // check if the token is blacklisted by performing a hmac digest + // on the string representation of the jwt. + // also checks the existence of the jti claim + if (await isTokenRevoked(decodedJwt, jwtString)) { + reject('verification_error') + return + } + + resolve(decodedJwt) + return + }) + }) +} const addRefreshToken = async ( - userId: string, - token: string, - expiresAt: Date, + userId: string, + token: string, + expiresAt: Date, ) => { - await drizzleClient.insert(refreshToken).values({ - userId, - token, - expiresAt, - }); -}; + await drizzleClient.insert(refreshToken).values({ + userId, + token, + expiresAt, + }) +} const deleteRefreshToken = async (tokenHash: string) => { - await drizzleClient - .delete(refreshToken) - .where(eq(refreshToken.token, tokenHash)); -}; + await drizzleClient + .delete(refreshToken) + .where(eq(refreshToken.token, tokenHash)) +} const isTokenRevoked = async (token: JwtPayload, tokenString: string) => { - if (!token.jti) { - // token has no id.. -> shouldn't be accepted - return true; - } + if (!token.jti) { + // token has no id.. -> shouldn't be accepted + return true + } - const hash = hashJwt(tokenString); + const hash = hashJwt(tokenString) - const revokedToken = await drizzleClient - .select() - .from(tokenRevocation) - .where(eq(tokenRevocation.hash, hash)); + const revokedToken = await drizzleClient + .select() + .from(tokenRevocation) + .where(eq(tokenRevocation.hash, hash)) - if (revokedToken.length > 0) return true; - return false; -}; + if (revokedToken.length > 0) return true + return false +} export const hashJwt = (jwt: string) => { - invariant(typeof REFRESH_TOKEN_ALGORITHM === "string"); - invariant(typeof REFRESH_TOKEN_SECRET === "string"); + invariant(typeof REFRESH_TOKEN_ALGORITHM === 'string') + invariant(typeof REFRESH_TOKEN_SECRET === 'string') - return createHmac(REFRESH_TOKEN_ALGORITHM, REFRESH_TOKEN_SECRET) - .update(jwt) - .digest("base64"); -}; + return createHmac(REFRESH_TOKEN_ALGORITHM, REFRESH_TOKEN_SECRET) + .update(jwt) + .digest('base64') +} export const refreshJwt = async ( - u: User, - refreshToken: string, + u: User, + refreshToken: string, ): Promise<{ token: string; refreshToken: string } | null> => { - // We have to check if the refresh token actually belongs to the user - const userForToken = await drizzleClient.query.refreshToken.findFirst({ - where: (r, { eq }) => eq(r.token, refreshToken), - with: { - user: true, - }, - }); - if (userForToken == undefined || userForToken.userId !== u.id) return null; - - await revokeRefreshToken(refreshToken); - return await createToken(u); -}; + // We have to check if the refresh token actually belongs to the user + const userForToken = await drizzleClient.query.refreshToken.findFirst({ + where: (r, { eq }) => eq(r.token, refreshToken), + with: { + user: true, + }, + }) + if (userForToken == undefined || userForToken.userId !== u.id) return null + + await revokeRefreshToken(refreshToken) + return await createToken(u) +} + +export const createDeviceApiKey = ( + device: Device, +): Promise<{ + key: string +}> => { + invariant(typeof JWT_ALGORITHM === 'string') + invariant(typeof JWT_ISSUER === 'string') + invariant(typeof JWT_VALIDITY_MS === 'string') + invariant(typeof JWT_SECRET === 'string') + + const payload = { role: device.id } + const signOptions = Object.assign( + { subject: device.id, jwtid: uuidv4() }, + jwtSignOptions, + ) + + return new Promise(function (resolve, reject) { + sign( + payload, + JWT_SECRET, + signOptions, + async (err: Error | null, token: string | undefined) => { + if (err) return reject(err) + if (typeof token === 'undefined') + return reject('Generated token was undefined and thus not valid') + + // We use the signature part of the jwt as a deviceApiKey + const k = token.split('.').at(-1) + if (k === undefined) + throw new Error('signature part of jwt may never be undefined') + return resolve({ key: k }) + }, + ) + }) +} diff --git a/app/lib/measurement-service.server.ts b/app/lib/measurement-service.server.ts index 6fc911be..608872c6 100644 --- a/app/lib/measurement-service.server.ts +++ b/app/lib/measurement-service.server.ts @@ -5,7 +5,6 @@ import { type DeviceWithoutSensors, getDeviceWithoutSensors, getDevice, - findAccessToken, } from '~/models/device.server' import { saveMeasurements } from '~/models/measurement.server' import { @@ -137,9 +136,7 @@ export const postNewMeasurements = async ( } if (device.useAuth) { - const deviceAccessToken = await findAccessToken(deviceId) - - if (deviceAccessToken?.token && deviceAccessToken.token !== authorization) { + if (device.apiKey !== authorization) { const error = new Error('Device access token not valid!') error.name = 'UnauthorizedError' throw error @@ -192,12 +189,7 @@ export const postSingleMeasurement = async ( } if (device.useAuth) { - const deviceAccessToken = await findAccessToken(deviceId) - - if ( - deviceAccessToken?.token && - deviceAccessToken.token !== authorization - ) { + if (device.apiKey !== authorization) { const error = new Error('Device access token not valid!') error.name = 'UnauthorizedError' throw error diff --git a/app/models/device.server.ts b/app/models/device.server.ts index 252e78c0..a7192a4b 100644 --- a/app/models/device.server.ts +++ b/app/models/device.server.ts @@ -1,5 +1,16 @@ import { point } from '@turf/helpers' -import { eq, sql, desc, ilike, arrayContains, and, between } from 'drizzle-orm' +import { + eq, + sql, + desc, + ilike, + arrayContains, + and, + between, + type ExtractTablesWithRelations, +} from 'drizzle-orm' +import { type PgTransaction } from 'drizzle-orm/pg-core' +import { type PostgresJsQueryResultHKT } from 'drizzle-orm/postgres-js' import BaseNewDeviceEmail, { messages as BaseNewDeviceMessages, } from 'emails/base-new-device' @@ -7,6 +18,7 @@ import { messages as NewLufdatenDeviceMessages } from 'emails/new-device-luftdat import { messages as NewSenseboxDeviceMessages } from 'emails/new-device-sensebox' import { type Point } from 'geojson' import { drizzleClient } from '~/db.server' +import { createDeviceApiKey } from '~/lib/jwt' import { sendMail } from '~/lib/mail.server' import { device, @@ -17,6 +29,7 @@ import { type Device, type Sensor, } from '~/schema' +import type * as schema from '~/schema/index' import { getSensorsForModel } from '~/utils/model-definitions' const BASE_DEVICE_COLUMNS = { @@ -36,7 +49,10 @@ const BASE_DEVICE_COLUMNS = { updatedAt: true, expiresAt: true, useAuth: true, + apiKey: true, sensorWikiModel: true, + public: true, + userId: true, } as const const DEVICE_COLUMNS_WITH_SENSORS = { @@ -377,6 +393,14 @@ export async function updateDevice( } } } + + if ( + args.useAuth && + args.useAuth == updatedDevice.useAuth && + args.useAuth === true + ) + await addOrReplaceDeviceApiKey(updatedDevice, tx) + return updatedDevice }) @@ -766,11 +790,17 @@ export async function createDevice(deviceData: any, userId: string) { } } + let apiKey: string = '' + if (createdDevice.useAuth) { + apiKey = (await addOrReplaceDeviceApiKey(createdDevice, tx)).apiKey + } + // Return device with sensors return [ { ...createdDevice, sensors: createdSensors, + apiKey: apiKey, }, u, ] @@ -856,14 +886,23 @@ export async function getLatestDevices() { return devices } -export async function findAccessToken( - deviceId: string, -): Promise<{ token: string } | null> { - const result = await drizzleClient.query.accessToken.findFirst({ - where: (token, { eq }) => eq(token.deviceId, deviceId), - }) +export async function addOrReplaceDeviceApiKey( + d: Device, + tx?: PgTransaction< + PostgresJsQueryResultHKT, + typeof schema, + ExtractTablesWithRelations + >, +): Promise<{ apiKey: string }> { + const { key } = await createDeviceApiKey(d) + const result = await (tx ?? drizzleClient) + .update(device) + .set({ apiKey: key }) + .where(eq(device.id, d.id)) + .returning() - if (!result || !result.token) return null + if (result[0].apiKey === null) + throw new Error('device api key cannot be null after inserting') - return { token: result.token } + return { apiKey: result[0].apiKey } } diff --git a/app/routes/device.$deviceId.dataupload.tsx b/app/routes/device.$deviceId.dataupload.tsx index 33159021..34217812 100644 --- a/app/routes/device.$deviceId.dataupload.tsx +++ b/app/routes/device.$deviceId.dataupload.tsx @@ -24,7 +24,7 @@ import { } from '~/components/ui/select' import { Textarea } from '~/components/ui/textarea' import { postNewMeasurements } from '~/lib/measurement-service.server' -import { findAccessToken } from '~/models/device.server' +import { getDevice } from '~/models/device.server' import { StandardResponse } from '~/utils/response-utils' import { getUserId } from '~/utils/session.server' @@ -63,14 +63,15 @@ export async function action({ return StandardResponse.badRequest( 'measurement data is either not set or has a wrong type', ) - const deviceApiKey = await findAccessToken(deviceId) + const deviceApiKey = (await getDevice({ id: deviceId }))?.apiKey + if (!deviceApiKey) return StandardResponse.badRequest('device not found') try { await postNewMeasurements(deviceId, measurementData, { contentType, luftdaten: false, hackair: false, - authorization: deviceApiKey?.token ?? '', + authorization: deviceApiKey, }) return StandardResponse.ok({}) diff --git a/app/routes/device.$deviceId.edit.security.tsx b/app/routes/device.$deviceId.edit.security.tsx index 973fe060..365ed84e 100644 --- a/app/routes/device.$deviceId.edit.security.tsx +++ b/app/routes/device.$deviceId.edit.security.tsx @@ -1,162 +1,209 @@ -import { RefreshCw, Save } from "lucide-react"; -import { useState } from "react"; -import { type LoaderFunctionArgs, redirect , Form } from "react-router"; -import { Checkbox } from "@/components/ui/checkbox"; -import ErrorMessage from "~/components/error-message"; -import { getUserId } from "~/utils/session.server"; - -//***************************************************** -export async function loader({ request }: LoaderFunctionArgs) { - //* if user is not logged in, redirect to home - const userId = await getUserId(request); - if (!userId) return redirect("/"); - - return ""; +import { Label } from '@radix-ui/react-label' +import { + LucideCopy, + LucideCopyCheck, + LucideEye, + LucideEyeOff, + RefreshCw, + Save, +} from 'lucide-react' +import { useEffect, useState } from 'react' +import { Trans, useTranslation } from 'react-i18next' +import { + type LoaderFunctionArgs, + redirect, + Form, + useLoaderData, + type ActionFunctionArgs, +} from 'react-router' +import { Checkbox } from '@/components/ui/checkbox' +import ErrorMessage from '~/components/error-message' +import { Callout } from '~/components/ui/alert' +import { + addOrReplaceDeviceApiKey, + getDevice, + updateDevice, +} from '~/models/device.server' +import { getUserId } from '~/utils/session.server' + +export async function loader({ request, params }: LoaderFunctionArgs) { + //* if user is not logged in, redirect to home + const userId = await getUserId(request) + if (!userId) return redirect('/') + + const deviceId = params.deviceId + if (typeof deviceId !== 'string') throw 'deviceID not found' + + const t = (await getDevice({ id: deviceId }))?.apiKey + if (!t) throw 'device not found' + + const device = await getDevice({ id: deviceId }) + return { key: t, deviceAuthEnabled: device?.useAuth ?? false } } -//***************************************************** -export async function action() { - return ""; +export async function action({ request, params }: ActionFunctionArgs) { + const { deviceId } = params + if (typeof deviceId !== 'string') throw 'deviceID not found' + + switch (request.method) { + case 'POST': + const formData = await request.formData() + const enableAuth = formData.has('enableAuth') + await updateDevice(deviceId, { useAuth: enableAuth }) + break + case 'PUT': + const device = await getDevice({ id: deviceId }) + if (device === undefined) throw 'device not found' + await addOrReplaceDeviceApiKey(device) + break + } + + return '' } -//********************************** export default function EditBoxSecurity() { - const [tokenVisibility, setTokenvisibility] = useState(false); - - return ( - (
-
-
- {/* Form */} -
- {/* Heading */} -
- {/* Title */} -
-
-

Change security settings

-
-
- {/* Save button */} - -
-
-
- - {/* divider */} -
- -
-

- DANGER: If you deactivate this option everyone can send data to - your senseBox. -

-
- -
- - -
- - {/* Access Token */} -
- -
- - - - -
-
- -
-

- DANGER: If you generate a new token you have to upload a new - sketch to your senseBox. This step can not be undone. -

-
- - -
- -
-

- Further information regarding security on openSenseMap can be - found here (also for TTN and MQTT): - - https://en.docs.sensebox.de/opensensemap/opensensemap-security/ - -

-
-
-
-
-
) - ); + const { t } = useTranslation('settings') + const { key, deviceAuthEnabled } = useLoaderData() + const [keyVisible, setTokenvisibility] = useState(false) + const [authEnabled, setAuthEnabled] = useState(deviceAuthEnabled) + const [copiedToClipboard, setCopiedToClipboard] = useState(false) + + const copyKeyToClipboard = async () => { + if (!key) return + await navigator.clipboard.writeText(key) + setCopiedToClipboard(true) + } + + useEffect(() => { + if (!copiedToClipboard) return + const timer = window.setTimeout(() => { + setCopiedToClipboard(false) + }, 2_500) + + return () => { + window.clearTimeout(timer) + } + }, [copiedToClipboard]) + + return ( +
+
+
+

{t('device_security.page_title')}

+ +
+ +
+ +

+ + Devices should use their API key shown on this page to authenticate + requests sent to the openSenseMap API. This ensures that only + authenticated devices update the state of the device on + openSenseMap. The API key is appended to every request made to the + API. More information can be found{' '} + + in the docs + + . + +

+ + + {t('device_security.warning_deactive_auth')} + + +
+ setAuthEnabled(!authEnabled)} + /> + +
+ +
+ +
+ + + + + + + +
+
+
+
+
+ +

+ + Generating a new key will require you to update your device (e.g. + change the sketch/ code). + This step can not be undone. + +

+ +
+
+
+ ) } export function ErrorBoundary() { - return ( -
- -
- ); + return ( +
+ +
+ ) } diff --git a/app/schema/accessToken.ts b/app/schema/accessToken.ts deleted file mode 100644 index 956d2420..00000000 --- a/app/schema/accessToken.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { type InferSelectModel, relations } from "drizzle-orm"; -import { pgTable, text } from "drizzle-orm/pg-core"; -import { device } from "./device"; - -export const accessToken = pgTable('access_token', { - deviceId: text('device_id').notNull() - .references(() => device.id, { - onDelete: 'cascade' - }), - token: text('token'), - }); - - export const accessTokenRelations = relations(accessToken, ({ one }) => ({ - user: one(device, { - fields: [accessToken.deviceId], - references: [device.id] - }) - })); - -export type AccessToken = InferSelectModel; diff --git a/app/schema/device.ts b/app/schema/device.ts index c9783b7e..db58f8a6 100644 --- a/app/schema/device.ts +++ b/app/schema/device.ts @@ -39,6 +39,7 @@ export const device = pgTable('device', { .default(sql`ARRAY[]::text[]`), link: text('link'), useAuth: boolean('use_auth'), + apiKey: text('apiKey'), exposure: DeviceExposureEnum('exposure'), status: DeviceStatusEnum('status').default('inactive'), model: DeviceModelEnum('model').default('custom'), diff --git a/app/schema/index.ts b/app/schema/index.ts index 8099e8d5..e597d199 100644 --- a/app/schema/index.ts +++ b/app/schema/index.ts @@ -1,14 +1,13 @@ -export * from "./device"; -export * from "./enum"; -export * from "./measurement"; -export * from "./password"; -export * from "./profile"; -export * from "./profile-image"; -export * from "./types"; -export * from "./sensor"; -export * from "./user"; -export * from "./location"; -export * from "./log-entry"; -export * from "./refreshToken"; -export * from "./claim"; -export * from "./accessToken"; +export * from './device' +export * from './enum' +export * from './measurement' +export * from './password' +export * from './profile' +export * from './profile-image' +export * from './types' +export * from './sensor' +export * from './user' +export * from './location' +export * from './log-entry' +export * from './refreshToken' +export * from './claim' diff --git a/app/styles/app.css b/app/styles/app.css index 2dfd8e15..245ca5b4 100644 --- a/app/styles/app.css +++ b/app/styles/app.css @@ -39,11 +39,12 @@ --color-zinc-700: #3f3f46; --color-zinc-800: #27272a; --color-zinc-900: #18181b; + --color-blue-50: #eff6ff; --color-blue-100: #3d9cce; --color-blue-200: #1879af; + --color-blue-400: #60a5fa; --color-blue-500: #3b82f6; --color-blue-700: #1d4ed8; - --color-blue-50: #f8fafc; --color-gray-50: #f9fafb; --color-gray-100: #f3f4f6; --color-gray-200: #e5e7eb; @@ -66,11 +67,25 @@ --color-green-800: #166534; --color-green-900: #14532d; --color-green-950: #052e16; - --color-red-500: #f10000; + --color-red-50: #fef2f2; + --color-red-100: #fee2e2; + --color-red-200: #fecaca; + --color-red-400: #f87171; + --color-red-500: #ef4444; --color-red-700: #b91c1c; --color-orange-500: #f97316; - --color-violet-500: #7f00ff; + --color-violet-50: #f5f3ff; + --color-violet-100: #ede9fe; + --color-violet-200: #ddd6fe; + --color-violet-400: #a78bfa; + --color-violet-500: #8b5cf6; + --color-violet-700: #6d28d9; + --color-yellow-50: #fefce8; + --color-yellow-100: #fef9c3; + --color-yellow-200: #fef08a; + --color-yellow-400: #facc15; --color-yellow-500: #eab308; + --color-yellow-700: #a16207; --color-yellow-sensorWiki: #ffdd57; --color-headerBorder: #e3e3e3; diff --git a/drizzle/0029_plain_black_widow.sql b/drizzle/0029_plain_black_widow.sql new file mode 100644 index 00000000..1c60572d --- /dev/null +++ b/drizzle/0029_plain_black_widow.sql @@ -0,0 +1,5 @@ +ALTER TABLE "access_token" RENAME TO "device_api_key";--> statement-breakpoint +ALTER TABLE "device_api_key" RENAME COLUMN "token" TO "api_key";--> statement-breakpoint +ALTER TABLE "device_api_key" DROP CONSTRAINT "access_token_device_id_device_id_fk"; +--> statement-breakpoint +ALTER TABLE "device_api_key" ADD CONSTRAINT "device_api_key_device_id_device_id_fk" FOREIGN KEY ("device_id") REFERENCES "public"."device"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/0030_parallel_toxin.sql b/drizzle/0030_parallel_toxin.sql new file mode 100644 index 00000000..f2e7efc5 --- /dev/null +++ b/drizzle/0030_parallel_toxin.sql @@ -0,0 +1,2 @@ +DROP TABLE "device_api_key" CASCADE;--> statement-breakpoint +ALTER TABLE "device" ADD COLUMN "apiKey" text; \ No newline at end of file diff --git a/drizzle/meta/0029_snapshot.json b/drizzle/meta/0029_snapshot.json new file mode 100644 index 00000000..892aaf3b --- /dev/null +++ b/drizzle/meta/0029_snapshot.json @@ -0,0 +1,1286 @@ +{ + "id": "825b79a1-9079-4a5e-83ea-864930b0f7b7", + "prevId": "d622d4a4-5722-4b7f-b22f-8c5184ad7148", + "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 + }, + "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 + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sensor_wiki_model": { + "name": "sensor_wiki_model", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "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.device_api_key": { + "name": "device_api_key", + "schema": "", + "columns": { + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "device_api_key_device_id_device_id_fk": { + "name": "device_api_key_device_id_device_id_fk", + "tableFrom": "device_api_key", + "tableTo": "device", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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/0030_snapshot.json b/drizzle/meta/0030_snapshot.json new file mode 100644 index 00000000..32268176 --- /dev/null +++ b/drizzle/meta/0030_snapshot.json @@ -0,0 +1,1253 @@ +{ + "id": "962a5afc-671c-43ef-b967-5571d0fc3776", + "prevId": "825b79a1-9079-4a5e-83ea-864930b0f7b7", + "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 + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sensor_wiki_model": { + "name": "sensor_wiki_model", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "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 + } + }, + "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 455d73e6..9c94b36f 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -204,6 +204,20 @@ "when": 1769504950323, "tag": "0028_gigantic_deadpool", "breakpoints": true + }, + { + "idx": 29, + "version": "7", + "when": 1770285162055, + "tag": "0029_plain_black_widow", + "breakpoints": true + }, + { + "idx": 30, + "version": "7", + "when": 1770303921106, + "tag": "0030_parallel_toxin", + "breakpoints": true } ] } \ No newline at end of file diff --git a/public/locales/de/settings.json b/public/locales/de/settings.json index 05990253..d3ecacff 100644 --- a/public/locales/de/settings.json +++ b/public/locales/de/settings.json @@ -3,7 +3,6 @@ "account": "Konto", "password": "Passwort", "delete_account": "Konto löschen", - "profile_updated": "Profil aktualisiert", "profile_updated_description": "Dein Profil wurde erfolgreich aktualisiert.", "something_went_wrong": "Etwas ist schief gelaufen.", @@ -18,12 +17,10 @@ "if_activated_public_3": " sehen können.", "change_profile_photo": "Profilbild ändern", "save_changes": "Änderungen speichern", - "profile_photo": "Profilbild", "save_photo": "Profilbild speichern", "reset": "Zurücksetzen", "change": "Ändern", - "invalid_password": "Falsches Passwort", "profile_successfully_updated": "Profil efolgreich aktualisiert", "account_information": "Konto Informationen", @@ -36,7 +33,6 @@ "select_language": "Wähle die Sprache aus", "confirm_password": "Bestätige dein Passwort", "enter_current_password": "Gib dein aktuelles Passwort ein", - "try_again": "Versuche es nochmal.", "update_password": "Aktualisiere dein Passwort", "update_password_description": "Gib dein aktuelles und ein neues Passwort ein, um dein Konto Passwort zu aktualisieren.", @@ -51,7 +47,13 @@ "new_passwords_do_not_match": "Die neuen Passwörter stimmen nicht überein.", "current_password_incorret": "Das aktuelle Passwort ist falsch.", "password_updated_successfully": "Das Passwort wurde erfolgreich aktualisiert.", - "delete_account_description": "Wenn du dein Konto löschst, werden alle deine Daten permanent von unseren Servern gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.", - "enter_password": "Gib dein Passwort ein" -} \ No newline at end of file + "enter_password": "Gib dein Passwort ein", + "device_security": { + "warning_deactive_auth": "Wenn die Gerätesicherheit deaktiviert ist, kann jede Person Messdaten senden, die Ihrem Gerät zugewiesen sind!", + "page_title": "Sicherheitseinstellungen ändern", + "api_key_label": "API Schlüssel", + "explanation_text": "Geräte sollten stets ihren API Schlüssel nutzen, um ihre Anfragen an die openSenseMap API zu authentifizieren. Dies stellt sicher, dass nur authentifizierte Geräte den Zustand des Gerätes auf der openSenseMap aktualisieren können. Der API Schlüssel wird an jede Anfrage an die API angehängt. Mehr Informationen <2>gibt es in der Dokumentation.", + "generate_new_key_warning": "Das Generieren eines neuen Schlüssel zieht zwangsläufig ein Update des Gerätes nach sich (z.B. Ändern des Codes/ Sketches). <1>Dieser Schritt kann nicht rückgängig gemacht werden." + } +} diff --git a/public/locales/de/ui-components.json b/public/locales/de/ui-components.json new file mode 100644 index 00000000..10f57331 --- /dev/null +++ b/public/locales/de/ui-components.json @@ -0,0 +1,9 @@ +{ + "callout": { + "callout_title_note": "Hinweis", + "callout_title_tip": "Tipp", + "callout_title_important": "Wichtig", + "callout_title_warning": "Warnung", + "callout_title_caution": "Achtung" + } +} diff --git a/public/locales/en/settings.json b/public/locales/en/settings.json index f8a3841f..9f6935ac 100644 --- a/public/locales/en/settings.json +++ b/public/locales/en/settings.json @@ -1,57 +1,61 @@ { - "public_profile": "Public Profile", - "account": "Account", - "password": "Password", - "delete_account": "Delete Account", - - "profile_updated": "Profile updated", - "profile_updated_description": "Your profile has been updated successfully.", - "something_went_wrong": "Something went wrong.", - "something_went_wrong_description": "Please try again later.", - "profile_settings": "Profile Settings", - "profile_settings_description": "This is how others see your profile.", - "username": "Username", - "if_public": "If your profile is public, this is how people will see you.", - "enter_username": "Enter your new username", - "if_activated_public_1": "If activated, others will be able to see your public", - "if_activated_public_2": "profile", - "if_activated_public_3": ".", - "change_profile_photo": "Change profile photo", - "save_changes": "Save changes", - - "profile_photo": "Profile photo", - "save_photo": "Save Photo", - "reset": "Reset", - "change": "Change", - - "invalid_password": "Invalid password", - "profile_successfully_updated": "Profile successfully updated.", - "account_information": "Account Information", - "update_basic_details": "Update your basic account details.", - "name": "Name", - "enter_name": "Enter your name", - "email": "Email", - "enter_email": "Enter your email", - "language": "Language", - "select_language": "Select language", - "confirm_password": "Confirm password", - "enter_current_password": "Enter your current password", - - "try_again": "Please try again.", - "update_password": "Update Password", - "update_password_description": "Enter your current password and a new password to update your account password.", - "current_password": "Current Password", - "new_password": "New Password", - "enter_new_password": "Enter your new password", - "confirm_new_password": "Confirm your new password", - "password_required": "Password is required.", - "password_length": "Password must be at least 8 characters long.", - "email_not_found": "Email not found!", - "current_password_required": "Current password is required.", - "new_passwords_do_not_match": "New passwords do not match.", - "current_password_incorret": "Current password is incorrect.", - "password_updated_successfully": "Password updated successfully.", - - "delete_account_description": "Deleting your account will permanently remove all of your data from our servers. This action cannot be undone.", - "enter_password": "Enter your password" -} \ No newline at end of file + "public_profile": "Public Profile", + "account": "Account", + "password": "Password", + "delete_account": "Delete Account", + "profile_updated": "Profile updated", + "profile_updated_description": "Your profile has been updated successfully.", + "something_went_wrong": "Something went wrong.", + "something_went_wrong_description": "Please try again later.", + "profile_settings": "Profile Settings", + "profile_settings_description": "This is how others see your profile.", + "username": "Username", + "if_public": "If your profile is public, this is how people will see you.", + "enter_username": "Enter your new username", + "if_activated_public_1": "If activated, others will be able to see your public", + "if_activated_public_2": "profile", + "if_activated_public_3": ".", + "change_profile_photo": "Change profile photo", + "save_changes": "Save changes", + "profile_photo": "Profile photo", + "save_photo": "Save Photo", + "reset": "Reset", + "change": "Change", + "invalid_password": "Invalid password", + "profile_successfully_updated": "Profile successfully updated.", + "account_information": "Account Information", + "update_basic_details": "Update your basic account details.", + "name": "Name", + "enter_name": "Enter your name", + "email": "Email", + "enter_email": "Enter your email", + "language": "Language", + "select_language": "Select language", + "confirm_password": "Confirm password", + "enter_current_password": "Enter your current password", + "try_again": "Please try again.", + "update_password": "Update Password", + "update_password_description": "Enter your current password and a new password to update your account password.", + "current_password": "Current Password", + "new_password": "New Password", + "enter_new_password": "Enter your new password", + "confirm_new_password": "Confirm your new password", + "password_required": "Password is required.", + "password_length": "Password must be at least 8 characters long.", + "email_not_found": "Email not found!", + "current_password_required": "Current password is required.", + "new_passwords_do_not_match": "New passwords do not match.", + "current_password_incorret": "Current password is incorrect.", + "password_updated_successfully": "Password updated successfully.", + "delete_account_description": "Deleting your account will permanently remove all of your data from our servers. This action cannot be undone.", + "enter_password": "Enter your password", + "device_security": { + "warning_deactive_auth": "If you disable device security, anyone can send measurement data that is assigned to your device!", + "page_title": "Change security settings", + "auth_enable_checkbox_label": "Enable authentication", + "api_key_label": "API Key", + "explanation_text": "Devices should use their API key shown on this page to authenticate requests sent to the openSenseMap API. This ensures that only authenticated devices update the state of the device on openSenseMap. The API key is appended to every request made to the API. More information can be found <2>in the docs.", + "generate_new_key_button": "Generate a new key", + "generate_new_key_warning": "Generating a new key will require you to update your device (e.g. change the sketch/ code). <1>This step can not be undone." + } +} diff --git a/public/locales/en/ui-components.json b/public/locales/en/ui-components.json new file mode 100644 index 00000000..2177d5bb --- /dev/null +++ b/public/locales/en/ui-components.json @@ -0,0 +1,9 @@ +{ + "callout": { + "callout_title_note": "Note", + "callout_title_tip": "Tip", + "callout_title_important": "Important", + "callout_title_warning": "Warning", + "callout_title_caution": "Caution" + } +} diff --git a/tailwind.config.ts b/tailwind.config.ts index 5a51b53f..fbe2bde3 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,241 +1,266 @@ -import tailwindTypography from "@tailwindcss/typography"; -import { type Config } from "tailwindcss"; -import defaultTheme from "tailwindcss/defaultTheme"; -import tailwindcssAnimate from "tailwindcss-animate"; +import tailwindTypography from '@tailwindcss/typography' +import { type Config } from 'tailwindcss' +import defaultTheme from 'tailwindcss/defaultTheme' +import tailwindcssAnimate from 'tailwindcss-animate' export default { - darkMode: ["class"], - content: ["./app/**/*.{ts,tsx,js,jsx}"], - theme: { - // shadcn container - container: { - center: true, - padding: "2rem", - screens: { - "2xl": "1400px", - }, - }, - colors: { - // Color schem is defined in /styles/app.css - transparent: "transparent", - current: "currentColor", - white: "var(--color-white)", - black: "var(--color-black)", - gray: { - 50: "var(--color-gray-50)", - 100: "var(--color-gray-100)", - 200: "var(--color-gray-200)", - 300: "var(--color-gray-300)", - 400: "var(--color-gray-400)", - 500: "var(--color-gray-500)", - 600: "var(--color-gray-600)", - 700: "var(--color-gray-700)", - 800: "var(--color-gray-800)", - 900: "var(--color-gray-900)", - 950: "var(--color-gray-950)", - }, - zinc: { - 50: "var(--color-zinc-50)", - 100: "var(--color-zinc-100)", - 200: "var(--color-zinc-200)", - 300: "var(--color-zinc-300)", - 400: "var(--color-zinc-400)", - 500: "var(--color-zinc-500)", - 600: "var(--color-zinc-600)", - 700: "var(--color-zinc-700)", - 800: "var(--color-zinc-800)", - 900: "var(--color-zinc-900)", - }, - slate: { - 50: "#f8fafc", - 100: "#f1f5f9", - 200: "#e2e8f0", - 300: "#cbd5e1", - 400: "#94a3b8", - 500: "#64748b", - 600: "#475569", - 700: "#334155", - 800: "#1e293b", - 900: "#0f172a", - 950: "#020617", - }, - green: { - 50: "var(--color-green-50)", - 100: "var(--color-green-100)", - 200: "var(--color-green-200)", - 300: "var(--color-green-300)", - 400: "var(--color-green-400)", - 500: "var(--color-green-500)", - 700: "var(--color-green-700)", - 900: "var(--color-green-900)", - 950: "var(--color-green-950)", - }, - blue: { - 100: "var(--color-blue-100)", - 300: "var(--color-blue-300)", - 500: "var(--color-blue-500)", - 700: "var(--color-blue-700)", - 900: "var(--color-blue-900)", - }, - red: { - 500: "var(--color-red-500)", - 700: "var(--color-red-700)", - }, - orange: { - 500: "var(--color-orange-500)", - }, - violet: { - 500: "var(--color-violet-500)", - }, - yellow: "#FFD700", - sensorWiki: "var(--color-yellow-sensorWiki)", - headerBorder: "var(--color-headerBorder)", - logo: { - green: "#4fae48", - blue: "#00b4e4", - }, - light: { - menu: "#727373", - text: "#363636", - green: "#3D843F", - blue: "#037EA1", - }, - dark: { - title: "FFFFFF", - menu: "#D2D1D0", - text: "#D2D1D0", - green: "#6FA161", - blue: "#0386AA", - background: "#242424", - boxes: "#3B3A3A", - }, - }, - extend: { - colors: { - //osem color scheme - logo: { - green: "#4fae48", - blue: "#00b4e4", - }, - light: { - menu: "#727373", - text: "#363636", - green: "#3D843F", - blue: "#037EA1", - }, - dark: { - menu: "#D2D1D0", - text: "#D2D1D0", - green: "#6FA161", - blue: "#0386AA", - background: "#242424", - boxes: "#3B3A3A", - }, - // shadcn colors - border: "hsl(var(--border))", - input: "hsl(var(--input))", - ring: "hsl(var(--ring))", - background: "hsl(var(--background))", - foreground: "hsl(var(--foreground))", - primary: { - DEFAULT: "hsl(var(--primary))", - foreground: "hsl(var(--primary-foreground))", - }, - secondary: { - DEFAULT: "hsl(var(--secondary))", - foreground: "hsl(var(--secondary-foreground))", - }, - destructive: { - DEFAULT: "hsl(var(--destructive))", - foreground: "hsl(var(--destructive-foreground))", - }, - muted: { - DEFAULT: "hsl(var(--muted))", - foreground: "hsl(var(--muted-foreground))", - }, - accent: { - DEFAULT: "hsl(var(--accent))", - foreground: "hsl(var(--accent-foreground))", - }, - popover: { - DEFAULT: "hsl(var(--popover))", - foreground: "hsl(var(--popover-foreground))", - }, - card: { - DEFAULT: "hsl(var(--card))", - foreground: "hsl(var(--card-foreground))", - }, - }, - // shadcn brder radius - borderRadius: { - lg: "var(--radius)", - md: "calc(var(--radius) - 2px)", - sm: "calc(var(--radius) - 4px)", - }, - fontFamily: { - sans: ["Urbanist", ...defaultTheme.fontFamily.sans], - serif: ["RobotoSlab", ...defaultTheme.fontFamily.serif], - monospace: ["Courier New", "Courier", "monospace"], - helvetica: ["Helvetica", "Arial", "sans-serif"], - }, - keyframes: { - sidebarOpen: { - from: { transform: "translateX(100%)" }, - to: { transform: "translateX(O)" }, - }, - sidebarClose: { - from: { transform: "translateX(0)" }, - to: { transform: "translateX(100%)" }, - }, - contentShow: { - from: { opacity: "0", transform: "translate(-50%, 0%) scale(0.5)" }, - to: { opacity: "1", transform: "translate(-50%, 0%) scale(1)" }, - }, - contentClose: { - from: { opacity: "1", transform: "translate(-50%, -50%) scale(1)" }, - to: { opacity: "0", transform: "translate(-50%, -48%) scale(0.5)" }, - }, - "fade-in-up": { - "0%": { - opacity: "0", - transform: "translateY(10px)", - }, - "100%": { - opacity: "1", - transform: "translateY(0)", - }, - }, - pulse: { - "0%, 100%": { - opacity: "1", - }, - "50%": { - opacity: ".5", - }, - }, + darkMode: ['class'], + content: ['./app/**/*.{ts,tsx,js,jsx}'], + theme: { + // shadcn container + container: { + center: true, + padding: '2rem', + screens: { + '2xl': '1400px', + }, + }, + colors: { + // Color schem is defined in /styles/app.css + transparent: 'transparent', + current: 'currentColor', + white: 'var(--color-white)', + black: 'var(--color-black)', + gray: { + 50: 'var(--color-gray-50)', + 100: 'var(--color-gray-100)', + 200: 'var(--color-gray-200)', + 300: 'var(--color-gray-300)', + 400: 'var(--color-gray-400)', + 500: 'var(--color-gray-500)', + 600: 'var(--color-gray-600)', + 700: 'var(--color-gray-700)', + 800: 'var(--color-gray-800)', + 900: 'var(--color-gray-900)', + 950: 'var(--color-gray-950)', + }, + zinc: { + 50: 'var(--color-zinc-50)', + 100: 'var(--color-zinc-100)', + 200: 'var(--color-zinc-200)', + 300: 'var(--color-zinc-300)', + 400: 'var(--color-zinc-400)', + 500: 'var(--color-zinc-500)', + 600: 'var(--color-zinc-600)', + 700: 'var(--color-zinc-700)', + 800: 'var(--color-zinc-800)', + 900: 'var(--color-zinc-900)', + }, + slate: { + 50: '#f8fafc', + 100: '#f1f5f9', + 200: '#e2e8f0', + 300: '#cbd5e1', + 400: '#94a3b8', + 500: '#64748b', + 600: '#475569', + 700: '#334155', + 800: '#1e293b', + 900: '#0f172a', + 950: '#020617', + }, + green: { + 50: 'var(--color-green-50)', + 100: 'var(--color-green-100)', + 200: 'var(--color-green-200)', + 300: 'var(--color-green-300)', + 400: 'var(--color-green-400)', + 500: 'var(--color-green-500)', + 700: 'var(--color-green-700)', + 900: 'var(--color-green-900)', + 950: 'var(--color-green-950)', + }, + blue: { + 50: 'var(--color-blue-50)', + 100: 'var(--color-blue-100)', + 200: 'var(--color-blue-200)', + 300: 'var(--color-blue-300)', + 400: 'var(--color-blue-400)', + 500: 'var(--color-blue-500)', + 700: 'var(--color-blue-700)', + 900: 'var(--color-blue-900)', + }, + red: { + 50: 'var(--color-red-50)', + 100: 'var(--color-red-100)', + 200: 'var(--color-red-200)', + 300: 'var(--color-red-300)', + 400: 'var(--color-red-400)', + 500: 'var(--color-red-500)', + 700: 'var(--color-red-700)', + 900: 'var(--color-red-900)', + }, + orange: { + 500: 'var(--color-orange-500)', + }, + violet: { + 50: 'var(--color-violet-50)', + 100: 'var(--color-violet-100)', + 200: 'var(--color-violet-200)', + 300: 'var(--color-violet-300)', + 400: 'var(--color-violet-400)', + 500: 'var(--color-violet-500)', + 700: 'var(--color-violet-700)', + 900: 'var(--color-violet-900)', + }, + yellow: { + 50: 'var(--color-yellow-50)', + 100: 'var(--color-yellow-100)', + 200: 'var(--color-yellow-200)', + 300: 'var(--color-yellow-300)', + 400: 'var(--color-yellow-400)', + 500: 'var(--color-yellow-500)', + 700: 'var(--color-yellow-700)', + 900: 'var(--color-yellow-900)', + }, + sensorWiki: 'var(--color-yellow-sensorWiki)', + headerBorder: 'var(--color-headerBorder)', + logo: { + green: '#4fae48', + blue: '#00b4e4', + }, + light: { + menu: '#727373', + text: '#363636', + green: '#3D843F', + blue: '#037EA1', + }, + dark: { + title: 'FFFFFF', + menu: '#D2D1D0', + text: '#D2D1D0', + green: '#6FA161', + blue: '#0386AA', + background: '#242424', + boxes: '#3B3A3A', + }, + }, + extend: { + colors: { + //osem color scheme + logo: { + green: '#4fae48', + blue: '#00b4e4', + }, + light: { + menu: '#727373', + text: '#363636', + green: '#3D843F', + blue: '#037EA1', + }, + dark: { + menu: '#D2D1D0', + text: '#D2D1D0', + green: '#6FA161', + blue: '#0386AA', + background: '#242424', + boxes: '#3B3A3A', + }, + // shadcn colors + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + }, + // shadcn brder radius + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + }, + fontFamily: { + sans: ['Urbanist', ...defaultTheme.fontFamily.sans], + serif: ['RobotoSlab', ...defaultTheme.fontFamily.serif], + monospace: ['Courier New', 'Courier', 'monospace'], + helvetica: ['Helvetica', 'Arial', 'sans-serif'], + }, + keyframes: { + sidebarOpen: { + from: { transform: 'translateX(100%)' }, + to: { transform: 'translateX(O)' }, + }, + sidebarClose: { + from: { transform: 'translateX(0)' }, + to: { transform: 'translateX(100%)' }, + }, + contentShow: { + from: { opacity: '0', transform: 'translate(-50%, 0%) scale(0.5)' }, + to: { opacity: '1', transform: 'translate(-50%, 0%) scale(1)' }, + }, + contentClose: { + from: { opacity: '1', transform: 'translate(-50%, -50%) scale(1)' }, + to: { opacity: '0', transform: 'translate(-50%, -48%) scale(0.5)' }, + }, + 'fade-in-up': { + '0%': { + opacity: '0', + transform: 'translateY(10px)', + }, + '100%': { + opacity: '1', + transform: 'translateY(0)', + }, + }, + pulse: { + '0%, 100%': { + opacity: '1', + }, + '50%': { + opacity: '.5', + }, + }, - // shadcn accordion animation - "accordion-down": { - from: { height: "0" }, - to: { height: "var(--radix-accordion-content-height)" }, - }, - "accordion-up": { - from: { height: "var(--radix-accordion-content-height)" }, - to: { height: "0" }, - }, - }, - animation: { - "fade-in-up": "fade-in-up 1s ease-out", - sidebarOpen: "sidebarOpen 300ms ease-out", - sidebarClose: "sidebarClose 300ms ease-out", - contentShow: "contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1)", - contentClose: "contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1)", - pulse: "pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite", - // shadcn accordion animation - "accordion-down": "accordion-down 0.2s ease-out", - "accordion-up": "accordion-up 0.2s ease-out", - }, - }, - }, - plugins: [tailwindcssAnimate, tailwindTypography], -} satisfies Config; + // shadcn accordion animation + 'accordion-down': { + from: { height: '0' }, + to: { height: 'var(--radix-accordion-content-height)' }, + }, + 'accordion-up': { + from: { height: 'var(--radix-accordion-content-height)' }, + to: { height: '0' }, + }, + }, + animation: { + 'fade-in-up': 'fade-in-up 1s ease-out', + sidebarOpen: 'sidebarOpen 300ms ease-out', + sidebarClose: 'sidebarClose 300ms ease-out', + contentShow: 'contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1)', + contentClose: 'contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1)', + pulse: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite', + // shadcn accordion animation + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + }, + }, + }, + plugins: [tailwindcssAnimate, tailwindTypography], +} satisfies Config diff --git a/tests/routes/api.boxes.spec.ts b/tests/routes/api.boxes.spec.ts index c258e002..2a8f764d 100644 --- a/tests/routes/api.boxes.spec.ts +++ b/tests/routes/api.boxes.spec.ts @@ -85,10 +85,12 @@ describe('openSenseMap API Routes: /boxes', () => { expect(response.status).toBe(201) expect(body).toHaveProperty('_id') expect(body).toHaveProperty('name', 'Test Weather Station') - expect(body).toHaveProperty('sensors') expect(body.sensors).toHaveLength(2) + expect(body).toHaveProperty('sensors') expect(body.sensors[0]).toHaveProperty('title', 'Temperature') expect(body.sensors[1]).toHaveProperty('title', 'Humidity') + expect(body).toHaveProperty('access_token') + expect(body.access_token).not.toBeNull() }) it('should create a box with minimal data (no sensors)', async () => { diff --git a/tests/routes/api.devices.spec.ts b/tests/routes/api.devices.spec.ts index 8e8dc274..e80439d6 100644 --- a/tests/routes/api.devices.spec.ts +++ b/tests/routes/api.devices.spec.ts @@ -54,6 +54,7 @@ describe('openSenseMap API Routes: /boxes', () => { latitude: 123, longitude: 12, tags: ['testgroup'], + useAuth: false, }, (testUser as User).id, ) @@ -625,6 +626,7 @@ describe('openSenseMap API Routes: /boxes', () => { description: 'total neue beschreibung', location: { lat: 54.2, lng: 21.1 }, weblink: 'http://www.google.de', + useAuth: true, image: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQYV2NgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII=', } @@ -643,17 +645,15 @@ describe('openSenseMap API Routes: /boxes', () => { params: { deviceId: queryableDevice?.id }, context: {} as AppLoadContext, } as ActionFunctionArgs) - - expect(response.status).toBe(200) - const data = await response.json() + expect(response.status).toBe(200) expect(data.name).toBe(update_payload.name) expect(data.exposure).toBe(update_payload.exposure) expect(Array.isArray(data.grouptag)).toBe(true) expect(data.grouptag).toContain(update_payload.grouptag) expect(data.description).toBe(update_payload.description) - + expect(data.access_token).not.toBeNull() expect(data.currentLocation).toEqual({ type: 'Point', coordinates: [ diff --git a/tests/routes/api.location.spec.ts b/tests/routes/api.location.spec.ts index ec572df8..b2da69ee 100644 --- a/tests/routes/api.location.spec.ts +++ b/tests/routes/api.location.spec.ts @@ -11,8 +11,6 @@ import { action as postSingleMeasurementAction } from '~/routes/api.boxes.$devic import { action as postMeasurementsAction } from '~/routes/api.boxes.$deviceId.data' import { location, deviceToLocation, measurement, type User } from '~/schema' -const mockAccessToken = 'valid-access-token-location-tests' - const TEST_USER = generateTestUserCredentials() const TEST_BOX = { @@ -36,6 +34,7 @@ describe('openSenseMap API Routes: Location Measurements', () => { let deviceId: string = '' let sensorIds: string[] = [] let sensors: any[] = [] + let deviceApiKey: string = '' // Helper function to get device's current location async function getDeviceCurrentLocation(deviceId: string) { @@ -121,6 +120,7 @@ describe('openSenseMap API Routes: Location Measurements', () => { userId = (user as User).id const device = await createDevice(TEST_BOX, userId) deviceId = device.id + deviceApiKey = device.apiKey const deviceWithSensors = await getDevice({ id: deviceId }) sensorIds = @@ -146,7 +146,7 @@ describe('openSenseMap API Routes: Location Measurements', () => { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: mockAccessToken, + Authorization: deviceApiKey, }, body: JSON.stringify(measurement), }, @@ -180,7 +180,7 @@ describe('openSenseMap API Routes: Location Measurements', () => { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: mockAccessToken, + Authorization: deviceApiKey, }, body: JSON.stringify(measurement), }, @@ -214,7 +214,7 @@ describe('openSenseMap API Routes: Location Measurements', () => { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: mockAccessToken, + Authorization: deviceApiKey, }, body: JSON.stringify(currentMeasurement), }, @@ -245,7 +245,7 @@ describe('openSenseMap API Routes: Location Measurements', () => { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: mockAccessToken, + Authorization: deviceApiKey, }, body: JSON.stringify(pastMeasurement), }, @@ -290,7 +290,7 @@ describe('openSenseMap API Routes: Location Measurements', () => { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: mockAccessToken, + Authorization: testDevice.apiKey, }, body: JSON.stringify(measurement), }, @@ -339,7 +339,7 @@ describe('openSenseMap API Routes: Location Measurements', () => { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: mockAccessToken, + Authorization: testDevice.apiKey, }, body: JSON.stringify(measurement1), }, @@ -365,7 +365,7 @@ describe('openSenseMap API Routes: Location Measurements', () => { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: mockAccessToken, + Authorization: testDevice.apiKey, }, body: JSON.stringify(measurement2), }, @@ -390,7 +390,7 @@ describe('openSenseMap API Routes: Location Measurements', () => { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: mockAccessToken, + Authorization: testDevice.apiKey, }, body: JSON.stringify(measurement3), }, @@ -450,7 +450,7 @@ describe('openSenseMap API Routes: Location Measurements', () => { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: mockAccessToken, + Authorization: testDevice.apiKey, }, body: JSON.stringify(measurement3), }, @@ -475,7 +475,7 @@ describe('openSenseMap API Routes: Location Measurements', () => { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: mockAccessToken, + Authorization: testDevice.apiKey, }, body: JSON.stringify(measurement2), }, @@ -501,7 +501,7 @@ describe('openSenseMap API Routes: Location Measurements', () => { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: mockAccessToken, + Authorization: testDevice.apiKey, }, body: JSON.stringify(measurement1), }, @@ -552,7 +552,7 @@ describe('openSenseMap API Routes: Location Measurements', () => { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: mockAccessToken, + Authorization: deviceApiKey, }, body: JSON.stringify(measurement), }, @@ -582,7 +582,7 @@ describe('openSenseMap API Routes: Location Measurements', () => { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: mockAccessToken, + Authorization: deviceApiKey, }, body: JSON.stringify(measurement), }, @@ -616,7 +616,7 @@ describe('openSenseMap API Routes: Location Measurements', () => { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: mockAccessToken, + Authorization: deviceApiKey, }, body: JSON.stringify(body), }) @@ -658,7 +658,7 @@ describe('openSenseMap API Routes: Location Measurements', () => { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: mockAccessToken, + Authorization: deviceApiKey, }, body: JSON.stringify(measurements), }) diff --git a/tests/routes/api.measurements.spec.ts b/tests/routes/api.measurements.spec.ts index 7414ba8c..98b6bd58 100644 --- a/tests/routes/api.measurements.spec.ts +++ b/tests/routes/api.measurements.spec.ts @@ -3,15 +3,12 @@ import { csvExampleData, jsonSubmitData, byteSubmitData } from 'tests/data' import { generateTestUserCredentials } from 'tests/data/generate_test_user' import { describe, it, expect, beforeAll, afterAll } from 'vitest' import { BASE_URL } from 'vitest.setup' -import { drizzleClient } from '~/db.server' import { registerUser } from '~/lib/user-service.server' import { createDevice, deleteDevice, getDevice } from '~/models/device.server' import { deleteUserByEmail } from '~/models/user.server' import { action as postSingleMeasurementAction } from '~/routes/api.boxes.$deviceId.$sensorId' import { action as postMeasurementsAction } from '~/routes/api.boxes.$deviceId.data' -import { accessToken, type User } from '~/schema' - -const mockAccessToken = 'valid-access-token' +import { type User } from '~/schema' const TEST_USER = generateTestUserCredentials() @@ -36,6 +33,7 @@ describe('openSenseMap API Routes: /boxes', () => { let deviceId: string = '' let sensorIds: string[] = [] let sensors: any[] = [] + let deviceApiKey: string = '' beforeAll(async () => { const user = await registerUser( @@ -52,11 +50,7 @@ describe('openSenseMap API Routes: /boxes', () => { sensorIds = deviceWithSensors?.sensors?.map((sensor: any) => sensor.id) || [] sensors = deviceWithSensors?.sensors?.map((sensor: any) => sensor) || [] - - await drizzleClient.insert(accessToken).values({ - deviceId: deviceId, - token: 'valid-access-token', - }) + deviceApiKey = deviceWithSensors?.apiKey ?? '' }) // --------------------------------------------------- @@ -70,7 +64,7 @@ describe('openSenseMap API Routes: /boxes', () => { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: mockAccessToken, + Authorization: deviceApiKey, }, body: JSON.stringify({ value: 312.1 }), }, @@ -120,7 +114,7 @@ describe('openSenseMap API Routes: /boxes', () => { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: mockAccessToken, + Authorization: deviceApiKey, }, body: JSON.stringify({ value: 123.4, createdAt: timestamp }), }, @@ -145,7 +139,7 @@ describe('openSenseMap API Routes: /boxes', () => { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: mockAccessToken, + Authorization: deviceApiKey, }, body: JSON.stringify({ value: 123.4, createdAt: future }), }, @@ -172,7 +166,7 @@ describe('openSenseMap API Routes: /boxes', () => { method: 'POST', headers: { 'Content-Type': 'text/csv', - Authorization: mockAccessToken, + Authorization: deviceApiKey, }, body: csvPayload, }) @@ -194,7 +188,7 @@ describe('openSenseMap API Routes: /boxes', () => { method: 'POST', headers: { 'Content-Type': 'text/csv', - Authorization: mockAccessToken, + Authorization: deviceApiKey, }, body: csvPayload, }) @@ -215,7 +209,7 @@ describe('openSenseMap API Routes: /boxes', () => { method: 'POST', headers: { 'Content-Type': 'text/csv', - Authorization: mockAccessToken, + Authorization: deviceApiKey, }, body: csvPayload, }) @@ -240,7 +234,7 @@ describe('openSenseMap API Routes: /boxes', () => { method: 'POST', headers: { 'Content-Type': 'application/sbx-bytes', - Authorization: mockAccessToken, + Authorization: deviceApiKey, }, body: byteSubmitData(sensors) as unknown as BodyInit, }) @@ -278,7 +272,7 @@ describe('openSenseMap API Routes: /boxes', () => { method: 'POST', headers: { 'Content-Type': 'application/sbx-bytes-ts', - Authorization: mockAccessToken, + Authorization: deviceApiKey, }, body: byteSubmitData(sensors, true) as unknown as BodyInit, }) @@ -342,7 +336,7 @@ describe('openSenseMap API Routes: /boxes', () => { method: 'POST', headers: { 'Content-Type': 'application/sbx-bytes', - Authorization: mockAccessToken, + Authorization: deviceApiKey, }, body: bytes, }) @@ -364,7 +358,7 @@ describe('openSenseMap API Routes: /boxes', () => { method: 'POST', headers: { 'Content-Type': 'application/sbx-bytes', - Authorization: mockAccessToken, + Authorization: deviceApiKey, }, body: new Uint8Array(0), }) @@ -420,7 +414,7 @@ describe('openSenseMap API Routes: /boxes', () => { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: mockAccessToken, + Authorization: deviceApiKey, }, body: JSON.stringify(submitData), }) @@ -457,7 +451,7 @@ describe('openSenseMap API Routes: /boxes', () => { const request = new Request(`${BASE_URL}/api/boxes/${deviceId}/data`, { method: 'POST', headers: { - Authorization: mockAccessToken, + Authorization: deviceApiKey, // TODO: remove header here 'Content-Type': 'application/json', }, @@ -495,7 +489,7 @@ describe('openSenseMap API Routes: /boxes', () => { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: mockAccessToken, + Authorization: deviceApiKey, }, body: JSON.stringify(submitData), }) diff --git a/tests/routes/api.tags.spec.ts b/tests/routes/api.tags.spec.ts index c7c0ab93..5e592385 100644 --- a/tests/routes/api.tags.spec.ts +++ b/tests/routes/api.tags.spec.ts @@ -12,7 +12,7 @@ const TEST_TAG_BOX = { name: `'${TAGS_TEST_USER.name}'s Box`, exposure: 'outdoor', expiresAt: null, - tags: ['tag1', 'tag2', 'testgroup'], + tags: ['tag1', 'tag2', 'testgrouptag'], latitude: 0, longitude: 0, model: 'luftdaten.info', @@ -79,7 +79,9 @@ describe('openSenseMap API Routes: /tags', () => { 'application/json; charset=utf-8', ) expect(Array.isArray(body.data)).toBe(true) - expect(body.data).toHaveLength(3) + expect( + body.data.filter((t: string) => TEST_TAG_BOX.tags.includes(t)), + ).toHaveLength(3) }) afterAll(async () => { diff --git a/tests/routes/api.users.me.boxes.spec.ts b/tests/routes/api.users.me.boxes.spec.ts index 6bf064df..7b0f86dd 100644 --- a/tests/routes/api.users.me.boxes.spec.ts +++ b/tests/routes/api.users.me.boxes.spec.ts @@ -78,6 +78,7 @@ describe('openSenseMap API Routes: /users', () => { expect(box).toHaveProperty('createdAt') expect(box).toHaveProperty('updatedAt') expect(box).toHaveProperty('useAuth') + expect(box).toHaveProperty('access_token') // kept for backwards compatibility, now called apiKey expect(box).toHaveProperty('currentLocation') expect(box.currentLocation).toHaveProperty('type', 'Point')