Skip to content

Commit 5cc4203

Browse files
committed
feat: add personal access tokens
1 parent 514412f commit 5cc4203

37 files changed

+2335
-187
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* @param {import('knex').Knex} knex
3+
*/
4+
export const up = async (knex) => {
5+
await knex.schema.createTable("user_access_tokens", (table) => {
6+
table.bigIncrements("id").primary();
7+
table.dateTime("createdAt").notNullable();
8+
table.dateTime("updatedAt").notNullable();
9+
table.bigInteger("userId").notNullable().index();
10+
table.foreign("userId").references("users.id");
11+
table.string("name").notNullable();
12+
table.string("token").notNullable().unique();
13+
table.dateTime("expireAt").nullable();
14+
table.dateTime("lastUsedAt").nullable();
15+
table.string("createdBy").notNullable();
16+
});
17+
18+
await knex.schema.createTable("user_access_token_scopes", (table) => {
19+
table.bigIncrements("id").primary();
20+
table.dateTime("createdAt").notNullable();
21+
table.dateTime("updatedAt").notNullable();
22+
table.bigInteger("userAccessTokenId").notNullable().index();
23+
table.foreign("userAccessTokenId").references("user_access_tokens.id");
24+
table.bigInteger("accountId").notNullable().index();
25+
table.foreign("accountId").references("accounts.id");
26+
table.unique(["userAccessTokenId", "accountId"]);
27+
});
28+
};
29+
30+
/**
31+
* @param {import('knex').Knex} knex
32+
*/
33+
export const down = async (knex) => {
34+
await knex.schema.dropTable("user_access_token_scopes");
35+
await knex.schema.dropTable("user_access_tokens");
36+
};

apps/backend/db/structure.sql

Lines changed: 191 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
-- PostgreSQL database dump
33
--
44

5+
\restrict JfsOuN39OUWpiPypbkunGr6fxbeB43MaGJ83Ohqy3EUrRTZUZKCGaWZR0OCiau5
6+
57
-- Dumped from database version 17.5
6-
-- Dumped by pg_dump version 17.5 (Homebrew)
8+
-- Dumped by pg_dump version 17.9 (Homebrew)
79

810
SET statement_timeout = 0;
911
SET lock_timeout = 0;
@@ -693,7 +695,7 @@ ALTER SEQUENCE public.github_installations_id_seq OWNED BY public.github_install
693695
--
694696

