use config file instead of env

This commit is contained in:
Milo Schwartz 2024-10-12 18:21:31 -04:00
parent 6fb569e2cd
commit d9ae322e2a
No known key found for this signature in database
19 changed files with 189 additions and 209 deletions

1
.gitignore vendored
View file

@ -25,3 +25,4 @@ next-env.d.ts
migrations migrations
package-lock.json package-lock.json
tsconfig.tsbuildinfo tsconfig.tsbuildinfo
config.yml

15
config/config.example.yml Normal file
View file

@ -0,0 +1,15 @@
app:
name: Pangolin
environment: dev
base_url: http://localhost:3000
log_level: debug
save_logs: "false"
secure_cookies: "false"
server:
external_port: "3000"
internal_port: "3001"
rate_limit:
window_minutes: "1"
max_requests: "100"

View file

@ -1,5 +1,5 @@
import { defineConfig } from "drizzle-kit"; import { defineConfig } from "drizzle-kit";
import environment from "@server/environment"; import config, { APP_PATH } from "@server/config";
import path from "path"; import path from "path";
export default defineConfig({ export default defineConfig({
@ -8,6 +8,6 @@ export default defineConfig({
out: path.join("server", "migrations"), out: path.join("server", "migrations"),
verbose: true, verbose: true,
dbCredentials: { dbCredentials: {
url: path.join(environment.CONFIG_PATH, "db", "db.sqlite"), url: path.join(APP_PATH, "db", "db.sqlite"),
}, },
}); });

View file

@ -37,6 +37,7 @@
"glob": "11.0.0", "glob": "11.0.0",
"helmet": "7.1.0", "helmet": "7.1.0",
"http-errors": "2.0.0", "http-errors": "2.0.0",
"js-yaml": "4.1.0",
"lucia": "3.2.0", "lucia": "3.2.0",
"lucide-react": "0.447.0", "lucide-react": "0.447.0",
"moment": "2.30.1", "moment": "2.30.1",
@ -62,6 +63,7 @@
"@types/cookie-parser": "1.4.7", "@types/cookie-parser": "1.4.7",
"@types/cors": "2.8.17", "@types/cors": "2.8.17",
"@types/express": "5.0.0", "@types/express": "5.0.0",
"@types/js-yaml": "4.0.9",
"@types/node": "^20", "@types/node": "^20",
"@types/nodemailer": "6.4.16", "@types/nodemailer": "6.4.16",
"@types/react": "^18", "@types/react": "^18",

View file

@ -5,6 +5,7 @@ import { Lucia, TimeSpan } from "lucia";
import { DrizzleSQLiteAdapter } from "@lucia-auth/adapter-drizzle"; import { DrizzleSQLiteAdapter } from "@lucia-auth/adapter-drizzle";
import db from "@server/db"; import db from "@server/db";
import { sessions, users } from "@server/db/schema"; import { sessions, users } from "@server/db/schema";
import config from "@server/config";
const adapter = new DrizzleSQLiteAdapter(db, sessions, users); const adapter = new DrizzleSQLiteAdapter(db, sessions, users);
@ -18,19 +19,18 @@ export const lucia = new Lucia(adapter, {
dateCreated: attributes.dateCreated, dateCreated: attributes.dateCreated,
}; };
}, },
// getSessionAttributes: (attributes) => {
// return {
// country: attributes.country,
// };
// },
sessionCookie: { sessionCookie: {
name: "session", name: "session",
expires: false, expires: false,
attributes: { attributes: {
// secure: environment.ENVIRONMENT === "prod", sameSite: "strict",
// sameSite: "strict", secure: config.app.secure_cookies || false,
secure: false, domain:
domain: ".testing123.io", "." +
config.app.external_base_url
.split("://")[1]
.split(":")[0]
.split("/")[0],
}, },
}, },
sessionExpiresIn: new TimeSpan(2, "w"), sessionExpiresIn: new TimeSpan(2, "w"),
@ -42,7 +42,6 @@ declare module "lucia" {
interface Register { interface Register {
Lucia: typeof lucia; Lucia: typeof lucia;
DatabaseUserAttributes: DatabaseUserAttributes; DatabaseUserAttributes: DatabaseUserAttributes;
DatabaseSessionAttributes: DatabaseSessionAttributes;
} }
} }
@ -54,7 +53,3 @@ interface DatabaseUserAttributes {
emailVerified: boolean; emailVerified: boolean;
dateCreated: string; dateCreated: string;
} }
interface DatabaseSessionAttributes {
// country: string;
}

85
server/config.ts Normal file
View file

@ -0,0 +1,85 @@
import { z } from "zod";
import { fromError } from "zod-validation-error";
import path from "path";
import fs from "fs";
import yaml from "js-yaml";
export const APP_PATH = path.join("config");
const environmentSchema = z.object({
app: z.object({
name: z.string(),
environment: z.enum(["dev", "prod"]),
external_base_url: z.string().url(),
internal_base_url: z.string().url(),
log_level: z.enum(["debug", "info", "warn", "error"]),
save_logs: z.string().transform((val) => val === "true"),
secure_cookies: z.string().transform((val) => val === "true"),
}),
server: z.object({
external_port: z
.string()
.transform((val) => parseInt(val, 10))
.pipe(z.number()),
internal_port: z
.string()
.transform((val) => parseInt(val, 10))
.pipe(z.number()),
}),
rate_limit: z.object({
window_minutes: z
.string()
.transform((val) => parseInt(val, 10))
.pipe(z.number()),
max_requests: z
.string()
.transform((val) => parseInt(val, 10))
.pipe(z.number()),
}),
email: z
.object({
smtp_host: z.string().optional(),
smtp_port: z
.string()
.optional()
.transform((val) => {
if (val) {
return parseInt(val, 10);
}
return val;
})
.pipe(z.number().optional()),
smtp_user: z.string().optional(),
smtp_pass: z.string().optional(),
no_reply: z.string().email().optional(),
})
.optional(),
});
const loadConfig = (configPath: string) => {
try {
const yamlContent = fs.readFileSync(configPath, "utf8");
const config = yaml.load(yamlContent);
return config;
} catch (error) {
if (error instanceof Error) {
throw new Error(
`Error loading configuration file: ${error.message}`,
);
}
throw error;
}
};
const configFilePath = path.join(APP_PATH, "config.yml");
const environment = loadConfig(configFilePath);
const parsedConfig = environmentSchema.safeParse(environment);
if (!parsedConfig.success) {
const errors = fromError(parsedConfig.error);
throw new Error(`Invalid configuration file: ${errors}`);
}
export default parsedConfig.data;

View file

@ -1,10 +1,10 @@
import { drizzle } from "drizzle-orm/better-sqlite3"; import { drizzle } from "drizzle-orm/better-sqlite3";
import Database from "better-sqlite3"; import Database from "better-sqlite3";
import * as schema from "@server/db/schema"; import * as schema from "@server/db/schema";
import environment from "@server/environment"; import config, { APP_PATH } from "@server/config";
import path from "path"; import path from "path";
const location = path.join(environment.CONFIG_PATH, "db", "db.sqlite"); const location = path.join(APP_PATH, "db", "db.sqlite");
const sqlite = new Database(location); const sqlite = new Database(location);
export const db = drizzle(sqlite, { schema }); export const db = drizzle(sqlite, { schema });

View file

