Compare commits
11 Commits
c548abc9ed
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 6114416b8b | |||
| 5771a182fe | |||
| dfc3f4cd87 | |||
| e0d8849bd1 | |||
| 429613c67e | |||
| 681555fef8 | |||
| 066b9884c2 | |||
| 9311cd3c96 | |||
| 89e6832e73 | |||
| 109f22c231 | |||
| 355338e397 |
@@ -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
|
||||||
|
|||||||
@@ -26,10 +26,14 @@ Running the back-end is as simple as:
|
|||||||
npm ci
|
npm ci
|
||||||
```
|
```
|
||||||
- Copying the .env.default file to .env, and customizing it to own preferences.
|
- 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`.
|
**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!**
|
**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`.
|
- 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.
|
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`:
|
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
|
```sh
|
||||||
cp wordlist.example-large.ts src/tools/wordlist.ts
|
cp wordlist.example-large.ts src/tools/wordlist.ts
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -94,7 +94,10 @@ export async function createLinkHandler(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
// Using locals to retrieve decoded user JWT.
|
// Using locals to retrieve decoded user JWT.
|
||||||
const decodedUser: jwt.JwtDecoded | undefined = res.locals.user?.decoded;
|
// jwt.JwtDecoded when JWT is supplied
|
||||||
|
// undefined if not
|
||||||
|
// null if is invalid (expired)
|
||||||
|
const decodedUser: jwt.JwtDecoded | undefined | null = res.locals.user?.decoded;
|
||||||
const linkService = new LinkService();
|
const linkService = new LinkService();
|
||||||
const subdomainsAllowed: boolean = env.getBool('useSubdomains', true)!;
|
const subdomainsAllowed: boolean = env.getBool('useSubdomains', true)!;
|
||||||
const rewriteStrings: env.RewriteStrings = env.getRewriteStrings();
|
const rewriteStrings: env.RewriteStrings = env.getRewriteStrings();
|
||||||
@@ -114,7 +117,7 @@ export async function createLinkHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let user: User | null = null;
|
let user: User | null = null;
|
||||||
if (decodedUser !== undefined) {
|
if (decodedUser !== undefined && decodedUser !== null) {
|
||||||
// If user is logged in, retrieve the account.
|
// If user is logged in, retrieve the account.
|
||||||
const userService = new UserService();
|
const userService = new UserService();
|
||||||
user = await userService.findById(decodedUser.sub);
|
user = await userService.findById(decodedUser.sub);
|
||||||
|
|||||||
@@ -15,5 +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
|
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|healthcheck|kttydocs|panel)/);
|
.regex(/^(about|assets|healthcheck|kttydocs|panel|robots\.txt)/);
|
||||||
|
|||||||
Reference in New Issue
Block a user