first commit
This commit is contained in:
commit
81749d24d3
17 changed files with 651 additions and 0 deletions
2
.dockerignore
Normal file
2
.dockerignore
Normal file
|
@ -0,0 +1,2 @@
|
|||
postgres-data
|
||||
dragonfly-data
|
12
.editorconfig
Normal file
12
.editorconfig
Normal file
|
@ -0,0 +1,12 @@
|
|||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
* text=auto eol=lf
|
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
node_modules
|
||||
postgres-data
|
||||
dragonfly-data
|
||||
logs
|
||||
bun.lock
|
||||
.env
|
37
Dockerfile
Normal file
37
Dockerfile
Normal file
|
@ -0,0 +1,37 @@
|
|||
# use the official Bun image
|
||||
# see all versions at https://hub.docker.com/r/oven/bun/tags
|
||||
FROM oven/bun:latest AS base
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
FROM base AS install
|
||||
RUN mkdir -p /temp/dev
|
||||
COPY package.json bun.lock /temp/dev/
|
||||
RUN cd /temp/dev && bun install --frozen-lockfile
|
||||
|
||||
# install with --production (exclude devDependencies)
|
||||
RUN mkdir -p /temp/prod
|
||||
COPY package.json bun.lock /temp/prod/
|
||||
RUN cd /temp/prod && bun install --frozen-lockfile --production
|
||||
|
||||
# copy node_modules from temp directory
|
||||
# then copy all (non-ignored) project files into the image
|
||||
FROM base AS prerelease
|
||||
COPY --from=install /temp/dev/node_modules node_modules
|
||||
COPY . .
|
||||
|
||||
# [optional] tests & build
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# copy production dependencies and source code into final image
|
||||
FROM base AS release
|
||||
COPY --from=install /temp/prod/node_modules node_modules
|
||||
COPY --from=prerelease /usr/src/app/src ./src
|
||||
COPY --from=prerelease /usr/src/app/package.json .
|
||||
COPY --from=prerelease /usr/src/app/tsconfig.json .
|
||||
COPY --from=prerelease /usr/src/app/config ./config
|
||||
COPY --from=prerelease /usr/src/app/types ./types
|
||||
|
||||
RUN mkdir -p /usr/src/app/logs && chown bun:bun /usr/src/app/logs
|
||||
|
||||
USER bun
|
||||
ENTRYPOINT [ "bun", "run", "start" ]
|
28
LICENSE
Normal file
28
LICENSE
Normal file
|
@ -0,0 +1,28 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2025, creations.works
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
78
README.md
Normal file
78
README.md
Normal file
|
@ -0,0 +1,78 @@
|
|||
# timezone-db
|
||||
|
||||
A simple Bun-powered API service for managing and retrieving user timezones. It supports both local and Discord OAuth-authenticated timezone storage, backed by PostgreSQL and Redis.
|
||||
|
||||
## Features
|
||||
|
||||
- Store user timezones via `/set` endpoint (requires Discord OAuth)
|
||||
- Retrieve timezones by user ID via `/get`
|
||||
- Cookie-based session handling using Redis
|
||||
- Built-in CORS support
|
||||
- Dockerized with PostgreSQL and DragonflyDB
|
||||
|
||||
## Requirements
|
||||
|
||||
- [Bun](https://bun.sh/)
|
||||
- Docker & Docker Compose
|
||||
- `.env` file with required environment variables
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Create a `.env` file with the following:
|
||||
|
||||
```
|
||||
HOST=0.0.0.0
|
||||
PORT=3000
|
||||
|
||||
PGHOST=postgres
|
||||
PGPORT=5432
|
||||
PGUSERNAME=postgres
|
||||
PGPASSWORD=postgres
|
||||
PGDATABASE=timezone
|
||||
|
||||
REDIS_URL=redis://dragonfly:6379
|
||||
|
||||
CLIENT_ID=your_discord_client_id
|
||||
CLIENT_SECRET=your_discord_client_secret
|
||||
REDIRECT_URI=https://your.domain/auth/discord/callback
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
### Build and Run with Docker
|
||||
|
||||
```bash
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
### Development Mode
|
||||
|
||||
```bash
|
||||
bun dev
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### `GET /get?id=<discord_user_id>`
|
||||
|
||||
Returns stored timezone and username for the given user ID.
|
||||
|
||||
### `GET /set?timezone=<iana_timezone>`
|
||||
|
||||
Stores timezone for the authenticated user. Requires Discord OAuth session.
|
||||
|
||||
### `GET /me`
|
||||
|
||||
Returns Discord profile info for the current session.
|
||||
|
||||
### `GET /auth/discord`
|
||||
|
||||
Starts OAuth2 authentication flow.
|
||||
|
||||
### `GET /auth/discord/callback`
|
||||
|
||||
Handles OAuth2 redirect and sets a session cookie.
|
||||
|
||||
## License
|
||||
|
||||
[BSD-3-Clause](LICENSE)
|
48
biome.json
Normal file
48
biome.json
Normal file
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": false
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": true,
|
||||
"ignore": ["dist", "dragonfly-data", "postgres-data"]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "tab",
|
||||
"lineEnding": "lf"
|
||||
},
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"css": {
|
||||
"formatter": {
|
||||
"indentStyle": "tab",
|
||||
"lineEnding": "lf"
|
||||
}
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"correctness": {
|
||||
"noUnusedImports": "error"
|
||||
},
|
||||
"style": {
|
||||
"useConst": "error",
|
||||
"noVar": "error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double",
|
||||
"indentStyle": "tab",
|
||||
"lineEnding": "lf",
|
||||
"jsxQuoteStyle": "double",
|
||||
"semicolons": "always"
|
||||
}
|
||||
}
|
||||
}
|
48
compose.yml
Normal file
48
compose.yml
Normal file
|
@ -0,0 +1,48 @@
|
|||
services:
|
||||
timezone-db:
|
||||
container_name: timezoneDB
|
||||
build:
|
||||
context: .
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${PORT:-3000}:${PORT:-3000}"
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
dragonfly:
|
||||
condition: service_started
|
||||
networks:
|
||||
- timezoneDB-network
|
||||
|
||||
postgres:
|
||||
image: postgres:16
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: ${PGUSERNAME:-postgres}
|
||||
POSTGRES_PASSWORD: ${PGPASSWORD:-postgres}
|
||||
POSTGRES_DB: ${PGDATABASE:-postgres}
|
||||
volumes:
|
||||
- ./postgres-data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- timezoneDB-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
dragonfly:
|
||||
image: docker.dragonflydb.io/dragonflydb/dragonfly
|
||||
restart: unless-stopped
|
||||
ulimits:
|
||||
memlock: -1
|
||||
volumes:
|
||||
- ./dragonfly-data:/data
|
||||
networks:
|
||||
- timezoneDB-network
|
||||
|
||||
networks:
|
||||
timezoneDB-network:
|
||||
driver: bridge
|
49
config/index.ts
Normal file
49
config/index.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { echo } from "@atums/echo";
|
||||
|
||||
const environment: Environment = {
|
||||
port: Number.parseInt(process.env.PORT || "8080", 10),
|
||||
host: process.env.HOST || "0.0.0.0",
|
||||
development:
|
||||
process.env.NODE_ENV === "development" || process.argv.includes("--dev"),
|
||||
};
|
||||
|
||||
const discordConfig = {
|
||||
clientId: process.env.CLIENT_ID || "",
|
||||
clientSecret: process.env.CLIENT_SECRET || "",
|
||||
redirectUri: process.env.REDIRECT_URI || "",
|
||||
};
|
||||
|
||||
function verifyRequiredVariables(): void {
|
||||
const requiredVariables = [
|
||||
"HOST",
|
||||
"PORT",
|
||||
|
||||
"PGHOST",
|
||||
"PGPORT",
|
||||
"PGUSERNAME",
|
||||
"PGPASSWORD",
|
||||
"PGDATABASE",
|
||||
|
||||
"REDIS_URL",
|
||||
|
||||
"CLIENT_ID",
|
||||
"CLIENT_SECRET",
|
||||
"REDIRECT_URI",
|
||||
];
|
||||
|
||||
let hasError = false;
|
||||
|
||||
for (const key of requiredVariables) {
|
||||
const value = process.env[key];
|
||||
if (value === undefined || value.trim() === "") {
|
||||
echo.error(`Missing or empty environment variable: ${key}`);
|
||||
hasError = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export { environment, discordConfig, verifyRequiredVariables };
|
39
logger.json
Normal file
39
logger.json
Normal file
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"directory": "./logs",
|
||||
"level": "debug",
|
||||
"disableFile": false,
|
||||
|
||||
"rotate": true,
|
||||
"maxFiles": 3,
|
||||
|
||||
"console": true,
|
||||
"consoleColor": true,
|
||||
|
||||
"dateFormat": "yyyy-MM-dd HH:mm:ss.SSS",
|
||||
"timezone": "local",
|
||||
|
||||
"silent": false,
|
||||
|
||||
"pattern": "{color:gray}{pretty-timestamp}{reset} {color:levelColor}[{level-name}]{reset} {color:gray}({reset}{file-name}:{color:blue}{line}{reset}:{color:blue}{column}{color:gray}){reset} {data}",
|
||||
"levelColor": {
|
||||
"debug": "blue",
|
||||
"info": "green",
|
||||
"warn": "yellow",
|
||||
"error": "red",
|
||||
"fatal": "red"
|
||||
},
|
||||
|
||||
"customPattern": "{color:gray}{pretty-timestamp}{reset} {color:tagColor}[{tag}]{reset} {color:contextColor}({context}){reset} {data}",
|
||||
"customColors": {
|
||||
"GET": "red",
|
||||
"POST": "blue",
|
||||
"PUT": "yellow",
|
||||
"DELETE": "red",
|
||||
"PATCH": "cyan",
|
||||
"HEAD": "magenta",
|
||||
"OPTIONS": "white",
|
||||
"TRACE": "gray"
|
||||
},
|
||||
|
||||
"prettyPrint": true
|
||||
}
|
22
package.json
Normal file
22
package.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "timezone-db",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "bun run src/index.ts",
|
||||
"dev": "bun run --hot src/index.ts --dev",
|
||||
"lint": "bunx biome check",
|
||||
"lint:fix": "bunx biome check --fix",
|
||||
"cleanup": "rm -rf logs node_modules bun.lock"
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@types/bun": "^1.2.14"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atums/echo": "^1.0.3"
|
||||
}
|
||||
}
|
79
src/discord.ts
Normal file
79
src/discord.ts
Normal file
|
@ -0,0 +1,79 @@
|
|||
import { discordConfig } from "@config";
|
||||
import { randomUUIDv7, redis } from "bun";
|
||||
|
||||
export class DiscordAuth {
|
||||
#clientId = discordConfig.clientId;
|
||||
#clientSecret = discordConfig.clientSecret;
|
||||
#redirectUri = discordConfig.redirectUri;
|
||||
|
||||
startOAuthRedirect(): Response {
|
||||
const query = new URLSearchParams({
|
||||
client_id: this.#clientId,
|
||||
redirect_uri: this.#redirectUri,
|
||||
response_type: "code",
|
||||
scope: "identify",
|
||||
});
|
||||
return Response.redirect(
|
||||
`https://discord.com/oauth2/authorize?${query}`,
|
||||
302,
|
||||
);
|
||||
}
|
||||
|
||||
async handleCallback(req: Request): Promise<Response> {
|
||||
const url = new URL(req.url);
|
||||
const code = url.searchParams.get("code");
|
||||
if (!code) return Response.json({ error: "Missing code" }, { status: 400 });
|
||||
|
||||
const tokenRes = await fetch("https://discord.com/api/oauth2/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
client_id: this.#clientId,
|
||||
client_secret: this.#clientSecret,
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
redirect_uri: this.#redirectUri,
|
||||
}),
|
||||
});
|
||||
|
||||
const tokenData: { access_token?: string } = await tokenRes.json();
|
||||
if (!tokenData.access_token)
|
||||
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
||||
|
||||
const userRes = await fetch("https://discord.com/api/users/@me", {
|
||||
headers: { Authorization: `Bearer ${tokenData.access_token}` },
|
||||
});
|
||||
const user: DiscordUser = await userRes.json();
|
||||
|
||||
const sessionId = randomUUIDv7();
|
||||
await redis.set(sessionId, JSON.stringify(user), "EX", 3600);
|
||||
|
||||
return Response.json(
|
||||
{ message: "Authenticated" },
|
||||
{
|
||||
headers: {
|
||||
"Set-Cookie": `session=${sessionId}; HttpOnly; Path=/; Max-Age=3600`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async getUser(req: Request): Promise<DiscordUser | null> {
|
||||
const cookie = req.headers.get("cookie");
|
||||
if (!cookie) return null;
|
||||
|
||||
const match = cookie.match(/session=([^;]+)/);
|
||||
if (!match) return null;
|
||||
|
||||
const sessionId = match[1];
|
||||
const userData = await redis.get(sessionId);
|
||||
if (!userData) return null;
|
||||
|
||||
try {
|
||||
return JSON.parse(userData);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
151
src/index.ts
Normal file
151
src/index.ts
Normal file
|
@ -0,0 +1,151 @@
|
|||
import { DiscordAuth } from "@/discord";
|
||||
import { echo } from "@atums/echo";
|
||||
import { environment, verifyRequiredVariables } from "@config";
|
||||
import { serve, sql } from "bun";
|
||||
|
||||
verifyRequiredVariables();
|
||||
|
||||
try {
|
||||
await sql`SELECT 1`;
|
||||
await sql`
|
||||
CREATE TABLE IF NOT EXISTS timezones (
|
||||
user_id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL,
|
||||
timezone TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
)
|
||||
`;
|
||||
|
||||
echo.info(
|
||||
`Connected to PostgreSQL on ${process.env.PGHOST}:${process.env.PGPORT}`,
|
||||
);
|
||||
} catch (error) {
|
||||
echo.error({
|
||||
message: "Could not establish a connection to PostgreSQL",
|
||||
error,
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(process.env.REDIS_URL || "redis://localhost:6379");
|
||||
echo.info(`Connected to Redis on ${url.hostname}:${url.port || "6379"}`);
|
||||
} catch (error) {
|
||||
echo.error({ message: "Redis connection failed", error });
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
echo.info(`Listening on http://${environment.host}:${environment.port}`);
|
||||
|
||||
const auth = new DiscordAuth();
|
||||
|
||||
function withCors(res: Response): Response {
|
||||
const headers = new Headers(res.headers);
|
||||
headers.set("Access-Control-Allow-Origin", "*");
|
||||
headers.set(
|
||||
"Access-Control-Allow-Methods",
|
||||
"GET, POST, PUT, DELETE, OPTIONS",
|
||||
);
|
||||
headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
||||
headers.set("Access-Control-Allow-Credentials", "true");
|
||||
headers.set("Access-Control-Max-Age", "86400");
|
||||
return new Response(res.body, {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
serve({
|
||||
port: environment.port,
|
||||
fetch: async (req) => {
|
||||
if (req.method === "OPTIONS") {
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
||||
"Access-Control-Max-Age": "86400", // 24 hours
|
||||
"Access-Control-Allow-Credentials": "true",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const url = new URL(req.url);
|
||||
|
||||
if (url.pathname === "/auth/discord") return auth.startOAuthRedirect();
|
||||
if (url.pathname === "/auth/discord/callback")
|
||||
return auth.handleCallback(req);
|
||||
|
||||
if (url.pathname === "/set") {
|
||||
const user = await auth.getUser(req);
|
||||
if (!user)
|
||||
return withCors(
|
||||
Response.json({ error: "Unauthorized" }, { status: 401 }),
|
||||
);
|
||||
|
||||
const tz = url.searchParams.get("timezone");
|
||||
if (!tz)
|
||||
return withCors(
|
||||
Response.json(
|
||||
{ error: "Timezone parameter is required" },
|
||||
{ status: 400 },
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
||||
} catch {
|
||||
return withCors(
|
||||
Response.json({ error: "Invalid timezone" }, { status: 400 }),
|
||||
);
|
||||
}
|
||||
|
||||
await sql`
|
||||
INSERT INTO timezones (user_id, username, timezone)
|
||||
VALUES (${user.id}, ${user.username}, ${tz})
|
||||
ON CONFLICT (user_id) DO UPDATE
|
||||
SET username = EXCLUDED.username, timezone = EXCLUDED.timezone
|
||||
`;
|
||||
|
||||
return withCors(Response.json({ success: true }));
|
||||
}
|
||||
|
||||
if (url.pathname === "/get") {
|
||||
const id = url.searchParams.get("id");
|
||||
if (!id)
|
||||
return withCors(
|
||||
Response.json({ error: "Missing user ID" }, { status: 400 }),
|
||||
);
|
||||
|
||||
const rows = await sql`
|
||||
SELECT username, timezone FROM timezones WHERE user_id = ${id}
|
||||
`;
|
||||
|
||||
if (rows.length === 0) {
|
||||
return withCors(
|
||||
Response.json({ error: "User not found" }, { status: 404 }),
|
||||
);
|
||||
}
|
||||
|
||||
return withCors(
|
||||
Response.json({
|
||||
user: { id, username: rows[0].username },
|
||||
timezone: rows[0].timezone,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (url.pathname === "/me") {
|
||||
const user = await auth.getUser(req);
|
||||
if (!user)
|
||||
return withCors(
|
||||
Response.json({ error: "Unauthorized" }, { status: 401 }),
|
||||
);
|
||||
return withCors(Response.json(user));
|
||||
}
|
||||
|
||||
return withCors(Response.json({ error: "Not Found" }, { status: 404 }));
|
||||
},
|
||||
});
|
29
tsconfig.json
Normal file
29
tsconfig.json
Normal file
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"@config": ["config/index.ts"]
|
||||
},
|
||||
"typeRoots": ["types", "./node_modules/@types"],
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"allowJs": false,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": false,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
},
|
||||
"include": ["src", "types", "config"]
|
||||
}
|
17
types/discord.d.ts
vendored
Normal file
17
types/discord.d.ts
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
type DiscordUser = {
|
||||
id: string;
|
||||
username: string;
|
||||
discriminator: string;
|
||||
avatar: string | null;
|
||||
bot?: boolean;
|
||||
system?: boolean;
|
||||
mfa_enabled?: boolean;
|
||||
banner?: string | null;
|
||||
accent_color?: number | null;
|
||||
locale?: string;
|
||||
verified?: boolean;
|
||||
email?: string | null;
|
||||
flags?: number;
|
||||
premium_type?: number;
|
||||
public_flags?: number;
|
||||
};
|
5
types/index.d.ts
vendored
Normal file
5
types/index.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
type Environment = {
|
||||
port: number;
|
||||
host: string;
|
||||
development: boolean;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue