Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dfc3f4cd87 | |||
| e0d8849bd1 | |||
| 429613c67e | |||
| 681555fef8 | |||
| 066b9884c2 | |||
| 9311cd3c96 | |||
| 89e6832e73 | |||
| 109f22c231 | |||
| c548abc9ed | |||
| 355338e397 | |||
| 518eeec8e8 | |||
| 413aa8994a |
@@ -1,3 +1,4 @@
|
|||||||
.env
|
.env
|
||||||
*/.env
|
*/.env
|
||||||
*.md
|
build
|
||||||
|
node_modules
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Server config
|
# Server config
|
||||||
ACCESS_TOKEN_PRIVATE_KEY=CHANGE_ME_TO_SOMETHING_RANDOM # Used to generate user tokens. Make sure this is pretty random.
|
ACCESS_TOKEN_PRIVATE_KEY=CHANGE_ME_TO_SOMETHING_RANDOM # Used to generate user tokens. Make sure this is pretty random.
|
||||||
TRUSTED_ORIGINS=http://localhost:6568 # Comma separated list of trusted origins. Make sure to include your PUBLIC_URL here.
|
TRUSTED_ORIGINS=http://localhost:6568,http://127.0.0.1:6568 # Comma separated list of trusted origins. Make sure to include your PUBLIC_URL here.
|
||||||
|
|
||||||
# TypeORM specific
|
# TypeORM specific
|
||||||
# Please make sure these match with docker-compose.yml, or your own postgres server.
|
# Please make sure these match with docker-compose.yml, or your own postgres server.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ FROM node:24-trixie-slim AS builder
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm ci --only=production && npm cache clean --force
|
RUN npm ci && npm cache clean --force
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
FROM node:24-trixie-slim AS production
|
FROM node:24-trixie-slim AS production
|
||||||
|
|||||||
70
README.md
70
README.md
@@ -1,2 +1,72 @@
|
|||||||
# kittyBE
|
# kittyBE
|
||||||
|
*Back-end for the [KittyURL](https://gitea.7o7.cx/kittyteam/kittyurl) project -- create short and memorable URLs with ease!*
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
Provide endpoints for:
|
||||||
|
- account management (`/api/v1/user/*`),
|
||||||
|
- link management (`/api/v1/link/*`),
|
||||||
|
- authed link management (when a link is bound to a user, `/api/v1/authed/*`),
|
||||||
|
- user management (for admins only, `/api/v1/admin/*`),
|
||||||
|
- general info (`/api/v1/info/*`),
|
||||||
|
|
||||||
|
KittyBE should also integrate nicely with [kittyFE](https://gitea.7o7.cx/kittyteam/kittyFE) and be easily dockerizable.
|
||||||
|
|
||||||
|
## Running kittyBE
|
||||||
|
KittyURL has been verified to work on Node 18.20+ and PostgreSQL 16.11+.
|
||||||
|
### On bare metal
|
||||||
|
Running the back-end is as simple as:
|
||||||
|
|
||||||
|
- Installing the dependencies:
|
||||||
|
- To just download the required dependencies:
|
||||||
|
```sh
|
||||||
|
npm i
|
||||||
|
```
|
||||||
|
- To install an exact copy of all of the dependencies:
|
||||||
|
```sh
|
||||||
|
npm ci
|
||||||
|
```
|
||||||
|
- Copying the .env.default file to .env, and customizing it to own preferences.
|
||||||
|
|
||||||
|
**Example:** Say, you want to add a domain to the trusted CORS origins list. To do so, your .env file in your editor of choice and append a comma (`,`) with the origin you want to add (say, `http://example.com`). Your .env file might then look as follows: `TRUSTED_ORIGINS=http://localhost:6568,http://example.com`.
|
||||||
|
|
||||||
|
**Important:** Make sure to change the `ACCESS_TOKEN_PRIVATE_KEY` variable to something secure, as this secret value will be used to generate user sessions. **Setting a weak key will allow attackers to potentially bruteforce your secret and forge user tokens!**
|
||||||
|
- Pasting your wordlist file into `src/tools/wordlist.ts`.
|
||||||
|
|
||||||
|
No wordlist file exists by default in `src/tools/wordlist.ts`. This is because wordlists were meant to be as modular as possible (with the philosophy of "bring your own wordlist"). If you leave that as-is, you'll run into runtime errors.
|
||||||
|
|
||||||
|
However, if you don't want to provide your own wordlist, and just want to get up and running as fast as possible, you're free to use the provided sample `wordlist.example-large.ts` file. Just copy it into `src/tools/wordlist.ts`:
|
||||||
|
```sh
|
||||||
|
cp wordlist.example-large.ts src/tools/wordlist.ts
|
||||||
|
```
|
||||||
|
- Launching the web server:
|
||||||
|
```sh
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
- And... that's it!
|
||||||
|
Now view your instance at http://localhost:6567, and -- if you've set the DEBUG flag in your `.env` file to `true` -- you can also visit http://localhost:6567/kttydocs/ for Swagger documentation.
|
||||||
|
|
||||||
|
### Using Docker
|
||||||
|
A Docker image is built for every release of kittyBE and [kittyFE](https://gitea.7o7.cx/kittyteam/kittyFE). For more instructions on how to run the project with Docker, please refer to the [kittyurl repository](https://gitea.7o7.cx/kittyteam/kittyurl) (which contains a sample [docker-compose.yaml file](https://gitea.7o7.cx/kittyteam/kittyurl/src/branch/master/docker-compose.yaml), as well as it's own .env file).
|
||||||
|
|
||||||
|
## Wordlists
|
||||||
|
You're free to provide your own wordlist file by pasting it into `src/tools/wordlist.ts`. For an example of how a wordlist file should look like, see `wordlist.example-large.ts`, and pay attention to the methods it exports.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
Two supplementary scripts have been provided for aid in troubleshooting database-related errors.
|
||||||
|
- Run pending migrations on your database
|
||||||
|
In a rare case, when you need to run the migrations before launching the server (as it will try running pending migrations on every launch), use:
|
||||||
|
```sh
|
||||||
|
npm run pendingMigration
|
||||||
|
```
|
||||||
|
|
||||||
|
- Issue a new migration
|
||||||
|
During development it might be necessary to issue new migrations. To do that, use:
|
||||||
|
```sh
|
||||||
|
# assuming you're in the base project directory
|
||||||
|
npm run newMigration ./src/migrations/myMigrationName
|
||||||
|
```
|
||||||
|
where `myMigrationName` is the name of your migration.
|
||||||
|
|
||||||
|
**Important:** TypeORM uses the state of your connected database when diffing for changes, unlike some other solutions, which take past migrations into consideration.
|
||||||
|
|
||||||
|
**Note:** If using other relational database than Postgres, make sure to do the due diligence of researching how to enable bigint support for your database driver. No other database type than Postgres has been tested.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "kittyBE",
|
"name": "kittyBE",
|
||||||
"version": "0.0.1",
|
"version": "0.0.3",
|
||||||
"description": "Your go-to place for short and memorable URLs.",
|
"description": "Your go-to place for short and memorable URLs.",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ AppDataSource.initialize().then(async () => {
|
|||||||
|
|
||||||
// Retrieve url, subdomain from request.
|
// Retrieve url, subdomain from request.
|
||||||
let uri: string = req.url.slice(1); // discards / from /abc, /abc -> abc
|
let uri: string = req.url.slice(1); // discards / from /abc, /abc -> abc
|
||||||
let subdomain: string | null = req.headers.host!.replace(rs.fqdn, '') || null;
|
let subdomain: string | null = req.headers.host!.replace(rs.fqdn, '').slice(0, -1) || null; // slice() to remove trailing dot
|
||||||
|
|
||||||
// Try to lookup the url in DB
|
// Try to lookup the url in DB
|
||||||
const reversedLink: Link | null = await linkService.lookupUriWithExpiryValidation(uri, subdomain);
|
const reversedLink: Link | null = await linkService.lookupUriWithExpiryValidation(uri, subdomain);
|
||||||
|
|||||||
@@ -167,7 +167,6 @@ export async function createLinkHandler(
|
|||||||
|
|
||||||
// Failed (short uri + if enabled, subdomain combo is taken)?
|
// Failed (short uri + if enabled, subdomain combo is taken)?
|
||||||
if (returnedId.exists && returnedId.id == -1) {
|
if (returnedId.exists && returnedId.id == -1) {
|
||||||
console.log(returnedId);
|
|
||||||
const error: ms.ErrorDTO = {
|
const error: ms.ErrorDTO = {
|
||||||
status: 'error',
|
status: 'error',
|
||||||
error: `"${req.body.uri}" is already taken. Maybe try "${generateSentenceString()}"?`,
|
error: `"${req.body.uri}" is already taken. Maybe try "${generateSentenceString()}"?`,
|
||||||
|
|||||||
@@ -15,4 +15,5 @@ export const AppDataSource = new DataSource({
|
|||||||
entities: [__dirname + '/entities/*.ts'],
|
entities: [__dirname + '/entities/*.ts'],
|
||||||
migrations: [__dirname + '/migrations/*.ts'],
|
migrations: [__dirname + '/migrations/*.ts'],
|
||||||
subscribers: [],
|
subscribers: [],
|
||||||
|
parseInt8: true // https://github.com/typeorm/typeorm/issues/9341#issuecomment-1268986627
|
||||||
})
|
})
|
||||||
|
|||||||
38
src/middleware/requireAdmin.ts
Normal file
38
src/middleware/requireAdmin.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { ErrorDTO } from '../schemas/miscSchema';
|
||||||
|
import * as jwt from '../tools/jwt';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if user has administrative privileges.
|
||||||
|
*
|
||||||
|
* This needs to happen AFTER ensuring this is not a guest session.
|
||||||
|
* So: use requireUser first, and after that requireAdmin to enforce
|
||||||
|
* admin privilege requirement.
|
||||||
|
*
|
||||||
|
* @param {Request} req The request
|
||||||
|
* @param {Response} res The resource
|
||||||
|
* @param {(Function|NextFunction)} next The next
|
||||||
|
* @return {any} Next function on success, unauthorized error otherwise
|
||||||
|
*/
|
||||||
|
const requireAdmin = (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const user: jwt.JwtStatus = res.locals.user;
|
||||||
|
let error: ErrorDTO | null = null;
|
||||||
|
|
||||||
|
// Check if role is set to 1 (1 = admin, 0 = standard user).
|
||||||
|
if (user.decoded?.role !== 1)
|
||||||
|
error = {
|
||||||
|
status: 'error',
|
||||||
|
error: 'Unauthorized, admin access required',
|
||||||
|
code: 'unauthorized_non_admin'
|
||||||
|
};
|
||||||
|
|
||||||
|
// It is? Send 401 unauthorized.
|
||||||
|
if (error !== null)
|
||||||
|
return res.status(401)
|
||||||
|
.send(error);
|
||||||
|
|
||||||
|
// Otherwise jump to next endpoint.
|
||||||
|
return next();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default requireAdmin;
|
||||||
@@ -1,7 +1,20 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { ErrorDTO } from "../schemas/miscSchema";
|
import { ErrorDTO } from '../schemas/miscSchema';
|
||||||
import * as jwt from "../tools/jwt";
|
import * as jwt from '../tools/jwt';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if user is singed in.
|
||||||
|
* Returns 401 when user is unauthorized.
|
||||||
|
*
|
||||||
|
* To check if user is an admin, chain requireUser and requireAdmin together.
|
||||||
|
* So: use requireUser first, and after that requireAdmin to enforce
|
||||||
|
* admin privilege requirement.
|
||||||
|
*
|
||||||
|
* @param {Request} req The request
|
||||||
|
* @param {Response} res The resource
|
||||||
|
* @param {(Function|NextFunction)} next The next
|
||||||
|
* @return {any} Next function on success, unauthorized error otherwise
|
||||||
|
*/
|
||||||
const requireUser = (req: Request, res: Response, next: NextFunction) => {
|
const requireUser = (req: Request, res: Response, next: NextFunction) => {
|
||||||
const user: jwt.JwtStatus = res.locals.user;
|
const user: jwt.JwtStatus = res.locals.user;
|
||||||
let error: ErrorDTO | null = null;
|
let error: ErrorDTO | null = null;
|
||||||
@@ -38,4 +51,4 @@ const requireUser = (req: Request, res: Response, next: NextFunction) => {
|
|||||||
return next();
|
return next();
|
||||||
};
|
};
|
||||||
|
|
||||||
export default requireUser;
|
export default requireUser;
|
||||||
|
|||||||
@@ -104,9 +104,11 @@ linkRouter.get('/api/v1/link/fromWordlist', validateSchema(ls.sentenceLinkReques
|
|||||||
* post:
|
* post:
|
||||||
* description:
|
* description:
|
||||||
* Register a new shortened URL. <br/>
|
* Register a new shortened URL. <br/>
|
||||||
* See linkSchema.ts for constraints.
|
* See linkSchema.ts for constraints. <br/>
|
||||||
|
* <b>Note:</b> This endpoint's functionality differs depending on the user info,
|
||||||
|
* which means guests will be treated differently from authenticated users.
|
||||||
* tags: [Link]
|
* tags: [Link]
|
||||||
* summary: Shorten a link
|
* summary: "[AUTHED?] Shorten a link"
|
||||||
* requestBody:
|
* requestBody:
|
||||||
* required: true
|
* required: true
|
||||||
* content:
|
* content:
|
||||||
|
|||||||
@@ -29,4 +29,4 @@ export type ErrorDTO = {
|
|||||||
// Used to check against reserved names.
|
// Used to check against reserved names.
|
||||||
export const disallowedUriSchema = z
|
export const disallowedUriSchema = z
|
||||||
.string()
|
.string()
|
||||||
.regex(/^(about|assets|kttydocs|panel)/);
|
.regex(/^(about|assets|healthcheck|kttydocs|panel)/);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"es2021"
|
"es2021"
|
||||||
],
|
],
|
||||||
"types": ["node"],
|
"types": ["node"],
|
||||||
"target": "es2021",
|
"target": "esnext",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"outDir": "./build",
|
"outDir": "./build",
|
||||||
|
|||||||
Reference in New Issue
Block a user