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 . .
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
@@ -16,7 +16,7 @@ FROM node:20-alpine AS runner
WORKDIR /app
# Curl used for the health checks
RUN apk add --no-cache curl
RUN apk add --no-cache curl
COPY package.json package-lock.json ./
RUN npm ci --only=production && npm cache clean --force

View File

@@ -18,12 +18,12 @@ _Your own self-hosted zero trust tunnel._
<div align="center">
<h5>
<a href="https://docs.fossorial.io/Getting%20Started/quick-install">
Install Guide
<a href="https://fossorial.io">
Website
</a>
<span> | </span>
<a href="https://docs.fossorial.io">
Full Documentation
<a href="https://docs.fossorial.io/Getting%20Started/quick-install">
Install Guide
</a>
<span> | </span>
<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
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

View File

@@ -12,7 +12,7 @@ post {
body:json {
{
"email": "owen@fossorial.io",
"email": "admin@fosrl.io",
"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"
session_cookie_name: "p_session_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"
traefik:
@@ -35,7 +38,7 @@ gerbil:
rate_limits:
global:
window_minutes: 1
max_requests: 100
max_requests: 500
users:
server_admin:

View File

@@ -4,10 +4,10 @@ import path from "path";
export default defineConfig({
dialect: "sqlite",
schema: path.join("server", "db", "schema.ts"),
schema: path.join("server", "db", "schemas"),
out: path.join("server", "migrations"),
verbose: true,
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"
session_cookie_name: "p_session_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"
cors:
origins: ["https://{{.DashboardDomain}}"]
@@ -41,7 +44,7 @@ gerbil:
rate_limits:
global:
window_minutes: 1
max_requests: 100
max_requests: 500
{{if .EnableEmail}}
email:
smtp_host: "{{.EmailSMTPHost}}"

View File

@@ -2,19 +2,19 @@ package main
import (
"bufio"
"bytes"
"embed"
"fmt"
"io"
"io/fs"
"os"
"time"
"os/exec"
"path/filepath"
"runtime"
"strings"
"syscall"
"bytes"
"text/template"
"time"
"unicode"
"golang.org/x/term"
@@ -48,8 +48,8 @@ type Config struct {
EmailSMTPPass string
EmailNoReply string
InstallGerbil bool
TraefikBouncerKey string
DoCrowdsecInstall bool
TraefikBouncerKey string
DoCrowdsecInstall bool
}
func main() {
@@ -84,7 +84,7 @@ func main() {
}
fmt.Println("\n=== Starting installation ===")
if isDockerInstalled() {
if readBool(reader, "Would you like to install and start the containers?", true) {
pullAndStartContainers()
@@ -95,33 +95,35 @@ func main() {
}
if !checkIsCrowdsecInstalledInCompose() {
fmt.Println("\n=== Crowdsec Install ===")
fmt.Println("\n=== CrowdSec Install ===")
// 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 == "" {
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
// print the values and check if they are right
fmt.Println("Detected values:")
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)
// print the values and check if they are right
fmt.Println("Detected values:")
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)
if !readBool(reader, "Are these values correct?", true) {
config = collectUserInput(reader)
}
}
config.DoCrowdsecInstall = true
installCrowdsec(config)
}
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 {
if term.IsTerminal(int(syscall.Stdin)) {
fmt.Print(prompt + ": ")
// Read password without echo if we're in a terminal
password, err := term.ReadPassword(int(syscall.Stdin))
fmt.Println() // Add a newline since ReadPassword doesn't add one
if err != nil {
return ""
}
input := strings.TrimSpace(string(password))
if input == "" {
return readPassword(prompt, reader)
}
return input
} else {
if term.IsTerminal(int(syscall.Stdin)) {
fmt.Print(prompt + ": ")
// Read password without echo if we're in a terminal
password, err := term.ReadPassword(int(syscall.Stdin))
fmt.Println() // Add a newline since ReadPassword doesn't add one
if err != nil {
return ""
}
input := strings.TrimSpace(string(password))
if input == "" {
return readPassword(prompt, reader)
}
return input
} else {
// Fallback to reading from stdin if not in a terminal
return readString(reader, prompt, "")
}
}
}
func readBool(reader *bufio.Reader, prompt string, defaultValue bool) bool {
@@ -319,15 +321,15 @@ func createConfigFiles(config Config) error {
if !config.DoCrowdsecInstall && strings.Contains(path, "crowdsec") {
return nil
}
if config.DoCrowdsecInstall && !strings.Contains(path, "crowdsec") {
return nil
}
// skip .DS_Store
if strings.Contains(path, ".DS_Store") {
return nil
}
// skip .DS_Store
if strings.Contains(path, ".DS_Store") {
return nil
}
if d.IsDir() {
// Create directory
@@ -376,7 +378,6 @@ func createConfigFiles(config Config) error {
return nil
}
func installDocker() error {
// Detect Linux distribution
cmd := exec.Command("cat", "/etc/os-release")
@@ -654,29 +655,29 @@ func moveFile(src, dst string) error {
}
func waitForContainer(containerName string) error {
maxAttempts := 30
retryInterval := time.Second * 2
maxAttempts := 30
retryInterval := time.Second * 2
for attempt := 0; attempt < maxAttempts; attempt++ {
// Check if container is running
cmd := exec.Command("docker", "container", "inspect", "-f", "{{.State.Running}}", containerName)
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
// If the container doesn't exist or there's another error, wait and retry
time.Sleep(retryInterval)
continue
}
for attempt := 0; attempt < maxAttempts; attempt++ {
// Check if container is running
cmd := exec.Command("docker", "container", "inspect", "-f", "{{.State.Running}}", containerName)
var out bytes.Buffer
cmd.Stdout = &out
isRunning := strings.TrimSpace(out.String()) == "true"
if isRunning {
return nil
}
if err := cmd.Run(); err != nil {
// If the container doesn't exist or there's another error, wait and retry
time.Sleep(retryInterval)
continue
}
// Container exists but isn't running yet, wait and retry
time.Sleep(retryInterval)
}
isRunning := strings.TrimSpace(out.String()) == "true"
if isRunning {
return nil
}
return fmt.Errorf("container %s did not start within %v seconds", containerName, maxAttempts*int(retryInterval.Seconds()))
}
// Container exists but isn't running yet, wait and retry
time.Sleep(retryInterval)
}
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",
"helmet": "8.0.0",
"http-errors": "2.0.0",
"i": "^0.3.7",
"input-otp": "1.4.1",
"js-yaml": "4.1.0",
"lucide-react": "0.469.0",
@@ -66,12 +67,14 @@
"node-cache": "5.1.2",
"node-fetch": "3.3.2",
"nodemailer": "6.9.16",
"npm": "^11.2.0",
"oslo": "1.2.1",
"qrcode.react": "4.2.0",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-easy-sort": "^1.6.0",
"react-hook-form": "7.54.2",
"react-icons": "^5.5.0",
"rebuild": "0.1.2",
"semver": "7.6.3",
"tailwind-merge": "2.6.0",

View File

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

View File

@@ -1,6 +1,6 @@
import { Request } from "express";
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 createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
@@ -63,6 +63,7 @@ export enum ActionsEnum {
listResourceRules = "listResourceRules",
updateResourceRule = "updateResourceRule",
listOrgDomains = "listOrgDomains",
createNewt = "createNewt",
}
export async function checkUserActionPermission(

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { db } from '@server/db';
import { limitsTable } from '@server/db/schema';
import { limitsTable } from '@server/db/schemas';
import { and, eq } from 'drizzle-orm';
import createHttpError from 'http-errors';
import HttpCode from '@server/types/HttpCode';
@@ -37,4 +37,4 @@ export async function checkOrgLimit({ orgId, limitName, currentValue, increment
}
throw createHttpError(HttpCode.INTERNAL_SERVER_ERROR, 'Unknown error occurred while checking limit');
}
}
}

View File

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

View File

@@ -1,7 +1,7 @@
import { TimeSpan, createDate } from "oslo";
import { generateRandomString, alphabet } from "oslo/crypto";
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 { sendEmail } from "@server/emails";
import config from "@server/lib/config";

View File

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

View File

@@ -2,7 +2,7 @@ import {
encodeHexLowerCase,
} from "@oslojs/encoding";
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 { eq } from "drizzle-orm";

View File

@@ -1,6 +1,6 @@
import { encodeHexLowerCase } from "@oslojs/encoding";
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 { eq, and } from "drizzle-orm";
import config from "@server/lib/config";
@@ -170,16 +170,17 @@ export function serializeResourceSessionCookie(
isHttp: boolean = false,
expiresAt?: Date
): string {
const now = new Date().getTime();
if (!isHttp) {
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 {
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 db from "@server/db";
import { twoFactorBackupCodes } from "@server/db/schema";
import { twoFactorBackupCodes } from "@server/db/schemas";
import { eq } from "drizzle-orm";
import { decodeHex } from "oslo/encoding";
import { TOTPController } from "oslo/otp";

View File

@@ -3,53 +3,95 @@ import {
Resource,
ResourceAccessToken,
resourceAccessToken,
} from "@server/db/schema";
resources
} from "@server/db/schemas";
import { and, eq } from "drizzle-orm";
import { isWithinExpirationDate } from "oslo";
import { verifyPassword } from "./password";
import { encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";
export async function verifyResourceAccessToken({
resource,
accessToken,
accessTokenId,
accessToken
resourceId
}: {
resource: Resource;
accessTokenId: string;
accessToken: string;
accessTokenId?: string;
resourceId?: number; // IF THIS IS NOT SET, THE TOKEN IS VALID FOR ALL RESOURCES
}): Promise<{
valid: boolean;
error?: string;
tokenItem?: ResourceAccessToken;
resource?: Resource;
}> {
const [result] = await db
.select()
.from(resourceAccessToken)
.where(
and(
eq(resourceAccessToken.resourceId, resource.resourceId),
eq(resourceAccessToken.accessTokenId, accessTokenId)
)
)
.limit(1);
const accessTokenHash = encodeHexLowerCase(
sha256(new TextEncoder().encode(accessToken))
);
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 {
valid: false,
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 (
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 {
valid: true,
tokenItem
tokenItem,
resource
};
}

View File

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

View File

@@ -1,7 +1,7 @@
import { join } from "path";
import { readFileSync } from "fs";
import { db } from "@server/db";
import { exitNodes, sites } from "./schema";
import { exitNodes, sites } from "./schemas/schema";
import { eq, and } from "drizzle-orm";
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" }),
applyRules: integer("applyRules", { mode: "boolean" })
.notNull()
.default(false)
.default(false),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true)
});
export const targets = sqliteTable("targets", {
@@ -405,6 +406,15 @@ export const resourceRules = sqliteTable("resourceRules", {
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 User = InferSelectModel<typeof users>;
export type Site = InferSelectModel<typeof sites>;
@@ -439,3 +449,4 @@ export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
export type VersionMigration = InferSelectModel<typeof versionMigrations>;
export type ResourceRule = InferSelectModel<typeof resourceRules>;
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 { createNextServer } from "./nextServer";
import { createInternalServer } from "./internalServer";
import { Session, User, UserOrg } from "./db/schema";
import { Session, User, UserOrg } from "./db/schemas/schema";
async function startServers() {
await runSetupFunctions();

View File

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

View File

@@ -10,6 +10,10 @@ import {
} from "@server/lib/consts";
import { passwordSchema } from "@server/auth/passwordSchema";
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);
@@ -62,6 +66,10 @@ const configSchema = z.object({
internal_hostname: z.string().transform((url) => url.toLowerCase()),
session_cookie_name: 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(),
dashboard_session_length_hours: z
.number()
@@ -155,6 +163,12 @@ const configSchema = z.object({
export class Config {
private rawConfig!: z.infer<typeof configSchema>;
supporterData: SupporterKey | null = null;
supporterHiddenUntil: number | null = null;
isDev: boolean = process.env.ENVIRONMENT !== "prod";
constructor() {
this.loadConfig();
}
@@ -183,7 +197,9 @@ export class Config {
}
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) {
@@ -227,6 +243,10 @@ export class Config {
: "false";
process.env.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 =
parsedConfig.data.server.resource_session_request_param;
process.env.FLAGS_ALLOW_BASE_DOMAIN_RESOURCES = parsedConfig.data.flags
@@ -235,6 +255,10 @@ export class Config {
: "false";
process.env.DASHBOARD_URL = parsedConfig.data.app.dashboard_url;
if (!this.isDev) {
this.checkSupporterKey();
}
this.rawConfig = parsedConfig.data;
}
@@ -251,6 +275,90 @@ export class Config {
public getDomain(domainId: string) {
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();

View File

@@ -2,7 +2,7 @@ import path from "path";
import { fileURLToPath } from "url";
// 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 __DIRNAME = path.dirname(__FILENAME);

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express";
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 createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";

View File

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

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express";
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 createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express";
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 createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express";
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 createHttpError from "http-errors";
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
for (const role of rolesData) {
const userOrgRole = await db
@@ -69,7 +71,16 @@ export async function verifyRoleAccess(
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) {
return next(
@@ -105,3 +116,4 @@ export async function verifyRoleAccess(
);
}
}

View File

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

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express";
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 createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";

View File

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

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express";
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 createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";

View File

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

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express";
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 createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import { userOrgs } from "@server/db/schema";
import { userOrgs } from "@server/db/schemas";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
@@ -55,7 +55,7 @@ export async function verifyUserIsOrgOwner(
)
);
}
return next();
} catch (e) {
return next(

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 logger from "@server/logger";
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 db from "@server/db";

View File

@@ -9,7 +9,7 @@ import {
ResourceAccessToken,
resourceAccessToken,
resources
} from "@server/db/schema";
} from "@server/db/schemas";
import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response";
import { eq } from "drizzle-orm";
@@ -20,6 +20,8 @@ import { fromError } from "zod-validation-error";
import logger from "@server/logger";
import { createDate, TimeSpan } from "oslo";
import { hashPassword } from "@server/auth/password";
import { encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";
export const generateAccessTokenBodySchema = z
.object({
@@ -90,11 +92,13 @@ export async function generateAccessToken(
? createDate(new TimeSpan(validForSeconds, "s")).getTime()
: 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
.insert(resourceAccessToken)
.values({

View File

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

View File

@@ -4,7 +4,7 @@ import HttpCode from "@server/types/HttpCode";
import { fromError } from "zod-validation-error";
import { z } from "zod";
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 { response } from "@server/lib";
import { verifyPassword } from "@server/auth/password";

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import { fromError } from "zod-validation-error";
import HttpCode from "@server/types/HttpCode";
import { response } from "@server/lib";
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 { alphabet, generateRandomString, sha256 } from "oslo/crypto";
import { createDate } from "oslo";

View File

@@ -6,7 +6,7 @@ import { encodeHex } from "oslo/encoding";
import HttpCode from "@server/types/HttpCode";
import { response } from "@server/lib";
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 { createTOTPKeyURI } from "oslo/otp";
import logger from "@server/logger";

View File

@@ -6,7 +6,7 @@ import { fromError } from "zod-validation-error";
import HttpCode from "@server/types/HttpCode";
import { response } from "@server/lib";
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 { hashPassword, verifyPassword } from "@server/auth/password";
import { verifyTotpCode } from "@server/auth/totp";

View File

@@ -2,7 +2,7 @@ import { NextFunction, Request, Response } from "express";
import db from "@server/db";
import HttpCode from "@server/types/HttpCode";
import { z } from "zod";
import { users } from "@server/db/schema";
import { users } from "@server/db/schemas";
import { fromError } from "zod-validation-error";
import createHttpError from "http-errors";
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 { response } from "@server/lib";
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 { isWithinExpirationDate } from "oslo";
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 { response } from "@server/lib";
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 { alphabet, generateRandomString } from "oslo/crypto";
import { hashPassword } from "@server/auth/password";

View File

@@ -4,7 +4,7 @@ import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
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 { eq } from "drizzle-orm";
import {

View File

@@ -21,7 +21,7 @@ import {
userOrgs,
userResources,
users
} from "@server/db/schema";
} from "@server/db/schemas";
import config from "@server/lib/config";
import { isIpInCidr } from "@server/lib/ip";
import { response } from "@server/lib/response";
@@ -41,12 +41,13 @@ const cache = new NodeCache({
const verifyResourceSessionSchema = z.object({
sessions: z.record(z.string()).optional(),
headers: z.record(z.string()).optional(),
query: z.record(z.string()).optional(),
originalRequestURL: z.string().url(),
scheme: z.string(),
host: z.string(),
path: z.string(),
method: z.string(),
accessToken: z.string().optional(),
tls: z.boolean(),
requestIp: z.string().optional()
});
@@ -85,7 +86,8 @@ export async function verifyResourceSession(
originalRequestURL,
requestIp,
path,
accessToken: token
headers,
query
} = parsedBody.data;
const clientIp = requestIp?.split(":")[0];
@@ -183,12 +185,33 @@ export async function verifyResourceSession(
resource.resourceId
)}?redirect=${encodeURIComponent(originalRequestURL)}`;
// check for access token
let validAccessToken: ResourceAccessToken | undefined;
if (token) {
const [accessTokenId, accessToken] = token.split(".");
// check for access token in headers
if (
headers &&
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(
{ resource, accessTokenId, accessToken }
{
accessToken,
accessTokenId,
resourceId: resource.resourceId
}
);
if (error) {
@@ -206,16 +229,44 @@ export async function verifyResourceSession(
}
if (valid && tokenItem) {
validAccessToken = tokenItem;
return allowed(res);
}
}
if (!sessions) {
return await createAccessTokenSession(
res,
resource,
tokenItem
if (
query &&
query[config.getRawConfig().server.resource_access_token_param]
) {
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) {
@@ -229,12 +280,10 @@ export async function verifyResourceSession(
return notAllowed(res);
}
const resourceSessionToken =
sessions[
`${config.getRawConfig().server.session_cookie_name}${
resource.ssl ? "_s" : ""
}`
];
const resourceSessionToken = extractResourceSessionToken(
sessions,
resource.ssl
);
if (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");
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) {
const data = {
data: { valid: false, redirectUrl },
@@ -612,21 +694,21 @@ export function isPathAllowed(pattern: string, path: string): boolean {
logger.debug(
`${indent}Found in-segment wildcard in "${currentPatternPart}"`
);
// Convert the pattern segment to a regex pattern
const regexPattern = currentPatternPart
.replace(/\*/g, ".*") // Replace * with .* for regex wildcard
.replace(/\?/g, "."); // Replace ? with . for single character wildcard if needed
const regex = new RegExp(`^${regexPattern}$`);
if (regex.test(currentPathPart)) {
logger.debug(
`${indent}Segment with wildcard matches: "${currentPatternPart}" matches "${currentPathPart}"`
);
return matchSegments(patternIndex + 1, pathIndex + 1);
}
logger.debug(
`${indent}Segment with wildcard mismatch: "${currentPatternPart}" doesn't match "${currentPathPart}"`
);
@@ -651,4 +733,4 @@ export function isPathAllowed(pattern: string, path: string): boolean {
const result = matchSegments(0, 0);
logger.debug(`Final result: ${result}`);
return result;
}
}

View File

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

View File

@@ -8,6 +8,7 @@ import * as target from "./target";
import * as user from "./user";
import * as auth from "./auth";
import * as role from "./role";
import * as supporterKey from "./supporterKey";
import * as accessToken from "./accessToken";
import HttpCode from "@server/types/HttpCode";
import {
@@ -22,7 +23,8 @@ import {
verifyRoleAccess,
verifySetResourceUsers,
verifyUserAccess,
getUserOrgs
getUserOrgs,
verifyUserIsServerAdmin
} from "@server/middlewares";
import { verifyUserHasAction } from "../middlewares/verifyUserHasAction";
import { ActionsEnum } from "@server/auth/actions";
@@ -239,7 +241,6 @@ authenticated.delete(
target.deleteTarget
);
authenticated.put(
"/org/:orgId/role",
verifyOrgAccess,
@@ -382,6 +383,12 @@ authenticated.get(
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);
// authenticated.get(
@@ -415,6 +422,13 @@ unauthenticated.get("/resource/:resourceId/auth", resource.getResourceAuthInfo);
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/users",
@@ -459,7 +473,11 @@ authenticated.delete(
// role.removeRoleAction
// );
authenticated.put("/newt", createNewt);
// authenticated.put(
// "/newt",
// verifyUserHasAction(ActionsEnum.createNewt),
// createNewt
// );
// Auth routes
export const authRouter = Router();
@@ -548,3 +566,8 @@ authRouter.post(
"/resource/:resourceId/access-token",
resource.authWithAccessToken
);
authRouter.post(
"/access-token",
resource.authWithAccessToken
);

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from 'express';
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 { eq } from 'drizzle-orm';
import response from "@server/lib/response";

View File

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

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express";
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 logger from "@server/logger";
import createHttpError from "http-errors";
@@ -59,8 +59,8 @@ export const receiveBandwidth = async (
await trx
.update(sites)
.set({
megabytesOut: (site.megabytesIn || 0) + bytesIn,
megabytesIn: (site.megabytesOut || 0) + bytesOut,
megabytesOut: (site.megabytesOut || 0) + bytesIn,
megabytesIn: (site.megabytesIn || 0) + bytesOut,
lastBandwidthUpdate: new Date().toISOString(),
online
})

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ import {
sites,
Target,
targets
} from "@server/db/schema";
} from "@server/db/schemas";
import { eq, and, sql, inArray } from "drizzle-orm";
import { addPeer, deletePeer } from "../gerbil/peers";
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";
export function addTargets(

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,12 +10,13 @@ import {
userResources,
users,
userSites
} from "@server/db/schema";
} from "@server/db/schemas";
import { and, count, eq, inArray } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromZodError } from "zod-validation-error";
const getOrgParamsSchema = z
.object({
@@ -45,7 +46,7 @@ export async function getOrgOverview(
return next(
createHttpError(
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 { z } from "zod";
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 HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import { sql, inArray } from "drizzle-orm";
import logger from "@server/logger";
import { fromZodError } from "zod-validation-error";
const listOrgsSchema = z.object({
limit: z
@@ -39,7 +40,7 @@ export async function listOrgs(
return next(
createHttpError(
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 { z } from "zod";
import { db } from "@server/db";
import { orgs } from "@server/db/schema";
import { orgs } from "@server/db/schemas";
import { eq } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";

View File

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

View File

@@ -1,7 +1,7 @@
import { verify } from "@node-rs/argon2";
import { generateSessionToken } from "@server/auth/sessions/app";
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 response from "@server/lib/response";
import { eq } from "drizzle-orm";

View File

@@ -1,6 +1,6 @@
import { generateSessionToken } from "@server/auth/sessions/app";
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 response from "@server/lib/response";
import { eq } from "drizzle-orm";

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
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 response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
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 response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
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 response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
@@ -68,4 +68,4 @@ export async function deleteResourceRule(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}
}

View File

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

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
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 response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";

View File

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

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
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 response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
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 response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";

View File

@@ -1,5 +1,5 @@
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 response from "@server/lib/response";
import { eq, sql } from "drizzle-orm";

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
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 response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
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 HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";

View File

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

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
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 HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";

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