Compare commits

...

47 Commits
1.0.1 ... 1.2.0

Author SHA1 Message Date
Milo Schwartz
6f683ca486 Merge pull request #467 from fosrl/dev
1.2.0
2025-04-06 13:11:02 -04:00
miloschwartz
0e65f8c921 check resource id on verify access token 2025-04-06 13:08:55 -04:00
Owen
dfcab90c2d Add permissions 2025-04-06 12:05:13 -04:00
Owen
5a6a035d30 Add permissions 2025-04-06 12:04:42 -04:00
Owen
d76ff17fb3 Add stale bot 2025-04-06 12:02:22 -04:00
Owen
1f570e9b46 Add stale bot 2025-04-06 12:01:37 -04:00
miloschwartz
4953e69b1b comment out old create newt endpoint 2025-04-06 11:48:42 -04:00
miloschwartz
ab6ecdbc9c update config file templates 2025-04-06 11:46:08 -04:00
miloschwartz
0b7ca95d21 update badger in migration 2025-04-06 11:39:16 -04:00
miloschwartz
6cc4bc2645 add pass access token in headers 2025-04-05 22:36:51 -04:00
miloschwartz
74d6b3d902 shorten share links and add migration 2025-04-04 22:58:01 -04:00
Owen
302094771b Merge branch 'main' into dev 2025-04-01 22:49:39 -04:00
miloschwartz
80ef8f189e replace old error formatting with zod format error 2025-03-31 22:37:39 -04:00
miloschwartz
6204fa0ade fix update server admin email cause create new user closes #443 2025-03-31 15:03:21 -04:00
miloschwartz
1d105fc5be fix count in list domains endpoint 2025-03-31 12:51:08 -04:00
miloschwartz
3612857585 use global search for tables 2025-03-31 10:31:35 -04:00
miloschwartz
8f1ee60119 fix showing not protected when only password enabled closes #129 2025-03-31 10:16:34 -04:00
miloschwartz
e7ca7fe89c add toggle resource visibility closes #442 2025-03-31 10:10:54 -04:00
Owen Schwartz
4be1d87602 Merge pull request #419 from hareland/fix-bandwidth
fix(bandwidth): Record correct megaBytesIn/megaBytesOut
2025-03-30 11:04:00 -04:00
hareland
131df8aeb7 fix(bandwidth): use correct flipping and addition 2025-03-28 14:54:37 +01:00
hareland
3442942893 fix(bandwidth): Record correct megaBytesIn/megaBytesOut 2025-03-28 12:06:17 +01:00
miloschwartz
fbd78ab842 add new checkbox variants 2025-03-27 23:12:36 -04:00
miloschwartz
66f324e18c fix typo in form description 2025-03-27 17:42:51 -04:00
miloschwartz
5e2f9e1eeb add createNewt action and remove max orgs restriction 2025-03-26 22:20:22 -04:00
miloschwartz
fefb07e14c move schema.ts to module 2025-03-23 17:11:48 -04:00
miloschwartz
013f342ff6 fix broken header layout on mobile 2025-03-23 12:37:57 -04:00
miloschwartz
aabdcea3c0 add docs link 2025-03-22 12:37:35 -04:00
miloschwartz
a178faa377 add support links 2025-03-22 12:35:33 -04:00
miloschwartz
edf0ce226f Merge branch 'main' into dev 2025-03-22 12:25:00 -04:00
miloschwartz
7118ae374d fix try catch in supporter keys 2025-03-22 12:24:20 -04:00
miloschwartz
f2a14e6a36 append timestamp to cookie name to prevent redirect loops 2025-03-21 21:38:36 -04:00
miloschwartz
f37be774a6 disable limited tier if already used 2025-03-21 18:36:11 -04:00
miloschwartz
0dcfeb3587 add server admin panel to delete users 2025-03-21 18:04:27 -04:00
Owen
dbfc8b51aa Update default email 2025-03-21 17:18:51 -04:00
miloschwartz
d72a8af04b log failed check key 2025-03-21 17:14:27 -04:00
Owen
7131dea7a0 package-lock update 2025-03-21 17:12:59 -04:00
Owen
deb30ed4ae Add admin user api interfaces 2025-03-21 17:05:04 -04:00
Owen
3b09ef3345 False by default for crowdsec 2nd question 2025-03-21 09:57:28 -04:00
miloschwartz
06e90c9555 don't add wildcard asterisk for base domain resource closes #356 2025-03-20 22:52:29 -04:00
miloschwartz
cdc415079c add supporer key program 2025-03-20 22:16:02 -04:00
miloschwartz
1c2ba4076a add crowdsec warning to installer 2025-03-19 21:17:19 -04:00
miloschwartz
af68aa692c fix header padding on large screens 2025-03-17 11:53:30 -04:00
miloschwartz
edba818615 add new create site workflow 2025-03-16 15:20:19 -04:00
miloschwartz
cdf904a2bc fix broken docs link closes #335 2025-03-15 17:37:27 -04:00
miloschwartz
fedab6c9a8 Merge branch 'main' into dev 2025-03-11 21:03:25 -04:00
Milo Schwartz
33e8ed4c93 Update README.md 2025-03-11 21:02:42 -04:00
miloschwartz
2b54dfe035 fix rules info columns 2025-03-10 12:47:42 -04:00
209 changed files with 6131 additions and 877 deletions

37
.github/workflows/stale-bot.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Mark and Close Stale Issues
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch: # Allow manual trigger
permissions:
contents: write # only for delete-branch option
issues: write
pull-requests: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
days-before-stale: 30
days-before-close: 14
stale-issue-message: 'This issue has been automatically marked as stale due to 30 days of inactivity. It will be closed in 14 days if no further activity occurs.'
close-issue-message: 'This issue has been automatically closed due to inactivity. If you believe this is still relevant, please open a new issue with up-to-date information.'
stale-issue-label: 'stale'
exempt-issue-labels: 'needs investigating, networking, new feature, reverse proxy, bug, api, authentication, documentation, enhancement, help wanted, good first issue, question'
exempt-all-issue-assignees: true
only-labels: ''
exempt-pr-labels: ''
days-before-pr-stale: -1
days-before-pr-close: -1
operations-per-run: 100
remove-stale-when-updated: true
delete-branch: false
enable-statistics: true

View File

@@ -7,7 +7,7 @@ RUN npm ci
COPY . . COPY . .
RUN npx drizzle-kit generate --dialect sqlite --schema ./server/db/schema.ts --out init RUN npx drizzle-kit generate --dialect sqlite --schema ./server/db/schemas/ --out init
RUN npm run build RUN npm run build

View File

