Skip to content

Commit

Permalink
Merge pull request #292 from porters-xyz/develop
Browse files Browse the repository at this point in the history
v0.6.0
  • Loading branch information
wtfsayo authored May 27, 2024
2 parents 1ca0b49 + 251f4f0 commit 751dcd4
Show file tree
Hide file tree
Showing 31 changed files with 960 additions and 681 deletions.
1 change: 1 addition & 0 deletions docs/fly.prod.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ primary_region = 'sea'

[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
Expand Down
1 change: 1 addition & 0 deletions docs/fly.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ primary_region = 'sea'

[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
Expand Down
49 changes: 32 additions & 17 deletions gateway/plugins/origin.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"context"
log "log/slog"
"net/http"
"regexp"
"strings"

"porters/db"
"porters/proxy"
Expand Down Expand Up @@ -43,14 +43,7 @@ func (a *AllowedOriginFilter) HandleRequest(req *http.Request) error {
}

rules := a.getRulesForScope(ctx, app)
allow := (len(rules) == 0)

for _, rule := range rules {
if rule.MatchString(origin) {
allow = true
break
}
}
allow := a.matchesRules(origin, rules)

if !allow {
return proxy.NewHTTPError(http.StatusUnauthorized)
Expand All @@ -59,8 +52,26 @@ func (a *AllowedOriginFilter) HandleRequest(req *http.Request) error {
return nil
}

func (a *AllowedOriginFilter) getRulesForScope(ctx context.Context, app *db.App) []regexp.Regexp {
origins := make([]regexp.Regexp, 0)
func (a *AllowedOriginFilter) HandleResponse(resp *http.Response) error {
ctx := resp.Request.Context()
app := &db.App{
Id: proxy.PluckAppId(resp.Request),
}
err := app.Lookup(ctx)
if err != nil {
return nil // don't modify header
}

rules := a.getRulesForScope(ctx, app)
if len(rules) > 0 {
allowedOrigins := strings.Join(rules, ",")
resp.Header.Set("Access-Control-Allow-Origin", allowedOrigins)
}
return nil
}

func (a *AllowedOriginFilter) getRulesForScope(ctx context.Context, app *db.App) []string {
origins := make([]string, 0)
rules, err := app.Rules(ctx)
if err != nil {
log.Error("couldn't get rules", "app", app.HashId(), "err", err)
Expand All @@ -69,13 +80,17 @@ func (a *AllowedOriginFilter) getRulesForScope(ctx context.Context, app *db.App)
if rule.RuleType != ALLOWED_ORIGIN || !rule.Active {
continue
}
matcher, err := regexp.Compile(rule.Value)
if err != nil {
log.Error("error compiling origin regex", "regex", rule.Value, "err", err)
continue
}
origins = append(origins, *matcher)
origins = append(origins, rule.Value)
}
}
return origins
}

func (a *AllowedOriginFilter) matchesRules(origin string, rules []string) bool {
for _, rule := range rules {
if strings.EqualFold(rule, origin) {
return true
}
}
return false
}
27 changes: 27 additions & 0 deletions gateway/plugins/origin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package plugins

import (
"testing"
)

func TestAllowedOriginMatches(t *testing.T) {
want := true
origin := "http://test.com"
allowed := []string{"http://test2.com", "http://test.com"}
filter := &AllowedOriginFilter{}
got := filter.matchesRules(origin, allowed)
if want != got {
t.Fatal("origin doesn't match")
}
}

func TestAllowedOriginMismatch(t *testing.T) {
want := false
origin := "http://test3.com"
allowed := []string{"http://test2.com", "http://test.com"}
filter := &AllowedOriginFilter{}
got := filter.matchesRules(origin, allowed)
if want != got {
t.Fatal("origin doesn't match")
}
}
7 changes: 7 additions & 0 deletions gateway/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ func setupProxy(remote *url.URL) *httputil.ReverseProxy {

revProxy.ModifyResponse = func(resp *http.Response) error {
ctx := resp.Request.Context()
defaultHeaders(resp)

if common.Enabled(common.INSTRUMENT_ENABLED) {
instr, ok := common.FromContext(ctx, common.INSTRUMENT)
Expand Down Expand Up @@ -191,6 +192,12 @@ func setupContext(req *http.Request) {
*req = *req.WithContext(ctx)
}

// Add or remove headers on response
// Dealing with CORS mostly
func defaultHeaders(resp *http.Response) {
resp.Header.Set("Access-Control-Allow-Origin", "*")
}

func lookupPoktId(req *http.Request) (string, bool) {
ctx := req.Context()
name := PluckProductName(req)
Expand Down
18 changes: 18 additions & 0 deletions web-portal/backend/src/alerts/alerts.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AlertsController } from './alerts.controller';

describe('AlertsController', () => {
let controller: AlertsController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AlertsController],
}).compile();

controller = module.get<AlertsController>(AlertsController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
15 changes: 15 additions & 0 deletions web-portal/backend/src/alerts/alerts.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Controller, Get, Param } from '@nestjs/common';
import { AlertsService } from './alerts.service';

@Controller('alerts')
export class AlertsController {
constructor(private readonly alertService: AlertsService) { }
@Get('app/:appId')
async getAppAlerts(@Param('appId') appId: string) {
return this.alertService.getAppAlerts(appId)
}
@Get('tenant/:tenantId')
async getTenantAlerts(@Param('appId') tenantId: string) {
return this.alertService.getTenantAlerts(tenantId)
}
}
9 changes: 9 additions & 0 deletions web-portal/backend/src/alerts/alerts.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { AlertsService } from './alerts.service';
import { AlertsController } from './alerts.controller';

@Module({
providers: [AlertsService],
controllers: [AlertsController]
})
export class AlertsModule {}
18 changes: 18 additions & 0 deletions web-portal/backend/src/alerts/alerts.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AlertsService } from './alerts.service';

describe('AlertsService', () => {
let service: AlertsService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AlertsService],
}).compile();

service = module.get<AlertsService>(AlertsService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
51 changes: 51 additions & 0 deletions web-portal/backend/src/alerts/alerts.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { HttpException, Injectable } from '@nestjs/common';
import { createHash } from 'crypto';


interface PromResponse {
data: {
result: {
value: any[];
};
};
}

@Injectable()
export class AlertsService {

async getAppAlerts(appId: string): Promise<any> {
const hashedAppId = createHash('sha256').update(appId).digest('hex');
const result = await this.fetchRateLimitStatus({ appId: hashedAppId })
return result.json()
}

async getTenantAlerts(tenantId: string): Promise<any> {
const result = await this.fetchRateLimitStatus({ tenantId })
return result.json()
}

private async fetchRateLimitStatus(
{ tenantId, appId }: { tenantId?: string; appId?: string }
): Promise<Response> {

const query = tenantId
? `query?query=gateway_rate_limit_hit{tenant="${tenantId}"}`
: `query?query=gateway_rate_limit_hit{appId="${appId}"}`;

const url = process.env.PROM_URL + query;

const res = await fetch(url, {
headers: {
Authorization: String(process.env.PROM_TOKEN),
},
});

if (!res.ok) {
throw new HttpException('Failed to fetch data', res.status);
}


return res
}

}
2 changes: 2 additions & 0 deletions web-portal/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { UsageModule } from './usage/usage.module';
import { SiweService } from './siwe/siwe.service';
import { AuthGuard } from './guards/auth.guard';
import { AppsService } from './apps/apps.service';
import { AlertsModule } from './alerts/alerts.module';

@Module({
imports: [
Expand All @@ -32,6 +33,7 @@ import { AppsService } from './apps/apps.service';
OrgModule,
UtilsModule,
UsageModule,
AlertsModule,
],
providers: [
SiweService,
Expand Down
2 changes: 1 addition & 1 deletion web-portal/backend/src/siwe/siwe.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export interface ISession {
}

export const SESSION_OPTIONS = {
ttl: 60 * 60, // 1 hour
ttl: 60 * 60 * 60,
password:
process.env.SESSION_SECRET! ?? `NNb774sZ7bNnGkWTwkXE3T9QWCAC5DkY0HTLz`, // TODO: get via env vars only
};
Expand Down
49 changes: 29 additions & 20 deletions web-portal/frontend/components/dashboard/applist.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import React from "react";
import { Stack, Table, Flex, Title, Card, Button, CopyButton, Input, Tooltip } from "@mantine/core";
import {
Stack,
Table,
Flex,
Title,
Button,
CopyButton,
Input,
Tooltip,
} from "@mantine/core";
import { IApp } from "@frontend/utils/types";
import { IconChevronRight, IconCopy } from "@tabler/icons-react";
import { usePathname, useRouter } from "next/navigation";
Expand All @@ -21,27 +30,27 @@ const AppList: React.FC = () => {
>
<Table.Th>{app.name ?? "Un-named App"}</Table.Th>
<Table.Td>
<CopyButton value={app.id}>
{({ copied, copy }) => (
<Tooltip
label={copied ? "Copied App Id" : "Copy App Id"}
bg={copied ? "orange" : "black"}
>
<Input
value={app.id}
readOnly
style={{ cursor: "pointer" }}
onClick={copy}
rightSection={
<IconCopy size={18}/>
}
/>
</Tooltip>
)}
</CopyButton>
<CopyButton value={app.id}>
{({ copied, copy }) => (
<Tooltip
label={copied ? "Copied App Id" : "Copy App Id"}
bg={copied ? "orange" : "black"}
>
<Input
value={app.id}
readOnly
style={{ cursor: "pointer" }}
onClick={copy}
rightSection={<IconCopy size={18} />}
/>
</Tooltip>
)}
</CopyButton>
</Table.Td>
<Table.Td>{app.active ? "Yes" : "No"}</Table.Td>
<Table.Td>{new Date(app?.createdAt as string).toLocaleDateString()}</Table.Td>
<Table.Td>
{new Date(app?.createdAt as string).toLocaleDateString()}
</Table.Td>
</Table.Tr>
));

Expand Down
3 changes: 1 addition & 2 deletions web-portal/frontend/components/dashboard/createAppModal.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { useSearchParams, usePathname, useRouter } from "next/navigation";
import { Modal, Button, TextInput, Textarea } from "@mantine/core";
import { useForm } from "@mantine/form";
import { useSession, useCreateAppMutation } from "@frontend/utils/hooks";
import { useCreateAppMutation } from "@frontend/utils/hooks";

export default function CreateAppModal() {
const searchParams = useSearchParams();
const shouldOpen = searchParams?.get("new") === "app";
const path = usePathname();
const router = useRouter();
const { data: session } = useSession();

const { values, getInputProps } = useForm({
initialValues: {
Expand Down
Loading

0 comments on commit 751dcd4

Please sign in to comment.