695697
CREATE TABLE public.github_pull_requests (
696-
id bigint NOT NULL,
698+
id integer NOT NULL,
697699
"createdAt" timestamp with time zone NOT NULL,
698700
"updatedAt" timestamp with time zone NOT NULL,
699701
"commentDeleted" boolean DEFAULT false NOT NULL,
@@ -1825,6 +1827,82 @@ ALTER SEQUENCE public.tests_id_seq OWNER TO postgres;
18251827
ALTER SEQUENCE public.tests_id_seq OWNED BY public.tests.id;
18261828

18271829

1830+
--
1831+
-- Name: user_access_token_scopes; Type: TABLE; Schema: public; Owner: postgres
1832+
--
1833+
1834+
CREATE TABLE public.user_access_token_scopes (
1835+
id bigint NOT NULL,
1836+
"createdAt" timestamp with time zone NOT NULL,
1837+
"updatedAt" timestamp with time zone NOT NULL,
1838+
"userAccessTokenId" bigint NOT NULL,
1839+
"accountId" bigint NOT NULL
1840+
);
1841+
1842+
1843+
ALTER TABLE public.user_access_token_scopes OWNER TO postgres;
1844+
1845+
--
1846+
-- Name: user_access_token_scopes_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
1847+
--
1848+
1849+
CREATE SEQUENCE public.user_access_token_scopes_id_seq
1850+
START WITH 1
1851+
INCREMENT BY 1
1852+
NO MINVALUE
1853+
NO MAXVALUE
1854+
CACHE 1;
1855+
1856+
1857+
ALTER SEQUENCE public.user_access_token_scopes_id_seq OWNER TO postgres;
1858+
1859+
--
1860+
-- Name: user_access_token_scopes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
1861+
--
1862+
1863+
ALTER SEQUENCE public.user_access_token_scopes_id_seq OWNED BY public.user_access_token_scopes.id;
1864+
1865+
1866+
--
1867+
-- Name: user_access_tokens; Type: TABLE; Schema: public; Owner: postgres
1868+
--
1869+
1870+
CREATE TABLE public.user_access_tokens (
1871+
id bigint NOT NULL,
1872+
"createdAt" timestamp with time zone NOT NULL,
1873+
"updatedAt" timestamp with time zone NOT NULL,
1874+
"userId" bigint NOT NULL,
1875+
name character varying(255) NOT NULL,
1876+
token character varying(255) NOT NULL,
1877+
"expireAt" timestamp with time zone,
1878+
"lastUsedAt" timestamp with time zone,
1879+
"createdBy" character varying(255) NOT NULL
1880+
);
1881+
1882+
1883+
ALTER TABLE public.user_access_tokens OWNER TO postgres;
1884+
1885+
--
1886+
-- Name: user_access_tokens_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
1887+
--
1888+
1889+
CREATE SEQUENCE public.user_access_tokens_id_seq
1890+
START WITH 1
1891+
INCREMENT BY 1
1892+
NO MINVALUE
1893+
NO MAXVALUE
1894+
CACHE 1;
1895+
1896+
1897+
ALTER SEQUENCE public.user_access_tokens_id_seq OWNER TO postgres;
1898+
1899+
--
1900+
-- Name: user_access_tokens_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
1901+
--
1902+
1903+
ALTER SEQUENCE public.user_access_tokens_id_seq OWNED BY public.user_access_tokens.id;
1904+
1905+
18281906
--
18291907
-- Name: user_emails; Type: TABLE; Schema: public; Owner: postgres
18301908
--
@@ -2152,6 +2230,20 @@ ALTER TABLE ONLY public.teams ALTER COLUMN id SET DEFAULT nextval('public.teams_
21522230
ALTER TABLE ONLY public.tests ALTER COLUMN id SET DEFAULT nextval('public.tests_id_seq'::regclass);
21532231

21542232

2233+
--
2234+
-- Name: user_access_token_scopes id; Type: DEFAULT; Schema: public; Owner: postgres
2235+
--
2236+
2237+
ALTER TABLE ONLY public.user_access_token_scopes ALTER COLUMN id SET DEFAULT nextval('public.user_access_token_scopes_id_seq'::regclass);
2238+
2239+
2240+
--
2241+
-- Name: user_access_tokens id; Type: DEFAULT; Schema: public; Owner: postgres
2242+
--
2243+
2244+
ALTER TABLE ONLY public.user_access_tokens ALTER COLUMN id SET DEFAULT nextval('public.user_access_tokens_id_seq'::regclass);
2245+
2246+
21552247
--
21562248
-- Name: users id; Type: DEFAULT; Schema: public; Owner: postgres
21572249
--
@@ -2663,6 +2755,38 @@ ALTER TABLE ONLY public.tests
26632755
ADD CONSTRAINT tests_pkey PRIMARY KEY (id);
26642756

26652757

2758+
--
2759+
-- Name: user_access_token_scopes user_access_token_scopes_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
2760+
--
2761+
2762+
ALTER TABLE ONLY public.user_access_token_scopes
2763+
ADD CONSTRAINT user_access_token_scopes_pkey PRIMARY KEY (id);
2764+
2765+
2766+
--
2767+
-- Name: user_access_token_scopes user_access_token_scopes_useraccesstokenid_accountid_unique; Type: CONSTRAINT; Schema: public; Owner: postgres
2768+
--
2769+
2770+
ALTER TABLE ONLY public.user_access_token_scopes
2771+
ADD CONSTRAINT user_access_token_scopes_useraccesstokenid_accountid_unique UNIQUE ("userAccessTokenId", "accountId");
2772+
2773+
2774+
--
2775+
-- Name: user_access_tokens user_access_tokens_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
2776+
--
2777+
2778+
ALTER TABLE ONLY public.user_access_tokens
2779+
ADD CONSTRAINT user_access_tokens_pkey PRIMARY KEY (id);
2780+
2781+
2782+
--
2783+
-- Name: user_access_tokens user_access_tokens_token_unique; Type: CONSTRAINT; Schema: public; Owner: postgres
2784+
--
2785+
2786+
ALTER TABLE ONLY public.user_access_tokens
2787+
ADD CONSTRAINT user_access_tokens_token_unique UNIQUE (token);
2788+
2789+
26662790
--
26672791
-- Name: user_emails user_emails_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
26682792
--
@@ -2671,6 +2795,22 @@ ALTER TABLE ONLY public.user_emails
26712795
ADD CONSTRAINT user_emails_pkey PRIMARY KEY (email);
26722796

26732797

2798+
--
2799+
-- Name: users users_email_unique; Type: CONSTRAINT; Schema: public; Owner: postgres
2800+
--
2801+
2802+
ALTER TABLE ONLY public.users
2803+
ADD CONSTRAINT users_email_unique UNIQUE (email);
2804+
2805+
2806+
--
2807+
-- Name: users users_gitlabuserid_unique; Type: CONSTRAINT; Schema: public; Owner: postgres
2808+
--
2809+
2810+
ALTER TABLE ONLY public.users
2811+
ADD CONSTRAINT users_gitlabuserid_unique UNIQUE ("gitlabUserId");
2812+
2813+
26742814
--
26752815
-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres
26762816
--
@@ -3169,6 +3309,27 @@ CREATE INDEX tests_projectid_buildname_createdat_id_idx ON public.tests USING bt
31693309
CREATE INDEX tests_projectid_index ON public.tests USING btree ("projectId");
31703310

31713311

3312+
--
3313+
-- Name: user_access_token_scopes_accountid_index; Type: INDEX; Schema: public; Owner: postgres
3314+
--
3315+
3316+
CREATE INDEX user_access_token_scopes_accountid_index ON public.user_access_token_scopes USING btree ("accountId");
3317+
3318+
3319+
--
3320+
-- Name: user_access_token_scopes_useraccesstokenid_index; Type: INDEX; Schema: public; Owner: postgres
3321+
--
3322+
3323+
CREATE INDEX user_access_token_scopes_useraccesstokenid_index ON public.user_access_token_scopes USING btree ("userAccessTokenId");
3324+
3325+
3326+
--
3327+
-- Name: user_access_tokens_userid_index; Type: INDEX; Schema: public; Owner: postgres
3328+
--
3329+
3330+
CREATE INDEX user_access_tokens_userid_index ON public.user_access_tokens USING btree ("userId");
3331+
3332+
31723333
--
31733334
-- Name: users_googleuserid_fk_index; Type: INDEX; Schema: public; Owner: postgres
31743335
--
@@ -3728,6 +3889,30 @@ ALTER TABLE ONLY public.tests
37283889
ADD CONSTRAINT tests_projectid_foreign FOREIGN KEY ("projectId") REFERENCES public.projects(id);
37293890

37303891

3892+
--
3893+
-- Name: user_access_token_scopes user_access_token_scopes_accountid_foreign; Type: FK CONSTRAINT; Schema: public; Owner: postgres
3894+
--
3895+
3896+
ALTER TABLE ONLY public.user_access_token_scopes
3897+
ADD CONSTRAINT user_access_token_scopes_accountid_foreign FOREIGN KEY ("accountId") REFERENCES public.accounts(id);
3898+
3899+
3900+
--
3901+
-- Name: user_access_token_scopes user_access_token_scopes_useraccesstokenid_foreign; Type: FK CONSTRAINT; Schema: public; Owner: postgres
3902+
--
3903+
3904+
ALTER TABLE ONLY public.user_access_token_scopes
3905+
ADD CONSTRAINT user_access_token_scopes_useraccesstokenid_foreign FOREIGN KEY ("userAccessTokenId") REFERENCES public.user_access_tokens(id);
3906+
3907+
3908+
--
3909+
-- Name: user_access_tokens user_access_tokens_userid_foreign; Type: FK CONSTRAINT; Schema: public; Owner: postgres
3910+
--
3911+
3912+
ALTER TABLE ONLY public.user_access_tokens
3913+
ADD CONSTRAINT user_access_tokens_userid_foreign FOREIGN KEY ("userId") REFERENCES public.users(id);
3914+
3915+
37313916
--
37323917
-- Name: user_emails user_emails_userid_foreign; Type: FK CONSTRAINT; Schema: public; Owner: postgres
37333918
--
@@ -3756,6 +3941,8 @@ ALTER TABLE ONLY public.users
37563941
-- PostgreSQL database dump complete
37573942
--
37583943

3944+
\unrestrict JfsOuN39OUWpiPypbkunGr6fxbeB43MaGJ83Ohqy3EUrRTZUZKCGaWZR0OCiau5
3945+
37593946
-- Knex migrations
37603947

37613948
INSERT INTO public.knex_migrations(name, batch, migration_time) VALUES ('20161217154940_init.js', 1, NOW());
@@ -3937,4 +4124,5 @@ INSERT INTO public.knex_migrations(name, batch, migration_time) VALUES ('2026021
39374124
INSERT INTO public.knex_migrations(name, batch, migration_time) VALUES ('20260216200736_enforce-sso.js', 1, NOW());
39384125
INSERT INTO public.knex_migrations(name, batch, migration_time) VALUES ('20260219170000_project-auto-ignore.js', 1, NOW());
39394126
INSERT INTO public.knex_migrations(name, batch, migration_time) VALUES ('20260222100000_team-saml-expiration-check.js', 1, NOW());
3940-
INSERT INTO public.knex_migrations(name, batch, migration_time) VALUES ('20260328120000_build-merge-queue-gh-pull-requests.js', 1, NOW());
4127+
INSERT INTO public.knex_migrations(name, batch, migration_time) VALUES ('20260328120000_build-merge-queue-gh-pull-requests.js', 1, NOW());
4128+
INSERT INTO public.knex_migrations(name, batch, migration_time) VALUES ('20260401084427_user_access_tokens.js', 1, NOW());

apps/backend/src/api/handlers/getAuthBuildByNumber.e2e.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import request from "supertest";
33
import { beforeAll, beforeEach, describe, expect, it } from "vitest";
44
import z from "zod";
55

6+
import { UserAccessTokenScope } from "@/database/models";
7+
import { hashToken } from "@/database/services/crypto";
68
import { factory, setupDatabase } from "@/database/testing";
79

810
import { createTestHandlerApp } from "../test-util";
@@ -137,4 +139,52 @@ describe("getAuthBuildByNumber", () => {
137139
.expect(400);
138140
});
139141
});
142+
143+
describe("with a user access token on explicit project route", () => {
144+
it("returns the build", async () => {
145+
const [user, account] = await Promise.all([
146+
factory.User.create(),
147+
factory.TeamAccount.create({ slug: "acme" }),
148+
]);
149+
150+
const [project] = await Promise.all([
151+
factory.Project.create({
152+
accountId: account.id,
153+
name: "web",
154+
}),
155+
factory.UserAccount.create({ userId: user.id }),
156+
factory.TeamUser.create({
157+
teamId: account.teamId,
158+
userId: user.id,
159+
userLevel: "member",
160+
}),
161+
]);
162+
const bucket = await factory.ScreenshotBucket.create({
163+
projectId: project.id,
164+
});
165+
const build = await factory.Build.create({
166+
projectId: project.id,
167+
compareScreenshotBucketId: bucket.id,
168+
number: 4,
169+
});
170+
171+
const token = `arp_${"f".repeat(36)}`;
172+
const userAccessToken = await factory.UserAccessToken.create({
173+
userId: user.id,
174+
token: hashToken(token),
175+
});
176+
await UserAccessTokenScope.query().insert({
177+
userAccessTokenId: userAccessToken.id,
178+
accountId: account.id,
179+
});
180+
181+
await request(app)
182+
.get("/projects/acme/web/builds/4")
183+
.set("Authorization", `Bearer ${token}`)
184+
.expect(200)
185+
.expect((res) => {
186+
expect(res.body.id).toBe(build.id);
187+
});
188+
});
189+
});
140190
});

0 commit comments

Comments
 (0)