@@ -18,12 +18,12 @@ _Your own self-hosted zero trust tunnel._
<div align="center"> <div align="center">
<h5> <h5>
<a href="https://docs.fossorial.io/Getting%20Started/quick-install"> <a href="https://fossorial.io">
Install Guide Website
</a> </a>
<span> | </span> <span> | </span>
<a href="https://docs.fossorial.io"> <a href="https://docs.fossorial.io/Getting%20Started/quick-install">
Full Documentation Install Guide
</a> </a>
<span> | </span> <span> | </span>
<a href="mailto:numbat@fossorial.io"> <a href="mailto:numbat@fossorial.io">
@@ -136,7 +136,7 @@ View the [project board](https://github.com/orgs/fosrl/projects/1) for more deta
## Licensing ## Licensing
Pangolin is dual licensed under the AGPLv3 and the Fossorial Commercial license. For inquiries about commercial licensing, please contact us at [numbat@fossorial.io](mailto:numbat@fossorial.io). Pangolin is dual licensed under the AGPL-3 and the Fossorial Commercial license. To see our commercial offerings, please see our [website](https://fossorial.io) for details. For inquiries about commercial licensing, please contact us at [numbat@fossorial.io](mailto:numbat@fossorial.io).
## Contributions ## Contributions

View File

@@ -12,7 +12,7 @@ post {
body:json { body:json {
{ {
"email": "owen@fossorial.io", "email": "admin@fosrl.io",
"password": "Password123!" "password": "Password123!"
} }
} }

View File

@@ -0,0 +1,11 @@
meta {
name: adminListUsers
type: http
seq: 2
}
get {
url: http://localhost:3000/api/v1/users
body: none
auth: none
}

View File

@@ -0,0 +1,11 @@
meta {
name: adminRemoveUser
type: http
seq: 3
}
delete {
url: http://localhost:3000/api/v1/user/ky5r7ivqs8wc7u4
body: none
auth: none
}

View File

@@ -18,6 +18,9 @@ server:
internal_hostname: "pangolin" internal_hostname: "pangolin"
session_cookie_name: "p_session_token" session_cookie_name: "p_session_token"
resource_access_token_param: "p_token" resource_access_token_param: "p_token"
resource_access_token_headers:
id: "P-Access-Token-Id"
token: "P-Access-Token"
resource_session_request_param: "p_session_request" resource_session_request_param: "p_session_request"
traefik: traefik:
@@ -35,7 +38,7 @@ gerbil:
rate_limits: rate_limits:
global: global:
window_minutes: 1 window_minutes: 1
max_requests: 100 max_requests: 500
users: users:
server_admin: server_admin:

View File

@@ -4,10 +4,10 @@ import path from "path";
export default defineConfig({ export default defineConfig({
dialect: "sqlite", dialect: "sqlite",
schema: path.join("server", "db", "schema.ts"), schema: path.join("server", "db", "schemas"),
out: path.join("server", "migrations"), out: path.join("server", "migrations"),
verbose: true, verbose: true,
dbCredentials: { dbCredentials: {
url: path.join(APP_PATH, "db", "db.sqlite"), url: path.join(APP_PATH, "db", "db.sqlite")
}, }
}); });

View File

@@ -18,6 +18,9 @@ server:
internal_hostname: "pangolin" internal_hostname: "pangolin"
session_cookie_name: "p_session_token" session_cookie_name: "p_session_token"
resource_access_token_param: "p_token" resource_access_token_param: "p_token"
resource_access_token_headers:
id: "P-Access-Token-Id"
token: "P-Access-Token"
resource_session_request_param: "p_session_request" resource_session_request_param: "p_session_request"
cors: cors:
origins: ["https://{{.DashboardDomain}}"] origins: ["https://{{.DashboardDomain}}"]
@@ -41,7 +44,7 @@ gerbil:
rate_limits: rate_limits:
global: global:
window_minutes: 1 window_minutes: 1
max_requests: 100 max_requests: 500
{{if .EnableEmail}} {{if .EnableEmail}}
email: email:
smtp_host: "{{.EmailSMTPHost}}" smtp_host: "{{.EmailSMTPHost}}"

View File

@@ -2,19 +2,19 @@ package main
import ( import (
"bufio" "bufio"
"bytes"
"embed" "embed"
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
"os" "os"
"time"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"syscall" "syscall"
"bytes"
"text/template" "text/template"
"time"
"unicode" "unicode"
"golang.org/x/term" "golang.org/x/term"
@@ -48,8 +48,8 @@ type Config struct {
EmailSMTPPass string EmailSMTPPass string
EmailNoReply string EmailNoReply string
InstallGerbil bool InstallGerbil bool
TraefikBouncerKey string TraefikBouncerKey string
DoCrowdsecInstall bool DoCrowdsecInstall bool
} }
func main() { func main() {
@@ -95,33 +95,35 @@ func main() {
} }
if !checkIsCrowdsecInstalledInCompose() { if !checkIsCrowdsecInstalledInCompose() {
fmt.Println("\n=== Crowdsec Install ===") fmt.Println("\n=== CrowdSec Install ===")
// check if crowdsec is installed // check if crowdsec is installed
if readBool(reader, "Would you like to install Crowdsec?", true) { if readBool(reader, "Would you like to install CrowdSec?", false) {
fmt.Println("This installer constitutes a minimal viable CrowdSec deployment. CrowdSec will add extra complexity to your Pangolin installation and may not work to the best of its abilities out of the box. Users are expected to implement configuration adjustments on their own to achieve the best security posture. Consult the CrowdSec documentation for detailed configuration instructions.")
if readBool(reader, "Are you willing to manage CrowdSec?", false) {
if config.DashboardDomain == "" {
traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml", "config/traefik/dynamic_config.yml")
if err != nil {
fmt.Printf("Error reading config: %v\n", err)
return
}
config.DashboardDomain = traefikConfig.DashboardDomain
config.LetsEncryptEmail = traefikConfig.LetsEncryptEmail
config.BadgerVersion = traefikConfig.BadgerVersion
if config.DashboardDomain == "" { // print the values and check if they are right
traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml", "config/traefik/dynamic_config.yml") fmt.Println("Detected values:")
if err != nil { fmt.Printf("Dashboard Domain: %s\n", config.DashboardDomain)
fmt.Printf("Error reading config: %v\n", err) fmt.Printf("Let's Encrypt Email: %s\n", config.LetsEncryptEmail)
return fmt.Printf("Badger Version: %s\n", config.BadgerVersion)
if !readBool(reader, "Are these values correct?", true) {
config = collectUserInput(reader)
}
} }
config.DashboardDomain = traefikConfig.DashboardDomain
config.LetsEncryptEmail = traefikConfig.LetsEncryptEmail
config.BadgerVersion = traefikConfig.BadgerVersion
// print the values and check if they are right config.DoCrowdsecInstall = true
fmt.Println("Detected values:") installCrowdsec(config)
fmt.Printf("Dashboard Domain: %s\n", config.DashboardDomain)
fmt.Printf("Let's Encrypt Email: %s\n", config.LetsEncryptEmail)
fmt.Printf("Badger Version: %s\n", config.BadgerVersion)
if !readBool(reader, "Are these values correct?", true) {
config = collectUserInput(reader)
}
} }
config.DoCrowdsecInstall = true
installCrowdsec(config)
} }
} }
@@ -143,23 +145,23 @@ func readString(reader *bufio.Reader, prompt string, defaultValue string) string
} }
func readPassword(prompt string, reader *bufio.Reader) string { func readPassword(prompt string, reader *bufio.Reader) string {
if term.IsTerminal(int(syscall.Stdin)) { if term.IsTerminal(int(syscall.Stdin)) {
fmt.Print(prompt + ": ") fmt.Print(prompt + ": ")
// Read password without echo if we're in a terminal // Read password without echo if we're in a terminal
password, err := term.ReadPassword(int(syscall.Stdin)) password, err := term.ReadPassword(int(syscall.Stdin))
fmt.Println() // Add a newline since ReadPassword doesn't add one fmt.Println() // Add a newline since ReadPassword doesn't add one
if err != nil { if err != nil {
return "" return ""
} }
input := strings.TrimSpace(string(password)) input := strings.TrimSpace(string(password))
if input == "" { if input == "" {
return readPassword(prompt, reader) return readPassword(prompt, reader)
} }
return input return input
} else { } else {
// Fallback to reading from stdin if not in a terminal // Fallback to reading from stdin if not in a terminal
return readString(reader, prompt, "") return readString(reader, prompt, "")
} }
} }
func readBool(reader *bufio.Reader, prompt string, defaultValue bool) bool { func readBool(reader *bufio.Reader, prompt string, defaultValue bool) bool {
@@ -324,10 +326,10 @@ func createConfigFiles(config Config) error {
return nil return nil
} }
// skip .DS_Store // skip .DS_Store
if strings.Contains(path, ".DS_Store") { if strings.Contains(path, ".DS_Store") {
return nil return nil
} }
if d.IsDir() { if d.IsDir() {
// Create directory // Create directory
@@ -376,7 +378,6 @@ func createConfigFiles(config Config) error {
return nil return nil
} }
func installDocker() error { func installDocker() error {
// Detect Linux distribution // Detect Linux distribution
cmd := exec.Command("cat", "/etc/os-release") cmd := exec.Command("cat", "/etc/os-release")
@@ -654,29 +655,29 @@ func moveFile(src, dst string) error {
} }
func waitForContainer(containerName string) error { func waitForContainer(containerName string) error {
maxAttempts := 30 maxAttempts := 30
retryInterval := time.Second * 2 retryInterval := time.Second * 2
for attempt := 0; attempt < maxAttempts; attempt++ { for attempt := 0; attempt < maxAttempts; attempt++ {
// Check if container is running // Check if container is running
cmd := exec.Command("docker", "container", "inspect", "-f", "{{.State.Running}}", containerName) cmd := exec.Command("docker", "container", "inspect", "-f", "{{.State.Running}}", containerName)
var out bytes.Buffer var out bytes.Buffer
cmd.Stdout = &out cmd.Stdout = &out
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
// If the container doesn't exist or there's another error, wait and retry // If the container doesn't exist or there's another error, wait and retry
time.Sleep(retryInterval) time.Sleep(retryInterval)
continue continue
} }
isRunning := strings.TrimSpace(out.String()) == "true" isRunning := strings.TrimSpace(out.String()) == "true"
if isRunning { if isRunning {
return nil return nil
} }
// Container exists but isn't running yet, wait and retry // Container exists but isn't running yet, wait and retry
time.Sleep(retryInterval) time.Sleep(retryInterval)
} }
return fmt.Errorf("container %s did not start within %v seconds", containerName, maxAttempts*int(retryInterval.Seconds())) return fmt.Errorf("container %s did not start within %v seconds", containerName, maxAttempts*int(retryInterval.Seconds()))
} }

2668
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -57,6 +57,7 @@
"glob": "11.0.0", "glob": "11.0.0",
"helmet": "8.0.0", "helmet": "8.0.0",
"http-errors": "2.0.0", "http-errors": "2.0.0",
"i": "^0.3.7",
"input-otp": "1.4.1", "input-otp": "1.4.1",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"lucide-react": "0.469.0", "lucide-react": "0.469.0",
@@ -66,12 +67,14 @@
"node-cache": "5.1.2", "node-cache": "5.1.2",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"nodemailer": "6.9.16", "nodemailer": "6.9.16",
"npm": "^11.2.0",
"oslo": "1.2.1", "oslo": "1.2.1",
"qrcode.react": "4.2.0", "qrcode.react": "4.2.0",
"react": "19.0.0", "react": "19.0.0",
"react-dom": "19.0.0", "react-dom": "19.0.0",
"react-easy-sort": "^1.6.0", "react-easy-sort": "^1.6.0",
"react-hook-form": "7.54.2", "react-hook-form": "7.54.2",
"react-icons": "^5.5.0",
"rebuild": "0.1.2", "rebuild": "0.1.2",
"semver": "7.6.3", "semver": "7.6.3",
"tailwind-merge": "2.6.0", "tailwind-merge": "2.6.0",

View File

@@ -14,7 +14,7 @@ import { logIncomingMiddleware } from "./middlewares/logIncoming";
import { csrfProtectionMiddleware } from "./middlewares/csrfProtection"; import { csrfProtectionMiddleware } from "./middlewares/csrfProtection";
import helmet from "helmet"; import helmet from "helmet";
const dev = process.env.ENVIRONMENT !== "prod"; const dev = config.isDev;
const externalPort = config.getRawConfig().server.external_port; const externalPort = config.getRawConfig().server.external_port;
export function createApiServer() { export function createApiServer() {

View File

@@ -1,6 +1,6 @@
import { Request } from "express"; import { Request } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { userActions, roleActions, userOrgs } from "@server/db/schema"; import { userActions, roleActions, userOrgs } from "@server/db/schemas";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
@@ -63,6 +63,7 @@ export enum ActionsEnum {
listResourceRules = "listResourceRules", listResourceRules = "listResourceRules",
updateResourceRule = "updateResourceRule", updateResourceRule = "updateResourceRule",
listOrgDomains = "listOrgDomains", listOrgDomains = "listOrgDomains",
createNewt = "createNewt",
} }
export async function checkUserActionPermission( export async function checkUserActionPermission(

View File

@@ -1,6 +1,6 @@
import db from "@server/db"; import db from "@server/db";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { roleResources, userResources } from "@server/db/schema"; import { roleResources, userResources } from "@server/db/schemas";
export async function canUserAccessResource({ export async function canUserAccessResource({
userId, userId,

View File

@@ -1,5 +1,5 @@
import db from "@server/db"; import db from "@server/db";
import { UserInvite, userInvites } from "@server/db/schema"; import { UserInvite, userInvites } from "@server/db/schemas";
import { isWithinExpirationDate } from "oslo"; import { isWithinExpirationDate } from "oslo";
import { verifyPassword } from "./password"; import { verifyPassword } from "./password";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";

View File

@@ -1,5 +1,5 @@
import { db } from '@server/db'; import { db } from '@server/db';
import { limitsTable } from '@server/db/schema'; import { limitsTable } from '@server/db/schemas';
import { and, eq } from 'drizzle-orm'; import { and, eq } from 'drizzle-orm';
import createHttpError from 'http-errors'; import createHttpError from 'http-errors';
import HttpCode from '@server/types/HttpCode'; import HttpCode from '@server/types/HttpCode';

View File

@@ -1,5 +1,5 @@
import db from "@server/db"; import db from "@server/db";
import { resourceOtp } from "@server/db/schema"; import { resourceOtp } from "@server/db/schemas";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { createDate, isWithinExpirationDate, TimeSpan } from "oslo"; import { createDate, isWithinExpirationDate, TimeSpan } from "oslo";
import { alphabet, generateRandomString, sha256 } from "oslo/crypto"; import { alphabet, generateRandomString, sha256 } from "oslo/crypto";

View File

@@ -1,7 +1,7 @@
import { TimeSpan, createDate } from "oslo"; import { TimeSpan, createDate } from "oslo";
import { generateRandomString, alphabet } from "oslo/crypto"; import { generateRandomString, alphabet } from "oslo/crypto";
import db from "@server/db"; import db from "@server/db";
import { users, emailVerificationCodes } from "@server/db/schema"; import { users, emailVerificationCodes } from "@server/db/schemas";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { sendEmail } from "@server/emails"; import { sendEmail } from "@server/emails";
import config from "@server/lib/config"; import config from "@server/lib/config";

View File

@@ -9,7 +9,7 @@ import {
sessions, sessions,
User, User,
users users
} from "@server/db/schema"; } from "@server/db/schemas";
import db from "@server/db"; import db from "@server/db";
import { eq, inArray } from "drizzle-orm"; import { eq, inArray } from "drizzle-orm";
import config from "@server/lib/config"; import config from "@server/lib/config";

View File

@@ -2,7 +2,7 @@ import {
encodeHexLowerCase, encodeHexLowerCase,
} from "@oslojs/encoding"; } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2"; import { sha256 } from "@oslojs/crypto/sha2";
import { Newt, newts, newtSessions, NewtSession } from "@server/db/schema"; import { Newt, newts, newtSessions, NewtSession } from "@server/db/schemas";
import db from "@server/db"; import db from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";

View File

@@ -1,6 +1,6 @@
import { encodeHexLowerCase } from "@oslojs/encoding"; import { encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2"; import { sha256 } from "@oslojs/crypto/sha2";
import { resourceSessions, ResourceSession } from "@server/db/schema"; import { resourceSessions, ResourceSession } from "@server/db/schemas";
import db from "@server/db"; import db from "@server/db";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import config from "@server/lib/config"; import config from "@server/lib/config";
@@ -170,16 +170,17 @@ export function serializeResourceSessionCookie(
isHttp: boolean = false, isHttp: boolean = false,
expiresAt?: Date expiresAt?: Date
): string { ): string {
const now = new Date().getTime();
if (!isHttp) { if (!isHttp) {
if (expiresAt === undefined) { if (expiresAt === undefined) {
return `${cookieName}_s=${token}; HttpOnly; SameSite=Lax; Path=/; Secure; Domain=${"." + domain}`; return `${cookieName}_s.${now}=${token}; HttpOnly; SameSite=Lax; Path=/; Secure; Domain=${"." + domain}`;
} }
return `${cookieName}_s=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Secure; Domain=${"." + domain}`; return `${cookieName}_s.${now}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Secure; Domain=${"." + domain}`;
} else { } else {
if (expiresAt === undefined) { if (expiresAt === undefined) {
return `${cookieName}=${token}; HttpOnly; SameSite=Lax; Path=/; Domain=${"." + domain}`; return `${cookieName}.${now}=${token}; HttpOnly; SameSite=Lax; Path=/; Domain=${"." + domain}`;
} }
return `${cookieName}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Domain=${"." + domain}`; return `${cookieName}.${now}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Domain=${"." + domain}`;
} }
} }

View File

@@ -1,6 +1,6 @@
import { verify } from "@node-rs/argon2"; import { verify } from "@node-rs/argon2";
import db from "@server/db"; import db from "@server/db";
import { twoFactorBackupCodes } from "@server/db/schema"; import { twoFactorBackupCodes } from "@server/db/schemas";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { decodeHex } from "oslo/encoding"; import { decodeHex } from "oslo/encoding";
import { TOTPController } from "oslo/otp"; import { TOTPController } from "oslo/otp";

View File

@@ -3,53 +3,95 @@ import {
Resource, Resource,
ResourceAccessToken, ResourceAccessToken,
resourceAccessToken, resourceAccessToken,
} from "@server/db/schema"; resources
} from "@server/db/schemas";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { isWithinExpirationDate } from "oslo"; import { isWithinExpirationDate } from "oslo";
import { verifyPassword } from "./password"; import { verifyPassword } from "./password";
import { encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";
export async function verifyResourceAccessToken({ export async function verifyResourceAccessToken({
resource, accessToken,
accessTokenId, accessTokenId,
accessToken resourceId
}: { }: {
resource: Resource;
accessTokenId: string;
accessToken: string; accessToken: string;
accessTokenId?: string;
resourceId?: number; // IF THIS IS NOT SET, THE TOKEN IS VALID FOR ALL RESOURCES
}): Promise<{ }): Promise<{
valid: boolean; valid: boolean;
error?: string; error?: string;
tokenItem?: ResourceAccessToken; tokenItem?: ResourceAccessToken;
resource?: Resource;
}> { }> {
const [result] = await db const accessTokenHash = encodeHexLowerCase(
.select() sha256(new TextEncoder().encode(accessToken))
.from(resourceAccessToken) );
.where(
and(
eq(resourceAccessToken.resourceId, resource.resourceId),
eq(resourceAccessToken.accessTokenId, accessTokenId)
)
)
.limit(1);
const tokenItem = result; let tokenItem: ResourceAccessToken | undefined;
let resource: Resource | undefined;
if (!tokenItem) { if (!accessTokenId) {
const [res] = await db
.select()
.from(resourceAccessToken)
.where(and(eq(resourceAccessToken.tokenHash, accessTokenHash)))
.innerJoin(
resources,
eq(resourceAccessToken.resourceId, resources.resourceId)
);
tokenItem = res?.resourceAccessToken;
resource = res?.resources;
} else {
const [res] = await db
.select()
.from(resourceAccessToken)
.where(and(eq(resourceAccessToken.accessTokenId, accessTokenId)))
.innerJoin(
resources,
eq(resourceAccessToken.resourceId, resources.resourceId)
);
if (res && res.resourceAccessToken) {
if (res.resourceAccessToken.tokenHash?.startsWith("$argon")) {
const validCode = await verifyPassword(
accessToken,
res.resourceAccessToken.tokenHash
);
if (!validCode) {
return {
valid: false,
error: "Invalid access token"
};
}
} else {
const tokenHash = encodeHexLowerCase(
sha256(new TextEncoder().encode(accessToken))
);
if (res.resourceAccessToken.tokenHash !== tokenHash) {
return {
valid: false,
error: "Invalid access token"
};
}
}
}
tokenItem = res?.resourceAccessToken;
resource = res?.resources;
}
if (!tokenItem || !resource) {
return { return {
valid: false, valid: false,
error: "Access token does not exist for resource" error: "Access token does not exist for resource"
}; };
} }
const validCode = await verifyPassword(accessToken, tokenItem.tokenHash);
if (!validCode) {
return {
valid: false,
error: "Invalid access token"
};
}
if ( if (
tokenItem.expiresAt && tokenItem.expiresAt &&
!isWithinExpirationDate(new Date(tokenItem.expiresAt)) !isWithinExpirationDate(new Date(tokenItem.expiresAt))
@@ -60,8 +102,16 @@ export async function verifyResourceAccessToken({
}; };
} }
if (resourceId && resource.resourceId !== resourceId) {
return {
valid: false,
error: "Resource ID does not match"
};
}
return { return {
valid: true, valid: true,
tokenItem tokenItem,
resource
}; };
} }

View File

@@ -1,6 +1,6 @@
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/schemas";
import path from "path"; import path from "path";
import fs from "fs/promises"; import fs from "fs/promises";
import { APP_PATH } from "@server/lib/consts"; import { APP_PATH } from "@server/lib/consts";

View File

@@ -1,7 +1,7 @@
import { join } from "path"; import { join } from "path";
import { readFileSync } from "fs"; import { readFileSync } from "fs";
import { db } from "@server/db"; import { db } from "@server/db";
import { exitNodes, sites } from "./schema"; import { exitNodes, sites } from "./schemas/schema";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { __DIRNAME } from "@server/lib/consts"; import { __DIRNAME } from "@server/lib/consts";

View File

@@ -0,0 +1 @@
export * from "./schema";

View File

@@ -76,7 +76,8 @@ export const resources = sqliteTable("resources", {
isBaseDomain: integer("isBaseDomain", { mode: "boolean" }), isBaseDomain: integer("isBaseDomain", { mode: "boolean" }),
applyRules: integer("applyRules", { mode: "boolean" }) applyRules: integer("applyRules", { mode: "boolean" })
.notNull() .notNull()
.default(false) .default(false),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true)
}); });
export const targets = sqliteTable("targets", { export const targets = sqliteTable("targets", {
@@ -405,6 +406,15 @@ export const resourceRules = sqliteTable("resourceRules", {
value: text("value").notNull() value: text("value").notNull()
}); });
export const supporterKey = sqliteTable("supporterKey", {
keyId: integer("keyId").primaryKey({ autoIncrement: true }),
key: text("key").notNull(),
githubUsername: text("githubUsername").notNull(),
phrase: text("phrase"),
tier: text("tier"),
valid: integer("valid", { mode: "boolean" }).notNull().default(false)
});
export type Org = InferSelectModel<typeof orgs>; export type Org = InferSelectModel<typeof orgs>;
export type User = InferSelectModel<typeof users>; export type User = InferSelectModel<typeof users>;
export type Site = InferSelectModel<typeof sites>; export type Site = InferSelectModel<typeof sites>;
@@ -439,3 +449,4 @@ export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
export type VersionMigration = InferSelectModel<typeof versionMigrations>; export type VersionMigration = InferSelectModel<typeof versionMigrations>;
export type ResourceRule = InferSelectModel<typeof resourceRules>; export type ResourceRule = InferSelectModel<typeof resourceRules>;
export type Domain = InferSelectModel<typeof domains>; export type Domain = InferSelectModel<typeof domains>;
export type SupporterKey = InferSelectModel<typeof supporterKey>;

View File

@@ -2,7 +2,7 @@ import { runSetupFunctions } from "./setup";
import { createApiServer } from "./apiServer"; import { createApiServer } from "./apiServer";
import { createNextServer } from "./nextServer"; import { createNextServer } from "./nextServer";
import { createInternalServer } from "./internalServer"; import { createInternalServer } from "./internalServer";
import { Session, User, UserOrg } from "./db/schema"; import { Session, User, UserOrg } from "./db/schemas/schema";
async function startServers() { async function startServers() {
await runSetupFunctions(); await runSetupFunctions();

View File

@@ -1,6 +1,6 @@
import db from "@server/db"; import db from "@server/db";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { roleResources, userResources } from "@server/db/schema"; import { roleResources, userResources } from "@server/db/schemas";
export async function canUserAccessResource({ export async function canUserAccessResource({
userId, userId,

View File

@@ -10,6 +10,10 @@ import {
} from "@server/lib/consts"; } from "@server/lib/consts";
import { passwordSchema } from "@server/auth/passwordSchema"; import { passwordSchema } from "@server/auth/passwordSchema";
import stoi from "./stoi"; import stoi from "./stoi";
import db from "@server/db";
import { SupporterKey, supporterKey } from "@server/db/schemas";
import { suppressDeprecationWarnings } from "moment";
import { eq } from "drizzle-orm";
const portSchema = z.number().positive().gt(0).lte(65535); const portSchema = z.number().positive().gt(0).lte(65535);
@@ -62,6 +66,10 @@ const configSchema = z.object({
internal_hostname: z.string().transform((url) => url.toLowerCase()), internal_hostname: z.string().transform((url) => url.toLowerCase()),
session_cookie_name: z.string(), session_cookie_name: z.string(),
resource_access_token_param: z.string(), resource_access_token_param: z.string(),
resource_access_token_headers: z.object({
id: z.string(),
token: z.string()
}),
resource_session_request_param: z.string(), resource_session_request_param: z.string(),
dashboard_session_length_hours: z dashboard_session_length_hours: z
.number() .number()
@@ -155,6 +163,12 @@ const configSchema = z.object({
export class Config { export class Config {
private rawConfig!: z.infer<typeof configSchema>; private rawConfig!: z.infer<typeof configSchema>;
supporterData: SupporterKey | null = null;
supporterHiddenUntil: number | null = null;
isDev: boolean = process.env.ENVIRONMENT !== "prod";
constructor() { constructor() {
this.loadConfig(); this.loadConfig();
} }
@@ -183,7 +197,9 @@ export class Config {
} }
if (process.env.APP_BASE_DOMAIN) { if (process.env.APP_BASE_DOMAIN) {
console.log("You're using deprecated environment variables. Transition to the configuration file. https://docs.fossorial.io/"); console.log(
"You're using deprecated environment variables. Transition to the configuration file. https://docs.fossorial.io/"
);
} }
if (!environment) { if (!environment) {
@@ -227,6 +243,10 @@ export class Config {
: "false"; : "false";
process.env.RESOURCE_ACCESS_TOKEN_PARAM = process.env.RESOURCE_ACCESS_TOKEN_PARAM =
parsedConfig.data.server.resource_access_token_param; parsedConfig.data.server.resource_access_token_param;
process.env.RESOURCE_ACCESS_TOKEN_HEADERS_ID =
parsedConfig.data.server.resource_access_token_headers.id;
process.env.RESOURCE_ACCESS_TOKEN_HEADERS_TOKEN =
parsedConfig.data.server.resource_access_token_headers.token;
process.env.RESOURCE_SESSION_REQUEST_PARAM = process.env.RESOURCE_SESSION_REQUEST_PARAM =
parsedConfig.data.server.resource_session_request_param; parsedConfig.data.server.resource_session_request_param;
process.env.FLAGS_ALLOW_BASE_DOMAIN_RESOURCES = parsedConfig.data.flags process.env.FLAGS_ALLOW_BASE_DOMAIN_RESOURCES = parsedConfig.data.flags
@@ -235,6 +255,10 @@ export class Config {
: "false"; : "false";
process.env.DASHBOARD_URL = parsedConfig.data.app.dashboard_url; process.env.DASHBOARD_URL = parsedConfig.data.app.dashboard_url;
if (!this.isDev) {
this.checkSupporterKey();
}
this.rawConfig = parsedConfig.data; this.rawConfig = parsedConfig.data;
} }
@@ -251,6 +275,90 @@ export class Config {
public getDomain(domainId: string) { public getDomain(domainId: string) {
return this.rawConfig.domains[domainId]; return this.rawConfig.domains[domainId];
} }
public hideSupporterKey(days: number = 7) {
const now = new Date().getTime();
if (this.supporterHiddenUntil && now < this.supporterHiddenUntil) {
return;
}
this.supporterHiddenUntil = now + 1000 * 60 * 60 * 24 * days;
}
public isSupporterKeyHidden() {
const now = new Date().getTime();
if (this.supporterHiddenUntil && now < this.supporterHiddenUntil) {
return true;
}
return false;
}
public async checkSupporterKey() {
const [key] = await db.select().from(supporterKey).limit(1);
if (!key) {
return;
}
const { key: licenseKey, githubUsername } = key;
try {
const response = await fetch(
"https://api.dev.fossorial.io/api/v1/license/validate",
{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
licenseKey,
githubUsername
})
}
);
if (!response.ok) {
this.supporterData = key;
return;
}
const data = await response.json();
if (!data.data.valid) {
this.supporterData = {
...key,
valid: false
};
return;
}
this.supporterData = {
...key,
tier: data.data.tier,
valid: true
};
// update the supporter key in the database
await db
.update(supporterKey)
.set({
tier: data.data.tier || null,
phrase: data.data.cutePhrase || null,
valid: true
})
.where(eq(supporterKey.keyId, key.keyId));
} catch (e) {
this.supporterData = key;
console.error("Failed to validate supporter key", e);
}
}
public getSupporterData() {
return this.supporterData;
}
} }
export const config = new Config(); export const config = new Config();

View File

@@ -2,7 +2,7 @@ import path from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
// This is a placeholder value replaced by the build process // This is a placeholder value replaced by the build process
export const APP_VERSION = "1.0.0"; export const APP_VERSION = "1.2.0";
export const __FILENAME = fileURLToPath(import.meta.url); export const __FILENAME = fileURLToPath(import.meta.url);
export const __DIRNAME = path.dirname(__FILENAME); export const __DIRNAME = path.dirname(__FILENAME);

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { userOrgs, orgs } from "@server/db/schema"; import { userOrgs, orgs } from "@server/db/schemas";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View File

@@ -14,3 +14,4 @@ export * from "./verifyAdmin";
export * from "./verifySetResourceUsers"; export * from "./verifySetResourceUsers";
export * from "./verifyUserInRole"; export * from "./verifyUserInRole";
export * from "./verifyAccessTokenAccess"; export * from "./verifyAccessTokenAccess";
export * from "./verifyUserIsServerAdmin";

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { resourceAccessToken, resources, userOrgs } from "@server/db/schema"; import { resourceAccessToken, resources, userOrgs } from "@server/db/schemas";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { roles, userOrgs } from "@server/db/schema"; import { roles, userOrgs } from "@server/db/schemas";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { userOrgs } from "@server/db/schema"; import { userOrgs } from "@server/db/schemas";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View File

@@ -5,7 +5,7 @@ import {
userOrgs, userOrgs,
userResources, userResources,
roleResources, roleResources,
} from "@server/db/schema"; } from "@server/db/schemas";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { roles, userOrgs } from "@server/db/schema"; import { roles, userOrgs } from "@server/db/schemas";
import { and, eq, inArray } from "drizzle-orm"; import { and, eq, inArray } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
@@ -44,6 +44,8 @@ export async function verifyRoleAccess(
); );
} }
const orgIds = new Set(rolesData.map((role) => role.orgId));
// Check user access to each role's organization // Check user access to each role's organization
for (const role of rolesData) { for (const role of rolesData) {
const userOrgRole = await db const userOrgRole = await db
@@ -69,7 +71,16 @@ export async function verifyRoleAccess(
req.userOrgId = role.orgId; req.userOrgId = role.orgId;
} }
const orgId = req.userOrgId; if (orgIds.size > 1) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Roles must belong to the same organization"
)
);
}
const orgId = orgIds.values().next().value;
if (!orgId) { if (!orgId) {
return next( return next(
@@ -105,3 +116,4 @@ export async function verifyRoleAccess(
); );
} }
} }

View File

@@ -1,7 +1,7 @@
import { NextFunction, Response } from "express"; import { NextFunction, Response } from "express";
import ErrorResponse from "@server/types/ErrorResponse"; import ErrorResponse from "@server/types/ErrorResponse";
import { db } from "@server/db"; import { db } from "@server/db";
import { users } from "@server/db/schema"; import { users } from "@server/db/schemas";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { userOrgs } from "@server/db/schema"; import { userOrgs } from "@server/db/schemas";
import { and, eq, inArray, or } from "drizzle-orm"; import { and, eq, inArray, or } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View File

@@ -6,7 +6,7 @@ import {
userSites, userSites,
roleSites, roleSites,
roles, roles,
} from "@server/db/schema"; } from "@server/db/schemas";
import { and, eq, or } from "drizzle-orm"; import { and, eq, or } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { resources, targets, userOrgs } from "@server/db/schema"; import { resources, targets, userOrgs } from "@server/db/schemas";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View File

@@ -1,7 +1,7 @@
import { NextFunction, Response } from "express"; import { NextFunction, Response } from "express";
import ErrorResponse from "@server/types/ErrorResponse"; import ErrorResponse from "@server/types/ErrorResponse";
import { db } from "@server/db"; import { db } from "@server/db";
import { users } from "@server/db/schema"; import { users } from "@server/db/schemas";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { userOrgs } from "@server/db/schema"; import { userOrgs } from "@server/db/schemas";
import { and, eq, or } from "drizzle-orm"; import { and, eq, or } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { db } from "@server/db"; import { db } from "@server/db";
import { userOrgs } from "@server/db/schema"; import { userOrgs } from "@server/db/schemas";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View File

@@ -0,0 +1,37 @@
import { Request, Response, NextFunction } from "express";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
export async function verifyUserIsServerAdmin(
req: Request,
res: Response,
next: NextFunction
) {
const userId = req.user!.userId;
if (!userId) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
);
}
try {
if (!req.user?.serverAdmin) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User is not a server admin"
)
);
}
return next();
} catch (e) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying organization access"
)
);
}
}

View File

@@ -5,7 +5,7 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { resourceAccessToken } from "@server/db/schema"; import { resourceAccessToken } from "@server/db/schemas";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import db from "@server/db"; import db from "@server/db";

View File

@@ -9,7 +9,7 @@ import {
ResourceAccessToken, ResourceAccessToken,
resourceAccessToken, resourceAccessToken,
resources resources
} from "@server/db/schema"; } from "@server/db/schemas";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response"; import response from "@server/lib/response";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
@@ -20,6 +20,8 @@ import { fromError } from "zod-validation-error";
import logger from "@server/logger"; import logger from "@server/logger";
import { createDate, TimeSpan } from "oslo"; import { createDate, TimeSpan } from "oslo";
import { hashPassword } from "@server/auth/password"; import { hashPassword } from "@server/auth/password";
import { encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";
export const generateAccessTokenBodySchema = z export const generateAccessTokenBodySchema = z
.object({ .object({
@@ -90,11 +92,13 @@ export async function generateAccessToken(
? createDate(new TimeSpan(validForSeconds, "s")).getTime() ? createDate(new TimeSpan(validForSeconds, "s")).getTime()
: undefined; : undefined;
const token = generateIdFromEntropySize(25); const token = generateIdFromEntropySize(16);
const tokenHash = await hashPassword(token); const tokenHash = encodeHexLowerCase(
sha256(new TextEncoder().encode(token))
);
const id = generateId(15); const id = generateId(8);
const [result] = await db const [result] = await db
.insert(resourceAccessToken) .insert(resourceAccessToken)
.values({ .values({

View File

@@ -7,13 +7,14 @@ import {
roleResources, roleResources,
resourceAccessToken, resourceAccessToken,
sites sites
} from "@server/db/schema"; } from "@server/db/schemas";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import { sql, eq, or, inArray, and, count, isNull, lt, gt } from "drizzle-orm"; import { sql, eq, or, inArray, and, count, isNull, lt, gt } from "drizzle-orm";
import logger from "@server/logger"; import logger from "@server/logger";
import stoi from "@server/lib/stoi"; import stoi from "@server/lib/stoi";
import { fromZodError } from "zod-validation-error";
const listAccessTokensParamsSchema = z const listAccessTokensParamsSchema = z
.object({ .object({
@@ -133,7 +134,7 @@ export async function listAccessTokens(
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedQuery.error.errors.map((e) => e.message).join(", ") fromZodError(parsedQuery.error)
) )
); );
} }
@@ -144,7 +145,7 @@ export async function listAccessTokens(
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map((e) => e.message).join(", ") fromZodError(parsedParams.error)
) )
); );
} }

View File

@@ -4,7 +4,7 @@ import HttpCode from "@server/types/HttpCode";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { User, users } from "@server/db/schema"; import { User, users } from "@server/db/schemas";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { response } from "@server/lib"; import { response } from "@server/lib";
import { import {

View File

@@ -4,7 +4,7 @@ import HttpCode from "@server/types/HttpCode";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { User, users } from "@server/db/schema"; import { User, users } from "@server/db/schemas";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { response } from "@server/lib"; import { response } from "@server/lib";
import { verifyPassword } from "@server/auth/password"; import { verifyPassword } from "@server/auth/password";

View File

@@ -4,7 +4,7 @@ import {
serializeSessionCookie serializeSessionCookie
} from "@server/auth/sessions/app"; } from "@server/auth/sessions/app";
import db from "@server/db"; import db from "@server/db";
import { users } from "@server/db/schema"; import { users } from "@server/db/schemas";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response"; import response from "@server/lib/response";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";

View File

@@ -2,7 +2,7 @@ import { Request, Response, NextFunction } from "express";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { response } from "@server/lib"; import { response } from "@server/lib";
import { User } from "@server/db/schema"; import { User } from "@server/db/schemas";
import { sendEmailVerificationCode } from "../../auth/sendEmailVerificationCode"; import { sendEmailVerificationCode } from "../../auth/sendEmailVerificationCode";
import config from "@server/lib/config"; import config from "@server/lib/config";
import logger from "@server/logger"; import logger from "@server/logger";

View File

@@ -5,7 +5,7 @@ import { fromError } from "zod-validation-error";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { response } from "@server/lib"; import { response } from "@server/lib";
import { db } from "@server/db"; import { db } from "@server/db";
import { passwordResetTokens, users } from "@server/db/schema"; import { passwordResetTokens, users } from "@server/db/schemas";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { alphabet, generateRandomString, sha256 } from "oslo/crypto"; import { alphabet, generateRandomString, sha256 } from "oslo/crypto";
import { createDate } from "oslo"; import { createDate } from "oslo";

View File

@@ -6,7 +6,7 @@ import { encodeHex } from "oslo/encoding";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { response } from "@server/lib"; import { response } from "@server/lib";
import { db } from "@server/db"; import { db } from "@server/db";
import { User, users } from "@server/db/schema"; import { User, users } from "@server/db/schemas";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { createTOTPKeyURI } from "oslo/otp"; import { createTOTPKeyURI } from "oslo/otp";
import logger from "@server/logger"; import logger from "@server/logger";

View File

@@ -6,7 +6,7 @@ import { fromError } from "zod-validation-error";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { response } from "@server/lib"; import { response } from "@server/lib";
import { db } from "@server/db"; import { db } from "@server/db";
import { passwordResetTokens, users } from "@server/db/schema"; import { passwordResetTokens, users } from "@server/db/schemas";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { hashPassword, verifyPassword } from "@server/auth/password"; import { hashPassword, verifyPassword } from "@server/auth/password";
import { verifyTotpCode } from "@server/auth/totp"; import { verifyTotpCode } from "@server/auth/totp";

View File

@@ -2,7 +2,7 @@ import { NextFunction, Request, Response } from "express";
import db from "@server/db"; import db from "@server/db";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { z } from "zod"; import { z } from "zod";
import { users } from "@server/db/schema"; import { users } from "@server/db/schemas";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import response from "@server/lib/response"; import response from "@server/lib/response";

View File

@@ -5,7 +5,7 @@ import { fromError } from "zod-validation-error";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { response } from "@server/lib"; import { response } from "@server/lib";
import { db } from "@server/db"; import { db } from "@server/db";
import { User, emailVerificationCodes, users } from "@server/db/schema"; import { User, emailVerificationCodes, users } from "@server/db/schemas";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { isWithinExpirationDate } from "oslo"; import { isWithinExpirationDate } from "oslo";
import config from "@server/lib/config"; import config from "@server/lib/config";

View File

@@ -5,7 +5,7 @@ import { fromError } from "zod-validation-error";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { response } from "@server/lib"; import { response } from "@server/lib";
import { db } from "@server/db"; import { db } from "@server/db";
import { twoFactorBackupCodes, User, users } from "@server/db/schema"; import { twoFactorBackupCodes, User, users } from "@server/db/schemas";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { alphabet, generateRandomString } from "oslo/crypto"; import { alphabet, generateRandomString } from "oslo/crypto";
import { hashPassword } from "@server/auth/password"; import { hashPassword } from "@server/auth/password";

View File

@@ -4,7 +4,7 @@ import createHttpError from "http-errors";
import { z } from "zod"; import { z } from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import logger from "@server/logger"; import logger from "@server/logger";
import { resourceAccessToken, resources, sessions } from "@server/db/schema"; import { resourceAccessToken, resources, sessions } from "@server/db/schemas";
import db from "@server/db"; import db from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { import {

View File

@@ -21,7 +21,7 @@ import {
userOrgs, userOrgs,
userResources, userResources,
users users
} from "@server/db/schema"; } from "@server/db/schemas";
import config from "@server/lib/config"; import config from "@server/lib/config";
import { isIpInCidr } from "@server/lib/ip"; import { isIpInCidr } from "@server/lib/ip";
import { response } from "@server/lib/response"; import { response } from "@server/lib/response";
@@ -41,12 +41,13 @@ const cache = new NodeCache({
const verifyResourceSessionSchema = z.object({ const verifyResourceSessionSchema = z.object({
sessions: z.record(z.string()).optional(), sessions: z.record(z.string()).optional(),
headers: z.record(z.string()).optional(),
query: z.record(z.string()).optional(),
originalRequestURL: z.string().url(), originalRequestURL: z.string().url(),
scheme: z.string(), scheme: z.string(),
host: z.string(), host: z.string(),
path: z.string(), path: z.string(),
method: z.string(), method: z.string(),
accessToken: z.string().optional(),
tls: z.boolean(), tls: z.boolean(),
requestIp: z.string().optional() requestIp: z.string().optional()
}); });
@@ -85,7 +86,8 @@ export async function verifyResourceSession(
originalRequestURL, originalRequestURL,
requestIp, requestIp,
path, path,
accessToken: token headers,
query
} = parsedBody.data; } = parsedBody.data;
const clientIp = requestIp?.split(":")[0]; const clientIp = requestIp?.split(":")[0];
@@ -183,12 +185,33 @@ export async function verifyResourceSession(
resource.resourceId resource.resourceId
)}?redirect=${encodeURIComponent(originalRequestURL)}`; )}?redirect=${encodeURIComponent(originalRequestURL)}`;
// check for access token // check for access token in headers
let validAccessToken: ResourceAccessToken | undefined; if (
if (token) { headers &&
const [accessTokenId, accessToken] = token.split("."); headers[
config.getRawConfig().server.resource_access_token_headers.id
] &&
headers[
config.getRawConfig().server.resource_access_token_headers.token
]
) {
const accessTokenId =
headers[
config.getRawConfig().server.resource_access_token_headers
.id
];
const accessToken =
headers[
config.getRawConfig().server.resource_access_token_headers
.token
];
const { valid, error, tokenItem } = await verifyResourceAccessToken( const { valid, error, tokenItem } = await verifyResourceAccessToken(
{ resource, accessTokenId, accessToken } {
accessToken,
accessTokenId,
resourceId: resource.resourceId
}
); );
if (error) { if (error) {
@@ -206,16 +229,44 @@ export async function verifyResourceSession(
} }
if (valid && tokenItem) { if (valid && tokenItem) {
validAccessToken = tokenItem; return allowed(res);
}
}
if (!sessions) { if (
return await createAccessTokenSession( query &&
res, query[config.getRawConfig().server.resource_access_token_param]
resource, ) {
tokenItem const token =
query[config.getRawConfig().server.resource_access_token_param];
const [accessTokenId, accessToken] = token.split(".");
const { valid, error, tokenItem } = await verifyResourceAccessToken(
{
accessToken,
accessTokenId,
resourceId: resource.resourceId
}
);
if (error) {
logger.debug("Access token invalid: " + error);
}
if (!valid) {
if (config.getRawConfig().app.log_failed_attempts) {
logger.info(
`Resource access token is invalid. Resource ID: ${
resource.resourceId
}. IP: ${clientIp}.`
); );
} }
} }
if (valid && tokenItem) {
return allowed(res);
}
} }
if (!sessions) { if (!sessions) {
@@ -229,12 +280,10 @@ export async function verifyResourceSession(
return notAllowed(res); return notAllowed(res);
} }
const resourceSessionToken = const resourceSessionToken = extractResourceSessionToken(
sessions[ sessions,
`${config.getRawConfig().server.session_cookie_name}${ resource.ssl
resource.ssl ? "_s" : "" );
}`
];
if (resourceSessionToken) { if (resourceSessionToken) {
const sessionCacheKey = `session:${resourceSessionToken}`; const sessionCacheKey = `session:${resourceSessionToken}`;
@@ -323,16 +372,6 @@ export async function verifyResourceSession(
} }
} }
// At this point we have checked all sessions, but since the access token is
// valid, we should allow access and create a new session.
if (validAccessToken) {
return await createAccessTokenSession(
res,
resource,
validAccessToken
);
}
logger.debug("No more auth to check, resource not allowed"); logger.debug("No more auth to check, resource not allowed");
if (config.getRawConfig().app.log_failed_attempts) { if (config.getRawConfig().app.log_failed_attempts) {
@@ -354,6 +393,49 @@ export async function verifyResourceSession(
} }
} }
function extractResourceSessionToken(
sessions: Record<string, string>,
ssl: boolean
) {
const prefix = `${config.getRawConfig().server.session_cookie_name}${
ssl ? "_s" : ""
}`;
const all: { cookieName: string; token: string; priority: number }[] = [];
for (const [key, value] of Object.entries(sessions)) {
const parts = key.split(".");
const timestamp = parts[parts.length - 1];
// check if string is only numbers
if (!/^\d+$/.test(timestamp)) {
continue;
}
// cookie name is the key without the timestamp
const cookieName = key.slice(0, -timestamp.length - 1);
if (cookieName === prefix) {
all.push({
cookieName,
token: value,
priority: parseInt(timestamp)
});
}
}
// sort by priority in desc order
all.sort((a, b) => b.priority - a.priority);
const latest = all[0];
if (!latest) {
return;
}
return latest.token;
}
function notAllowed(res: Response, redirectUrl?: string) { function notAllowed(res: Response, redirectUrl?: string) {
const data = { const data = {
data: { valid: false, redirectUrl }, data: { valid: false, redirectUrl },

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { domains, orgDomains, users } from "@server/db/schema"; import { domains, orgDomains, users } from "@server/db/schemas";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
@@ -80,15 +80,15 @@ export async function listDomains(
const { orgId } = parsedParams.data; const { orgId } = parsedParams.data;
const domains = await queryDomains(orgId.toString(), limit, offset); const domainsList = await queryDomains(orgId.toString(), limit, offset);
const [{ count }] = await db const [{ count }] = await db
.select({ count: sql<number>`count(*)` }) .select({ count: sql<number>`count(*)` })
.from(users); .from(domains);
return response<ListDomainsResponse>(res, { return response<ListDomainsResponse>(res, {
data: { data: {
domains, domains: domainsList,
pagination: { pagination: {
total: count, total: count,
limit, limit,

View File

@@ -8,6 +8,7 @@ import * as target from "./target";
import * as user from "./user"; import * as user from "./user";
import * as auth from "./auth"; import * as auth from "./auth";
import * as role from "./role"; import * as role from "./role";
import * as supporterKey from "./supporterKey";
import * as accessToken from "./accessToken"; import * as accessToken from "./accessToken";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { import {
@@ -22,7 +23,8 @@ import {
verifyRoleAccess, verifyRoleAccess,
verifySetResourceUsers, verifySetResourceUsers,
verifyUserAccess, verifyUserAccess,
getUserOrgs getUserOrgs,
verifyUserIsServerAdmin
} from "@server/middlewares"; } from "@server/middlewares";
import { verifyUserHasAction } from "../middlewares/verifyUserHasAction"; import { verifyUserHasAction } from "../middlewares/verifyUserHasAction";
import { ActionsEnum } from "@server/auth/actions"; import { ActionsEnum } from "@server/auth/actions";
@@ -239,7 +241,6 @@ authenticated.delete(
target.deleteTarget target.deleteTarget
); );
authenticated.put( authenticated.put(
"/org/:orgId/role", "/org/:orgId/role",
verifyOrgAccess, verifyOrgAccess,
@@ -382,6 +383,12 @@ authenticated.get(
authenticated.get(`/org/:orgId/overview`, verifyOrgAccess, org.getOrgOverview); authenticated.get(`/org/:orgId/overview`, verifyOrgAccess, org.getOrgOverview);
authenticated.post(
`/supporter-key/validate`,
supporterKey.validateSupporterKey
);
authenticated.post(`/supporter-key/hide`, supporterKey.hideSupporterKey);
unauthenticated.get("/resource/:resourceId/auth", resource.getResourceAuthInfo); unauthenticated.get("/resource/:resourceId/auth", resource.getResourceAuthInfo);
// authenticated.get( // authenticated.get(
@@ -415,6 +422,13 @@ unauthenticated.get("/resource/:resourceId/auth", resource.getResourceAuthInfo);
unauthenticated.get("/user", verifySessionMiddleware, user.getUser); unauthenticated.get("/user", verifySessionMiddleware, user.getUser);
authenticated.get("/users", verifyUserIsServerAdmin, user.adminListUsers);
authenticated.delete(
"/user/:userId",
verifyUserIsServerAdmin,
user.adminRemoveUser
);
authenticated.get("/org/:orgId/user/:userId", verifyOrgAccess, user.getOrgUser); authenticated.get("/org/:orgId/user/:userId", verifyOrgAccess, user.getOrgUser);
authenticated.get( authenticated.get(
"/org/:orgId/users", "/org/:orgId/users",
@@ -459,7 +473,11 @@ authenticated.delete(
// role.removeRoleAction // role.removeRoleAction
// ); // );
authenticated.put("/newt", createNewt); // authenticated.put(
// "/newt",
// verifyUserHasAction(ActionsEnum.createNewt),
// createNewt
// );
// Auth routes // Auth routes
export const authRouter = Router(); export const authRouter = Router();
@@ -548,3 +566,8 @@ authRouter.post(
"/resource/:resourceId/access-token", "/resource/:resourceId/access-token",
resource.authWithAccessToken resource.authWithAccessToken
); );
authRouter.post(
"/access-token",
resource.authWithAccessToken
);

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { z } from 'zod'; import { z } from 'zod';
import { sites, resources, targets, exitNodes } from '@server/db/schema'; import { sites, resources, targets, exitNodes } from '@server/db/schemas';
import { db } from '@server/db'; import { db } from '@server/db';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import response from "@server/lib/response"; import response from "@server/lib/response";

View File

@@ -1,7 +1,7 @@
import axios from 'axios'; import axios from 'axios';
import logger from '@server/logger'; import logger from '@server/logger';
import db from '@server/db'; import db from '@server/db';
import { exitNodes } from '@server/db/schema'; import { exitNodes } from '@server/db/schemas';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
export async function addPeer(exitNodeId: number, peer: { export async function addPeer(exitNodeId: number, peer: {

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { DrizzleError, eq } from "drizzle-orm"; import { DrizzleError, eq } from "drizzle-orm";
import { sites, resources, targets, exitNodes } from "@server/db/schema"; import { sites, resources, targets, exitNodes } from "@server/db/schemas";
import db from "@server/db"; import db from "@server/db";
import logger from "@server/logger"; import logger from "@server/logger";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
@@ -59,8 +59,8 @@ export const receiveBandwidth = async (
await trx await trx
.update(sites) .update(sites)
.set({ .set({
megabytesOut: (site.megabytesIn || 0) + bytesIn, megabytesOut: (site.megabytesOut || 0) + bytesIn,
megabytesIn: (site.megabytesOut || 0) + bytesOut, megabytesIn: (site.megabytesIn || 0) + bytesOut,
lastBandwidthUpdate: new Date().toISOString(), lastBandwidthUpdate: new Date().toISOString(),
online online
}) })

View File

@@ -4,8 +4,12 @@ import * as traefik from "@server/routers/traefik";
import * as resource from "./resource"; import * as resource from "./resource";
import * as badger from "./badger"; import * as badger from "./badger";
import * as auth from "@server/routers/auth"; import * as auth from "@server/routers/auth";
import * as supporterKey from "@server/routers/supporterKey";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { verifyResourceAccess, verifySessionUserMiddleware } from "@server/middlewares"; import {
verifyResourceAccess,
verifySessionUserMiddleware
} from "@server/middlewares";
// Root routes // Root routes
const internalRouter = Router(); const internalRouter = Router();
@@ -28,6 +32,11 @@ internalRouter.post(
resource.getExchangeToken resource.getExchangeToken
); );
internalRouter.get(
`/supporter-key/visible`,
supporterKey.isSupporterKeyVisible
);
// Gerbil routes // Gerbil routes
const gerbilRouter = Router(); const gerbilRouter = Router();
internalRouter.use("/gerbil", gerbilRouter); internalRouter.use("/gerbil", gerbilRouter);

View File

@@ -3,7 +3,7 @@ import db from "@server/db";
import { hash } from "@node-rs/argon2"; import { hash } from "@node-rs/argon2";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { z } from "zod"; import { z } from "zod";
import { newts } from "@server/db/schema"; import { newts } from "@server/db/schemas";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import response from "@server/lib/response"; import response from "@server/lib/response";
import { SqliteError } from "better-sqlite3"; import { SqliteError } from "better-sqlite3";

View File

@@ -1,6 +1,6 @@
import { generateSessionToken } from "@server/auth/sessions/app"; import { generateSessionToken } from "@server/auth/sessions/app";
import db from "@server/db"; import db from "@server/db";
import { newts } from "@server/db/schema"; import { newts } from "@server/db/schemas";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response"; import response from "@server/lib/response";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";

View File

@@ -6,7 +6,7 @@ import {
sites, sites,
Target, Target,
targets targets
} from "@server/db/schema"; } from "@server/db/schemas";
import { eq, and, sql, inArray } from "drizzle-orm"; import { eq, and, sql, inArray } from "drizzle-orm";
import { addPeer, deletePeer } from "../gerbil/peers"; import { addPeer, deletePeer } from "../gerbil/peers";
import logger from "@server/logger"; import logger from "@server/logger";

View File

@@ -1,4 +1,4 @@
import { Target } from "@server/db/schema"; import { Target } from "@server/db/schemas";
import { sendToClient } from "../ws"; import { sendToClient } from "../ws";
export function addTargets( export function addTargets(

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { orgs } from "@server/db/schema"; import { orgs } from "@server/db/schemas";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View File

@@ -10,7 +10,7 @@ import {
roleActions, roleActions,
roles, roles,
userOrgs userOrgs
} from "@server/db/schema"; } from "@server/db/schemas";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
@@ -27,7 +27,7 @@ const createOrgSchema = z
}) })
.strict(); .strict();
const MAX_ORGS = 5; // const MAX_ORGS = 5;
export async function createOrg( export async function createOrg(
req: Request, req: Request,
@@ -57,15 +57,15 @@ export async function createOrg(
); );
} }
const userOrgIds = req.userOrgIds; // const userOrgIds = req.userOrgIds;
if (userOrgIds && userOrgIds.length > MAX_ORGS) { // if (userOrgIds && userOrgIds.length > MAX_ORGS) {
return next( // return next(
createHttpError( // createHttpError(
HttpCode.FORBIDDEN, // HttpCode.FORBIDDEN,
`Maximum number of organizations reached.` // `Maximum number of organizations reached.`
) // )
); // );
} // }
const { orgId, name } = parsedBody.data; const { orgId, name } = parsedBody.data;

View File

@@ -7,7 +7,7 @@ import {
orgs, orgs,
sites, sites,
userActions userActions
} from "@server/db/schema"; } from "@server/db/schemas";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View File

@@ -1,12 +1,13 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { Org, orgs } from "@server/db/schema"; import { Org, orgs } from "@server/db/schemas";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromZodError } from "zod-validation-error";
const getOrgSchema = z const getOrgSchema = z
.object({ .object({
@@ -29,7 +30,7 @@ export async function getOrg(
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map((e) => e.message).join(", ") fromZodError(parsedParams.error)
) )
); );
} }

View File

@@ -10,12 +10,13 @@ import {
userResources, userResources,
users, users,
userSites userSites
} from "@server/db/schema"; } from "@server/db/schemas";
import { and, count, eq, inArray } from "drizzle-orm"; import { and, count, eq, inArray } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromZodError } from "zod-validation-error";
const getOrgParamsSchema = z const getOrgParamsSchema = z
.object({ .object({
@@ -45,7 +46,7 @@ export async function getOrgOverview(
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map((e) => e.message).join(", ") fromZodError(parsedParams.error)
) )
); );
} }

View File

@@ -1,12 +1,13 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { Org, orgs } from "@server/db/schema"; import { Org, orgs } from "@server/db/schemas";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import { sql, inArray } from "drizzle-orm"; import { sql, inArray } from "drizzle-orm";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromZodError } from "zod-validation-error";
const listOrgsSchema = z.object({ const listOrgsSchema = z.object({
limit: z limit: z
@@ -39,7 +40,7 @@ export async function listOrgs(
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedQuery.error.errors.map((e) => e.message).join(", ") fromZodError(parsedQuery.error)
) )
); );
} }

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { orgs } from "@server/db/schema"; import { orgs } from "@server/db/schemas";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View File

@@ -1,6 +1,6 @@
import { generateSessionToken } from "@server/auth/sessions/app"; import { generateSessionToken } from "@server/auth/sessions/app";
import db from "@server/db"; import db from "@server/db";
import { resources } from "@server/db/schema"; import { Resource, resources } from "@server/db/schemas";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response"; import response from "@server/lib/response";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
@@ -10,13 +10,16 @@ import { z } from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { createResourceSession } from "@server/auth/sessions/resource"; import { createResourceSession } from "@server/auth/sessions/resource";
import logger from "@server/logger"; import logger from "@server/logger";
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken"; import {
verifyResourceAccessToken
} from "@server/auth/verifyResourceAccessToken";
import config from "@server/lib/config"; import config from "@server/lib/config";
import stoi from "@server/lib/stoi";
const authWithAccessTokenBodySchema = z const authWithAccessTokenBodySchema = z
.object({ .object({
accessToken: z.string(), accessToken: z.string(),
accessTokenId: z.string() accessTokenId: z.string().optional()
}) })
.strict(); .strict();
@@ -24,13 +27,15 @@ const authWithAccessTokenParamsSchema = z
.object({ .object({
resourceId: z resourceId: z
.string() .string()
.transform(Number) .optional()
.pipe(z.number().int().positive()) .transform(stoi)
.pipe(z.number().int().positive().optional())
}) })
.strict(); .strict();
export type AuthWithAccessTokenResponse = { export type AuthWithAccessTokenResponse = {
session?: string; session?: string;
redirectUrl?: string | null;
}; };
export async function authWithAccessToken( export async function authWithAccessToken(
@@ -64,23 +69,61 @@ export async function authWithAccessToken(
const { accessToken, accessTokenId } = parsedBody.data; const { accessToken, accessTokenId } = parsedBody.data;
try { try {
const [resource] = await db let valid;
.select() let tokenItem;
.from(resources) let error;
.where(eq(resources.resourceId, resourceId)) let resource: Resource | undefined;
.limit(1);
if (!resource) { if (accessTokenId) {
return next( if (!resourceId) {
createHttpError(HttpCode.NOT_FOUND, "Resource not found") return next(
); createHttpError(
HttpCode.BAD_REQUEST,
"Resource ID is required"
)
);
}
const [foundResource] = await db
.select()
.from(resources)
.where(eq(resources.resourceId, resourceId))
.limit(1);
if (!foundResource) {
return next(
createHttpError(HttpCode.NOT_FOUND, "Resource not found")
);
}
const res = await verifyResourceAccessToken({
accessTokenId,
accessToken
});
valid = res.valid;
tokenItem = res.tokenItem;
error = res.error;
resource = foundResource;
} else {
const res = await verifyResourceAccessToken({
accessToken
});
valid = res.valid;
tokenItem = res.tokenItem;
error = res.error;
resource = res.resource;
} }
const { valid, error, tokenItem } = await verifyResourceAccessToken({ if (!tokenItem || !resource) {
resource, return next(
accessTokenId, createHttpError(
accessToken HttpCode.UNAUTHORIZED,
}); "Access token does not exist for resource"
)
);
}
if (!valid) { if (!valid) {
if (config.getRawConfig().app.log_failed_attempts) { if (config.getRawConfig().app.log_failed_attempts) {
@@ -96,18 +139,9 @@ export async function authWithAccessToken(
); );
} }
if (!tokenItem || !resource) {
return next(
createHttpError(
HttpCode.UNAUTHORIZED,
"Access token does not exist for resource"
)
);
}
const token = generateSessionToken(); const token = generateSessionToken();
await createResourceSession({ await createResourceSession({
resourceId, resourceId: resource.resourceId,
token, token,
accessTokenId: tokenItem.accessTokenId, accessTokenId: tokenItem.accessTokenId,
isRequestToken: true, isRequestToken: true,
@@ -118,7 +152,8 @@ export async function authWithAccessToken(
return response<AuthWithAccessTokenResponse>(res, { return response<AuthWithAccessTokenResponse>(res, {
data: { data: {
session: token session: token,
redirectUrl: `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`
}, },
success: true, success: true,
error: false, error: false,

View File

@@ -1,7 +1,7 @@
import { verify } from "@node-rs/argon2"; import { verify } from "@node-rs/argon2";
import { generateSessionToken } from "@server/auth/sessions/app"; import { generateSessionToken } from "@server/auth/sessions/app";
import db from "@server/db"; import db from "@server/db";
import { orgs, resourcePassword, resources } from "@server/db/schema"; import { orgs, resourcePassword, resources } from "@server/db/schemas";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response"; import response from "@server/lib/response";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";

View File

@@ -1,6 +1,6 @@
import { generateSessionToken } from "@server/auth/sessions/app"; import { generateSessionToken } from "@server/auth/sessions/app";
import db from "@server/db"; import db from "@server/db";
import { orgs, resourcePincode, resources } from "@server/db/schema"; import { orgs, resourcePincode, resources } from "@server/db/schemas";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response"; import response from "@server/lib/response";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";

View File

@@ -5,7 +5,7 @@ import {
resourceOtp, resourceOtp,
resources, resources,
resourceWhitelist resourceWhitelist
} from "@server/db/schema"; } from "@server/db/schemas";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response"; import response from "@server/lib/response";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";

View File

@@ -10,7 +10,7 @@ import {
roleResources, roleResources,
roles, roles,
userResources userResources
} from "@server/db/schema"; } from "@server/db/schemas";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { resourceRules, resources } from "@server/db/schema"; import { resourceRules, resources } from "@server/db/schemas";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { newts, resources, sites, targets } from "@server/db/schema"; import { newts, resources, sites, targets } from "@server/db/schemas";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { resourceRules, resources } from "@server/db/schema"; import { resourceRules, resources } from "@server/db/schemas";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { resources } from "@server/db/schema"; import { resources } from "@server/db/schemas";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { createResourceSession } from "@server/auth/sessions/resource"; import { createResourceSession } from "@server/auth/sessions/resource";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { Resource, resources, sites } from "@server/db/schema"; import { Resource, resources, sites } from "@server/db/schemas";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View File

@@ -5,7 +5,7 @@ import {
resourcePassword, resourcePassword,
resourcePincode, resourcePincode,
resources resources
} from "@server/db/schema"; } from "@server/db/schemas";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { resourceWhitelist, users } from "@server/db/schema"; // Assuming these are the correct tables import { resourceWhitelist, users } from "@server/db/schemas"; // Assuming these are the correct tables
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { roleResources, roles } from "@server/db/schema"; import { roleResources, roles } from "@server/db/schemas";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View File

@@ -1,5 +1,5 @@
import { db } from "@server/db"; import { db } from "@server/db";
import { resourceRules, resources } from "@server/db/schema"; import { resourceRules, resources } from "@server/db/schemas";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response"; import response from "@server/lib/response";
import { eq, sql } from "drizzle-orm"; import { eq, sql } from "drizzle-orm";

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { userResources, users } from "@server/db/schema"; // Assuming these are the correct tables import { userResources, users } from "@server/db/schemas"; // Assuming these are the correct tables
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";

View File

@@ -8,13 +8,14 @@ import {
roleResources, roleResources,
resourcePassword, resourcePassword,
resourcePincode resourcePincode
} from "@server/db/schema"; } from "@server/db/schemas";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import { sql, eq, or, inArray, and, count } from "drizzle-orm"; import { sql, eq, or, inArray, and, count } from "drizzle-orm";
import logger from "@server/logger"; import logger from "@server/logger";
import stoi from "@server/lib/stoi"; import stoi from "@server/lib/stoi";
import { fromZodError } from "zod-validation-error";
const listResourcesParamsSchema = z const listResourcesParamsSchema = z
.object({ .object({
@@ -66,7 +67,8 @@ function queryResources(
whitelist: resources.emailWhitelistEnabled, whitelist: resources.emailWhitelistEnabled,
http: resources.http, http: resources.http,
protocol: resources.protocol, protocol: resources.protocol,
proxyPort: resources.proxyPort proxyPort: resources.proxyPort,
enabled: resources.enabled
}) })
.from(resources) .from(resources)
.leftJoin(sites, eq(resources.siteId, sites.siteId)) .leftJoin(sites, eq(resources.siteId, sites.siteId))
@@ -99,7 +101,8 @@ function queryResources(
whitelist: resources.emailWhitelistEnabled, whitelist: resources.emailWhitelistEnabled,
http: resources.http, http: resources.http,
protocol: resources.protocol, protocol: resources.protocol,
proxyPort: resources.proxyPort proxyPort: resources.proxyPort,
enabled: resources.enabled
}) })
.from(resources) .from(resources)
.leftJoin(sites, eq(resources.siteId, sites.siteId)) .leftJoin(sites, eq(resources.siteId, sites.siteId))
@@ -136,7 +139,7 @@ export async function listResources(
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedQuery.error.errors.map((e) => e.message).join(", ") fromZodError(parsedQuery.error)
) )
); );
} }
@@ -147,7 +150,7 @@ export async function listResources(
return next( return next(
createHttpError( createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
parsedParams.error.errors.map((e) => e.message).join(", ") fromZodError(parsedParams.error)
) )
); );
} }

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { resourcePassword } from "@server/db/schema"; import { resourcePassword } from "@server/db/schemas";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { resourcePincode } from "@server/db/schema"; import { resourcePincode } from "@server/db/schemas";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { roleResources, roles } from "@server/db/schema"; import { roleResources, roles } from "@server/db/schemas";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { userResources } from "@server/db/schema"; import { userResources } from "@server/db/schemas";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { resources, resourceWhitelist } from "@server/db/schema"; import { resources, resourceWhitelist } from "@server/db/schemas";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";

Some files were not shown because too many files have changed in this diff Show More