Skip to content

Commit 751dcd4

Browse files
authored
Merge pull request #292 from porters-xyz/develop
v0.6.0
2 parents 1ca0b49 + 251f4f0 commit 751dcd4

31 files changed

+960
-681
lines changed

docs/fly.prod.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ primary_region = 'sea'
88

99
[http_service]
1010
internal_port = 3000
11+
force_https = true
1112
auto_stop_machines = true
1213
auto_start_machines = true
1314
min_machines_running = 0

docs/fly.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ primary_region = 'sea'
99

1010
[http_service]
1111
internal_port = 3000
12+
force_https = true
1213
auto_stop_machines = true
1314
auto_start_machines = true
1415
min_machines_running = 0

gateway/plugins/origin.go

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import (
44
"context"
55
log "log/slog"
66
"net/http"
7-
"regexp"
7+
"strings"
88

99
"porters/db"
1010
"porters/proxy"
@@ -43,14 +43,7 @@ func (a *AllowedOriginFilter) HandleRequest(req *http.Request) error {
4343
}
4444

4545
rules := a.getRulesForScope(ctx, app)
46-
allow := (len(rules) == 0)
47-
48-
for _, rule := range rules {
49-
if rule.MatchString(origin) {
50-
allow = true
51-
break
52-
}
53-
}
46+
allow := a.matchesRules(origin, rules)
5447

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

62-
func (a *AllowedOriginFilter) getRulesForScope(ctx context.Context, app *db.App) []regexp.Regexp {
63-
origins := make([]regexp.Regexp, 0)
55+
func (a *AllowedOriginFilter) HandleResponse(resp *http.Response) error {
56+
ctx := resp.Request.Context()
57+
app := &db.App{
58+
Id: proxy.PluckAppId(resp.Request),
59+
}
60+
err := app.Lookup(ctx)
61+
if err != nil {
62+
return nil // don't modify header
63+
}
64+
65+
rules := a.getRulesForScope(ctx, app)
66+
if len(rules) > 0 {
67+
allowedOrigins := strings.Join(rules, ",")
68+
resp.Header.Set("Access-Control-Allow-Origin", allowedOrigins)
69+
}
70+
return nil
71+
}
72+
73+
func (a *AllowedOriginFilter) getRulesForScope(ctx context.Context, app *db.App) []string {
74+
origins := make([]string, 0)
6475
rules, err := app.Rules(ctx)
6576
if err != nil {
6677
log.Error("couldn't get rules", "app", app.HashId(), "err", err)
@@ -69,13 +80,17 @@ func (a *AllowedOriginFilter) getRulesForScope(ctx context.Context, app *db.App)
6980
if rule.RuleType != ALLOWED_ORIGIN || !rule.Active {
7081
continue
7182
}
72-
matcher, err := regexp.Compile(rule.Value)
73-
if err != nil {
74-
log.Error("error compiling origin regex", "regex", rule.Value, "err", err)
75-
continue
76-
}
77-
origins = append(origins, *matcher)
83+
origins = append(origins, rule.Value)
7884
}
7985
}
8086
return origins
8187
}
88+
89+
func (a *AllowedOriginFilter) matchesRules(origin string, rules []string) bool {
90+
for _, rule := range rules {
91+
if strings.EqualFold(rule, origin) {
92+
return true
93+
}
94+
}
95+
return false
96+
}

gateway/plugins/origin_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package plugins
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestAllowedOriginMatches(t *testing.T) {
8+
want := true
9+
origin := "http://test.com"
10+
allowed := []string{"http://test2.com", "http://test.com"}
11+
filter := &AllowedOriginFilter{}
12+
got := filter.matchesRules(origin, allowed)
13+
if want != got {
14+
t.Fatal("origin doesn't match")
15+
}
16+
}
17+
18+
func TestAllowedOriginMismatch(t *testing.T) {
19+
want := false
20+
origin := "http://test3.com"
21+
allowed := []string{"http://test2.com", "http://test.com"}
22+
filter := &AllowedOriginFilter{}
23+
got := filter.matchesRules(origin, allowed)
24+
if want != got {
25+
t.Fatal("origin doesn't match")
26+
}
27+
}

gateway/proxy/proxy.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ func setupProxy(remote *url.URL) *httputil.ReverseProxy {
132132

133133
revProxy.ModifyResponse = func(resp *http.Response) error {
134134
ctx := resp.Request.Context()
135+
defaultHeaders(resp)
135136

136137
if common.Enabled(common.INSTRUMENT_ENABLED) {
137138
instr, ok := common.FromContext(ctx, common.INSTRUMENT)
@@ -191,6 +192,12 @@ func setupContext(req *http.Request) {
191192
*req = *req.WithContext(ctx)
192193
}
193194

195+
// Add or remove headers on response
196+
// Dealing with CORS mostly
197+
func defaultHeaders(resp *http.Response) {
198+
resp.Header.Set("Access-Control-Allow-Origin", "*")
199+
}
200+
194201
func lookupPoktId(req *http.Request) (string, bool) {
195202
ctx := req.Context()
196203
name := PluckProductName(req)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { AlertsController } from './alerts.controller';
3+
4+
describe('AlertsController', () => {
5+
let controller: AlertsController;
6+
7+
beforeEach(async () => {
8+
const module: TestingModule = await Test.createTestingModule({
9+
controllers: [AlertsController],
10+
}).compile();
11+
12+
controller = module.get<AlertsController>(AlertsController);
13+
});
14+
15+
it('should be defined', () => {
16+
expect(controller).toBeDefined();
17+
});
18+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Controller, Get, Param } from '@nestjs/common';
2+
import { AlertsService } from './alerts.service';
3+
4+
@Controller('alerts')
5+
export class AlertsController {
6+
constructor(private readonly alertService: AlertsService) { }
7+
@Get('app/:appId')
8+
async getAppAlerts(@Param('appId') appId: string) {
9+
return this.alertService.getAppAlerts(appId)
10+
}
11+
@Get('tenant/:tenantId')
12+
async getTenantAlerts(@Param('appId') tenantId: string) {
13+
return this.alertService.getTenantAlerts(tenantId)
14+
}
15+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Module } from '@nestjs/common';
2+
import { AlertsService } from './alerts.service';
3+
import { AlertsController } from './alerts.controller';
4+
5+
@Module({
6+
providers: [AlertsService],
7+
controllers: [AlertsController]
8+
})
9+
export class AlertsModule {}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { AlertsService } from './alerts.service';
3+
4+
describe('AlertsService', () => {
5+
let service: AlertsService;
6+
7+
beforeEach(async () => {
8+
const module: TestingModule = await Test.createTestingModule({
9+
providers: [AlertsService],
10+
}).compile();
11+
12+
service = module.get<AlertsService>(AlertsService);
13+
});
14+
15+
it('should be defined', () => {
16+
expect(service).toBeDefined();
17+
});
18+
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { HttpException, Injectable } from '@nestjs/common';
2+
import { createHash } from 'crypto';
3+
4+
5+
interface PromResponse {
6+
data: {
7+
result: {
8+
value: any[];
9+
};
10+
};
11+
}
12+
13+
@Injectable()
14+
export class AlertsService {
15+
16+
async getAppAlerts(appId: string): Promise<any> {
17+
const hashedAppId = createHash('sha256').update(appId).digest('hex');
18+
const result = await this.fetchRateLimitStatus({ appId: hashedAppId })
19+
return result.json()
20+
}
21+
22+
async getTenantAlerts(tenantId: string): Promise<any> {
23+
const result = await this.fetchRateLimitStatus({ tenantId })
24+
return result.json()
25+
}
26+
27+
private async fetchRateLimitStatus(
28+
{ tenantId, appId }: { tenantId?: string; appId?: string }
29+
): Promise<Response> {
30+
31+
const query = tenantId
32+
? `query?query=gateway_rate_limit_hit{tenant="${tenantId}"}`
33+
: `query?query=gateway_rate_limit_hit{appId="${appId}"}`;
34+
35+
const url = process.env.PROM_URL + query;
36+
37+
const res = await fetch(url, {
38+
headers: {
39+
Authorization: String(process.env.PROM_TOKEN),
40+
},
41+
});
42+
43+
if (!res.ok) {
44+
throw new HttpException('Failed to fetch data', res.status);
45+
}
46+
47+
48+
return res
49+
}
50+
51+
}

web-portal/backend/src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { UsageModule } from './usage/usage.module';
1414
import { SiweService } from './siwe/siwe.service';
1515
import { AuthGuard } from './guards/auth.guard';
1616
import { AppsService } from './apps/apps.service';
17+
import { AlertsModule } from './alerts/alerts.module';
1718

1819
@Module({
1920
imports: [
@@ -32,6 +33,7 @@ import { AppsService } from './apps/apps.service';
3233
OrgModule,
3334
UtilsModule,
3435
UsageModule,
36+
AlertsModule,
3537
],
3638
providers: [
3739
SiweService,

web-portal/backend/src/siwe/siwe.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export interface ISession {
1313
}
1414

1515
export const SESSION_OPTIONS = {
16-
ttl: 60 * 60, // 1 hour
16+
ttl: 60 * 60 * 60,
1717
password:
1818
process.env.SESSION_SECRET! ?? `NNb774sZ7bNnGkWTwkXE3T9QWCAC5DkY0HTLz`, // TODO: get via env vars only
1919
};

web-portal/frontend/components/dashboard/applist.tsx

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import React from "react";
2-
import { Stack, Table, Flex, Title, Card, Button, CopyButton, Input, Tooltip } from "@mantine/core";
2+
import {
3+
Stack,
4+
Table,
5+
Flex,
6+
Title,
7+
Button,
8+
CopyButton,
9+
Input,
10+
Tooltip,
11+
} from "@mantine/core";
312
import { IApp } from "@frontend/utils/types";
413
import { IconChevronRight, IconCopy } from "@tabler/icons-react";
514
import { usePathname, useRouter } from "next/navigation";
@@ -21,27 +30,27 @@ const AppList: React.FC = () => {
2130
>
2231
<Table.Th>{app.name ?? "Un-named App"}</Table.Th>
2332
<Table.Td>
24-
<CopyButton value={app.id}>
25-
{({ copied, copy }) => (
26-
<Tooltip
27-
label={copied ? "Copied App Id" : "Copy App Id"}
28-
bg={copied ? "orange" : "black"}
29-
>
30-
<Input
31-
value={app.id}
32-
readOnly
33-
style={{ cursor: "pointer" }}
34-
onClick={copy}
35-
rightSection={
36-
<IconCopy size={18}/>
37-
}
38-
/>
39-
</Tooltip>
40-
)}
41-
</CopyButton>
33+
<CopyButton value={app.id}>
34+
{({ copied, copy }) => (
35+
<Tooltip
36+
label={copied ? "Copied App Id" : "Copy App Id"}
37+
bg={copied ? "orange" : "black"}
38+
>
39+
<Input
40+
value={app.id}
41+
readOnly
42+
style={{ cursor: "pointer" }}
43+
onClick={copy}
44+
rightSection={<IconCopy size={18} />}
45+
/>
46+
</Tooltip>
47+
)}
48+
</CopyButton>
4249
</Table.Td>
4350
<Table.Td>{app.active ? "Yes" : "No"}</Table.Td>
44-
<Table.Td>{new Date(app?.createdAt as string).toLocaleDateString()}</Table.Td>
51+
<Table.Td>
52+
{new Date(app?.createdAt as string).toLocaleDateString()}
53+
</Table.Td>
4554
</Table.Tr>
4655
));
4756

web-portal/frontend/components/dashboard/createAppModal.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import { useSearchParams, usePathname, useRouter } from "next/navigation";
22
import { Modal, Button, TextInput, Textarea } from "@mantine/core";
33
import { useForm } from "@mantine/form";
4-
import { useSession, useCreateAppMutation } from "@frontend/utils/hooks";
4+
import { useCreateAppMutation } from "@frontend/utils/hooks";
55

66
export default function CreateAppModal() {
77
const searchParams = useSearchParams();
88
const shouldOpen = searchParams?.get("new") === "app";
99
const path = usePathname();
1010
const router = useRouter();
11-
const { data: session } = useSession();
1211

1312
const { values, getInputProps } = useForm({
1413
initialValues: {

0 commit comments

Comments
 (0)