@ -1,15 +1,15 @@
export * from "@server/emails/sendEmail"; export * from "@server/emails/sendEmail";
import nodemailer from "nodemailer"; import nodemailer from "nodemailer";
import environment from "@server/environment"; import config from "@server/config";
import logger from "@server/logger"; import logger from "@server/logger";
function createEmailClient() { function createEmailClient() {
if ( if (
!environment.EMAIL_SMTP_HOST || !config.email?.smtp_host ||
!environment.EMAIL_SMTP_PORT || !config.email?.smtp_pass ||
!environment.EMAIL_SMTP_USER || !config.email?.smtp_port ||
!environment.EMAIL_SMTP_PASS !config.email?.smtp_user
) { ) {
logger.warn( logger.warn(
"Email SMTP configuration is missing. Emails will not be sent.", "Email SMTP configuration is missing. Emails will not be sent.",
@ -18,12 +18,12 @@ function createEmailClient() {
} }
return nodemailer.createTransport({ return nodemailer.createTransport({
host: environment.EMAIL_SMTP_HOST, host: config.email.smtp_host,
port: environment.EMAIL_SMTP_PORT, port: config.email.smtp_port,
secure: false, secure: false,
auth: { auth: {
user: environment.EMAIL_SMTP_USER, user: config.email.smtp_user,
pass: environment.EMAIL_SMTP_PASS, pass: config.email.smtp_pass,
}, },
}); });
} }

View file

@ -6,8 +6,8 @@ import logger from "@server/logger";
export async function sendEmail( export async function sendEmail(
template: ReactElement, template: ReactElement,
opts: { opts: {
from: string; from: string | undefined;
to: string; to: string | undefined;
subject: string; subject: string;
}, },
) { ) {

View file

@ -1,86 +0,0 @@
import { z } from "zod";
import { fromError } from "zod-validation-error";
import path from "path";
const environmentSchema = z.object({
ENVIRONMENT: z.enum(["dev", "prod"]),
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]),
SAVE_LOGS: z.string().transform((val) => val === "true"),
CONFIG_PATH: z.string().transform((val) => {
// validate the path and remove any trailing slashes
const resolvedPath = path.resolve(val);
return resolvedPath.endsWith(path.sep)
? resolvedPath.slice(0, -1)
: resolvedPath;
}),
EXTERNAL_PORT: z
.string()
.transform((val) => parseInt(val, 10))
.pipe(z.number()),
INTERNAL_PORT: z
.string()
.transform((val) => parseInt(val, 10))
.pipe(z.number()),
RATE_LIMIT_WINDOW_MIN: z
.string()
.transform((val) => parseInt(val, 10))
.pipe(z.number()),
RATE_LIMIT_MAX: z
.string()
.transform((val) => parseInt(val, 10))
.pipe(z.number()),
APP_NAME: z.string(),
EMAIL_SMTP_HOST: z.string().optional(),
EMAIL_SMTP_PORT: z
.string()
.optional()
.transform((val) => {
if (val) {
return parseInt(val, 10);
}
return val;
})
.pipe(z.number().optional()),
EMAIL_SMTP_USER: z.string().optional(),
EMAIL_SMTP_PASS: z.string().optional(),
EMAIL_NOREPLY: z.string().email().optional(),
BASE_URL: z
.string()
.optional()
.transform((val) => {
if (!val) {
return `http://localhost:${environment.EXTERNAL_PORT}`;
}
return val;
})
.pipe(z.string().url()),
});
const environment = {
ENVIRONMENT: (process.env.ENVIRONMENT as string) || "dev",
LOG_LEVEL: (process.env.LOG_LEVEL as string) || "debug",
SAVE_LOGS: (process.env.SAVE_LOGS as string) || "false",
CONFIG_PATH:
(process.env.CONFIG_PATH && path.join(process.env.CONFIG_PATH)) ||
path.join("config"),
EXTERNAL_PORT: (process.env.EXTERNAL_PORT as string) || "3000",
INTERNAL_PORT: (process.env.INTERNAL_PORT as string) || "3001",
RATE_LIMIT_WINDOW_MIN: (process.env.RATE_LIMIT_WINDOW_MIN as string) || "1",
RATE_LIMIT_MAX: (process.env.RATE_LIMIT_MAX as string) || "100",
APP_NAME: (process.env.APP_NAME as string) || "Pangolin",
EMAIL_SMTP_HOST: process.env.EMAIL_SMTP_HOST as string,
EMAIL_SMTP_PORT: process.env.EMAIL_SMTP_PORT as string,
EMAIL_SMTP_USER: process.env.EMAIL_SMTP_USER as string,
EMAIL_SMTP_PASS: process.env.EMAIL_SMTP_PASS as string,
EMAIL_NOREPLY: process.env.EMAIL_NOREPLY as string,
BASE_URL: process.env.BASE_URL as string,
};
const parsedConfig = environmentSchema.safeParse(environment);
if (!parsedConfig.success) {
const errors = fromError(parsedConfig.error);
throw new Error(`Invalid environment configuration: ${errors}`);
}
export default parsedConfig.data;

View file

@ -1,7 +1,7 @@
import express, { Request, Response } from "express"; import express, { Request, Response } from "express";
import next from "next"; import next from "next";
import { parse } from "url"; import { parse } from "url";
import environment from "@server/environment"; import config from "@server/config";
import logger from "@server/logger"; import logger from "@server/logger";
import helmet from "helmet"; import helmet from "helmet";
import cors from "cors"; import cors from "cors";
@ -15,16 +15,16 @@ import { authenticated, unauthenticated } from "@server/routers/external";
import cookieParser from "cookie-parser"; import cookieParser from "cookie-parser";
import { User } from "@server/db/schema"; import { User } from "@server/db/schema";
const dev = environment.ENVIRONMENT !== "prod"; const dev = config.app.environment !== "prod";
const app = next({ dev }); const app = next({ dev });
const handle = app.getRequestHandler(); const handle = app.getRequestHandler();
const externalPort = environment.EXTERNAL_PORT; const externalPort = config.server.external_port;
const internalPort = environment.INTERNAL_PORT; const internalPort = config.server.internal_port;
app.prepare().then(() => {
app.prepare().then(() => {
// External server // External server
const externalServer = express(); const externalServer = express();
externalServer.set("trust proxy", 1); externalServer.set("trust proxy", 1);

View file

@ -1,5 +1,5 @@
import "winston-daily-rotate-file"; import "winston-daily-rotate-file";
import environment from "@server/environment"; import config, { APP_PATH } from "@server/config";
import * as winston from "winston"; import * as winston from "winston";
import path from "path"; import path from "path";
@ -24,11 +24,11 @@ const transports: any = [
}), }),
]; ];
if (environment.SAVE_LOGS) { if (config.app.save_logs) {
transports.push( transports.push(
new winston.transports.DailyRotateFile({ new winston.transports.DailyRotateFile({
filename: path.join( filename: path.join(
environment.CONFIG_PATH, APP_PATH,
"logs", "logs",
"pangolin-%DATE%.log", "pangolin-%DATE%.log",
), ),
@ -43,7 +43,7 @@ if (environment.SAVE_LOGS) {
transports.push( transports.push(
new winston.transports.DailyRotateFile({ new winston.transports.DailyRotateFile({
filename: path.join( filename: path.join(
environment.CONFIG_PATH, APP_PATH,
"logs", "logs",
".machinelogs-%DATE%.json", ".machinelogs-%DATE%.json",
), ),
@ -63,7 +63,7 @@ if (environment.SAVE_LOGS) {
} }
const logger = winston.createLogger({ const logger = winston.createLogger({
level: environment.LOG_LEVEL.toLowerCase(), level: config.app.log_level.toLowerCase(),
format: winston.format.combine( format: winston.format.combine(
winston.format.splat(), winston.format.splat(),
winston.format.timestamp(), winston.format.timestamp(),

View file

@ -2,7 +2,7 @@ import { ErrorRequestHandler, NextFunction, Response } from "express";
import ErrorResponse from "@server/types/ErrorResponse"; import ErrorResponse from "@server/types/ErrorResponse";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import logger from "@server/logger"; import logger from "@server/logger";
import environment from "@server/environment"; import config from "@server/config";
export const errorHandlerMiddleware: ErrorRequestHandler = ( export const errorHandlerMiddleware: ErrorRequestHandler = (
error, error,
@ -11,7 +11,7 @@ export const errorHandlerMiddleware: ErrorRequestHandler = (
next: NextFunction, next: NextFunction,
) => { ) => {
const statusCode = error.statusCode || HttpCode.INTERNAL_SERVER_ERROR; const statusCode = error.statusCode || HttpCode.INTERNAL_SERVER_ERROR;
if (environment.ENVIRONMENT !== "prod") { if (config.app.environment !== "prod") {
logger.error(error); logger.error(error);
} }
res?.status(statusCode).send({ res?.status(statusCode).send({
@ -20,6 +20,6 @@ export const errorHandlerMiddleware: ErrorRequestHandler = (
error: true, error: true,
message: error.message || "Internal Server Error", message: error.message || "Internal Server Error",
status: statusCode, status: statusCode,
stack: environment.ENVIRONMENT === "prod" ? null : error.stack, stack: config.app.environment === "prod" ? null : error.stack,
}); });
}; };

View file

@ -11,7 +11,7 @@ import { User, users } from "@server/db/schema";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { verify } from "@node-rs/argon2"; import { verify } from "@node-rs/argon2";
import { createTOTPKeyURI } from "oslo/otp"; import { createTOTPKeyURI } from "oslo/otp";
import env from "@server/environment"; import config from "@server/config";
export const requestTotpSecretBody = z.object({ export const requestTotpSecretBody = z.object({
password: z.string(), password: z.string(),
@ -65,7 +65,7 @@ export async function requestTotpSecret(
const hex = crypto.getRandomValues(new Uint8Array(20)); const hex = crypto.getRandomValues(new Uint8Array(20));
const secret = encodeHex(hex); const secret = encodeHex(hex);
const uri = createTOTPKeyURI(env.APP_NAME, user.email, hex); const uri = createTOTPKeyURI(config.app.name, user.email, hex);
await db await db
.update(users) .update(users)

View file

@ -5,7 +5,7 @@ import { users, emailVerificationCodes } from "@server/db/schema";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { sendEmail } from "@server/emails"; import { sendEmail } from "@server/emails";
import VerifyEmail from "@server/emails/templates/verifyEmailCode"; import VerifyEmail from "@server/emails/templates/verifyEmailCode";
import env from "@server/environment"; import config from "@server/config";
export async function sendEmailVerificationCode( export async function sendEmailVerificationCode(
email: string, email: string,
@ -15,7 +15,7 @@ export async function sendEmailVerificationCode(
await sendEmail(VerifyEmail({ username: email, verificationCode: code }), { await sendEmail(VerifyEmail({ username: email, verificationCode: code }), {
to: email, to: email,
from: env.EMAIL_NOREPLY!, from: config.email?.no_reply,
subject: "Verify your email address", subject: "Verify your email address",
}); });
} }

View file

@ -24,8 +24,6 @@ export async function verifyUser(
): Promise<any> { ): Promise<any> {
const parsedBody = verifyUserBody.safeParse(req.query); const parsedBody = verifyUserBody.safeParse(req.query);
logger.debug("Parsed body", parsedBody);
if (!parsedBody.success) { if (!parsedBody.success) {
return next( return next(
createHttpError( createHttpError(
@ -40,9 +38,6 @@ export async function verifyUser(
try { try {
const { session, user } = await lucia.validateSession(sessionId); const { session, user } = await lucia.validateSession(sessionId);
logger.debug("Session", session);
logger.debug("User", user);
if (!session || !user) { if (!session || !user) {
return next( return next(
createHttpError(HttpCode.UNAUTHORIZED, "Invalid session"), createHttpError(HttpCode.UNAUTHORIZED, "Invalid session"),

View file

@ -5,14 +5,13 @@ import { DynamicTraefikConfig } from "./configSchema";
import { and, like, eq } from "drizzle-orm"; import { and, like, eq } from "drizzle-orm";
import logger from "@server/logger"; import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import env from "@server/environment"; import env from "@server/config";
import environment from "@server/environment"; import config from "@server/config";
export async function traefikConfigProvider(_: Request, res: Response) { export async function traefikConfigProvider(_: Request, res: Response) {
try { try {
const targets = await getAllTargets(); const targets = await getAllTargets();
const traefikConfig = buildTraefikConfig(targets); const traefikConfig = buildTraefikConfig(targets);
// logger.debug("Built traefik config");
res.status(HttpCode.OK).json(traefikConfig); res.status(HttpCode.OK).json(traefikConfig);
} catch (e) { } catch (e) {
logger.error(`Failed to build traefik config: ${e}`); logger.error(`Failed to build traefik config: ${e}`);
@ -32,35 +31,13 @@ export function buildTraefikConfig(
} }
const http: DynamicTraefikConfig["http"] = { const http: DynamicTraefikConfig["http"] = {
routers: { routers: {},
"themainwebpage": { services: {},
"entryPoints": [
"http"
],
"middlewares": [
],
"service": "service-themainwebpage",
"rule": "Host(`testing123.io`)"
},
},
services: {
"service-themainwebpage": {
"loadBalancer": {
"servers": [
{
"url": `http://${environment.APP_NAME.toLowerCase()}:3000`
}
]
}
},
},
middlewares: { middlewares: {
[middlewareName]: { [middlewareName]: {
plugin: { plugin: {
[middlewareName]: { [middlewareName]: {
apiBaseUrl: `http://${environment.APP_NAME.toLowerCase()}:3001/api/v1`, apiBaseUrl: config.app.internal_base_url,
// appBaseUrl: env.BASE_URL,
appBaseUrl: "http://testing123.io:8081",
}, },
}, },
}, },

View file

@ -1,10 +1,7 @@
import axios from "axios"; import axios from "axios";
// const baseURL = `${window.location.protocol}//${window.location.host}/api/v1`;
export const api = axios.create({ export const api = axios.create({
baseURL: "http://testing123.io:8081/api/v1", baseURL: process.env.NEXT_PUBLIC_EXTERNAL_API_BASE_URL,
timeout: 10000, timeout: 10000,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -12,7 +9,7 @@ export const api = axios.create({
}); });
export const internal = axios.create({ export const internal = axios.create({
baseURL: "http://pangolin:3000/api/v1", baseURL: process.env.NEXT_PUBLIC_INTERNAL_API_BASE_URL,
timeout: 10000, timeout: 10000,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",

View file

@ -4,49 +4,48 @@
@layer base { @layer base {
:root { :root {
--background: 37 100% 100%; --background: 37 0% 95%;
--foreground: 37 5% 10%; --foreground: 37 0% 10%;
--card: 37 50% 100%; --card: 37 0% 90%;
--card-foreground: 37 5% 15%; --card-foreground: 37 0% 15%;
--popover: 37 100% 100%; --popover: 37 0% 95%;
--popover-foreground: 37 100% 10%; --popover-foreground: 37 95% 10%;
--primary: 37 8% 51%; --primary: 37 31% 25%;
--primary-foreground: 0 0% 100%; --primary-foreground: 0 0% 100%;
--secondary: 37 30% 90%; --secondary: 37 10% 74%;
--secondary-foreground: 0 0% 0%; --secondary-foreground: 0 0% 0%;
--muted: -1 30% 95%; --muted: -1 10% 85%;
--muted-foreground: 37 5% 40%; --muted-foreground: 37 0% 40%;
--accent: -1 30% 90%; --accent: -1 10% 80%;
--accent-foreground: 37 5% 15%; --accent-foreground: 37 0% 15%;
--destructive: 0 100% 50%; --destructive: 0 50% 50%;
--destructive-foreground: 37 5% 100%; --destructive-foreground: 37 0% 90%;
--border: 37 30% 82%; --border: 37 20% 74%;
--input: 37 30% 50%; --input: 37 20% 50%;
--ring: 37 8% 51%; --ring: 37 31% 25%;
--radius: 0rem; --radius: 0.5rem;
} }
.dark { .dark {
--background: 37 50% 10%; --background: 37 10% 10%;
--foreground: 37 5% 100%; --foreground: 37 0% 90%;
--card: 37 50% 10%; --card: 37 0% 10%;
--card-foreground: 37 5% 100%; --card-foreground: 37 0% 90%;
--popover: 37 50% 5%; --popover: 37 10% 5%;
--popover-foreground: 37 5% 100%; --popover-foreground: 37 0% 90%;
--primary: 37 8% 51%; --primary: 37 31% 25%;
--primary-foreground: 0 0% 100%; --primary-foreground: 0 0% 100%;
--secondary: 37 30% 20%; --secondary: 37 10% 20%;
--secondary-foreground: 0 0% 100%; --secondary-foreground: 0 0% 100%;
--muted: -1 30% 25%; --muted: -1 10% 25%;
--muted-foreground: 37 5% 65%; --muted-foreground: 37 0% 65%;
--accent: -1 30% 25%; --accent: -1 10% 25%;
--accent-foreground: 37 5% 95%; --accent-foreground: 37 0% 90%;
--destructive: 0 100% 50%; --destructive: 0 50% 50%;
--destructive-foreground: 37 5% 100%; --destructive-foreground: 37 0% 90%;
--border: 37 30% 50%; --border: 37 20% 50%;
--input: 37 30% 50%; --input: 37 20% 50%;
--ring: 37 8% 51%; --ring: 37 31% 25%;
--radius: 0rem; --radius: 0.5rem;
} }
} }
@ -58,4 +57,4 @@
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
} }