Skip to content

Commit ddc8544

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

36 files changed

+2363
-178
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: 174 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
-- PostgreSQL database dump
33
--
44

5+
\restrict tcwwa0navv4WKeaYIi4EKa3vQQ4QsdRuGwaRIjvsh5ua5zp7TYRTpd8lgNzd9lh
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;
@@ -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
--
@@ -3169,6 +3293,27 @@ CREATE INDEX tests_projectid_buildname_createdat_id_idx ON public.tests USING bt
31693293
CREATE INDEX tests_projectid_index ON public.tests USING btree ("projectId");
31703294

31713295

3296+
--
3297+
-- Name: user_access_token_scopes_accountid_index; Type: INDEX; Schema: public; Owner: postgres
3298+
--
3299+
3300+
CREATE INDEX user_access_token_scopes_accountid_index ON public.user_access_token_scopes USING btree ("accountId");
3301+
3302+
3303+
--
3304+
-- Name: user_access_token_scopes_useraccesstokenid_index; Type: INDEX; Schema: public; Owner: postgres
3305+
--
3306+
3307+
CREATE INDEX user_access_token_scopes_useraccesstokenid_index ON public.user_access_token_scopes USING btree ("userAccessTokenId");
3308+
3309+
3310+
--
3311+
-- Name: user_access_tokens_userid_index; Type: INDEX; Schema: public; Owner: postgres
3312+
--
3313+
3314+
CREATE INDEX user_access_tokens_userid_index ON public.user_access_tokens USING btree ("userId");
3315+
3316+
31723317
--
31733318
-- Name: users_googleuserid_fk_index; Type: INDEX; Schema: public; Owner: postgres
31743319
--
@@ -3728,6 +3873,30 @@ ALTER TABLE ONLY public.tests
37283873
ADD CONSTRAINT tests_projectid_foreign FOREIGN KEY ("projectId") REFERENCES public.projects(id);
37293874

37303875

3876+
--
3877+
-- Name: user_access_token_scopes user_access_token_scopes_accountid_foreign; Type: FK CONSTRAINT; Schema: public; Owner: postgres
3878+
--
3879+
3880+
ALTER TABLE ONLY public.user_access_token_scopes
3881+
ADD CONSTRAINT user_access_token_scopes_accountid_foreign FOREIGN KEY ("accountId") REFERENCES public.accounts(id);
3882+
3883+
3884+
--
3885+
-- Name: user_access_token_scopes user_access_token_scopes_useraccesstokenid_foreign; Type: FK CONSTRAINT; Schema: public; Owner: postgres
3886+
--
3887+
3888+
ALTER TABLE ONLY public.user_access_token_scopes
3889+
ADD CONSTRAINT user_access_token_scopes_useraccesstokenid_foreign FOREIGN KEY ("userAccessTokenId") REFERENCES public.user_access_tokens(id);
3890+
3891+
3892+
--
3893+
-- Name: user_access_tokens user_access_tokens_userid_foreign; Type: FK CONSTRAINT; Schema: public; Owner: postgres
3894+
--
3895+
3896+
ALTER TABLE ONLY public.user_access_tokens
3897+
ADD CONSTRAINT user_access_tokens_userid_foreign FOREIGN KEY ("userId") REFERENCES public.users(id);
3898+
3899+
37313900
--
37323901
-- Name: user_emails user_emails_userid_foreign; Type: FK CONSTRAINT; Schema: public; Owner: postgres
37333902
--
@@ -3756,6 +3925,8 @@ ALTER TABLE ONLY public.users
37563925
-- PostgreSQL database dump complete
37573926
--
37583927

3928+
\unrestrict tcwwa0navv4WKeaYIi4EKa3vQQ4QsdRuGwaRIjvsh5ua5zp7TYRTpd8lgNzd9lh
3929+
37593930
-- Knex migrations
37603931

37613932
INSERT INTO public.knex_migrations(name, batch, migration_time) VALUES ('20161217154940_init.js', 1, NOW());
@@ -3937,4 +4108,5 @@ INSERT INTO public.knex_migrations(name, batch, migration_time) VALUES ('2026021
39374108
INSERT INTO public.knex_migrations(name, batch, migration_time) VALUES ('20260216200736_enforce-sso.js', 1, NOW());
39384109
INSERT INTO public.knex_migrations(name, batch, migration_time) VALUES ('20260219170000_project-auto-ignore.js', 1, NOW());
39394110
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());
4111+
INSERT INTO public.knex_migrations(name, batch, migration_time) VALUES ('20260328120000_build-merge-queue-gh-pull-requests.js', 1, NOW());
4112+
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 { UserAccessToken, 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 = UserAccessToken.generateToken();
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)