pierre-review 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,6 +26,14 @@ export async function authRoutes(app) {
26
26
  // Verify state → exchange code for a token → fetch the user → upsert account →
27
27
  // set session → 302 to the app.
28
28
  app.get('/api/auth/callback', async (req, reply) => {
29
+ // Already signed in? The callback URL carries a one-time ?code= that GitHub
30
+ // expires in ~10 min and invalidates on first use. A back-button, a restored
31
+ // tab, or a Chrome profile switch can re-request this exact URL; re-running the
32
+ // exchange would then fail on the already-consumed code ("code incorrect or
33
+ // expired"). Short-circuit a valid session straight to the app instead.
34
+ if (req.session.get('accountId') != null) {
35
+ return reply.redirect('/app');
36
+ }
29
37
  const { code, state } = req.query;
30
38
  const rawCookie = req.cookies.pierre_oauth_state;
31
39
  const unsigned = rawCookie
@@ -33,9 +41,9 @@ export async function authRoutes(app) {
33
41
  : { valid: false, value: null };
34
42
  reply.clearCookie('pierre_oauth_state', { path: '/' });
35
43
  if (!code || !state || !unsigned.valid || unsigned.value !== state) {
36
- return reply
37
- .code(400)
38
- .send({ error: 'BadRequest', message: 'Invalid or expired OAuth state.' });
44
+ // Stale/replayed callback (state cookie expired or CSRF mismatch). Bounce
45
+ // back so the user starts a fresh sign-in — never render raw JSON to a human.
46
+ return reply.redirect('/app?auth=expired');
39
47
  }
40
48
  // Exchange the code for a user access token.
41
49
  let token;
@@ -52,18 +60,14 @@ export async function authRoutes(app) {
52
60
  });
53
61
  const json = (await res.json());
54
62
  if (!json.access_token) {
55
- return reply.code(401).send({
56
- error: 'Unauthorized',
57
- message: `OAuth token exchange failed: ${json.error_description ?? json.error ?? 'no token'}`,
58
- });
63
+ req.log.warn({ err: json.error_description ?? json.error }, 'oauth token exchange returned no token');
64
+ return reply.redirect('/app?auth=failed');
59
65
  }
60
66
  token = json.access_token;
61
67
  }
62
68
  catch (err) {
63
- return reply.code(502).send({
64
- error: 'GitHubError',
65
- message: `OAuth exchange request failed: ${err instanceof Error ? err.message : err}`,
66
- });
69
+ req.log.error({ err }, 'oauth token exchange request failed');
70
+ return reply.redirect('/app?auth=error');
67
71
  }
68
72
  // Identify the user.
69
73
  let user;
@@ -76,17 +80,14 @@ export async function authRoutes(app) {
76
80
  },
77
81
  });
78
82
  if (!res.ok) {
79
- return reply
80
- .code(401)
81
- .send({ error: 'Unauthorized', message: 'Failed to fetch GitHub user.' });
83
+ req.log.warn({ status: res.status }, 'failed to fetch github user');
84
+ return reply.redirect('/app?auth=failed');
82
85
  }
83
86
  user = (await res.json());
84
87
  }
85
88
  catch (err) {
86
- return reply.code(502).send({
87
- error: 'GitHubError',
88
- message: `Fetching GitHub user failed: ${err instanceof Error ? err.message : err}`,
89
- });
89
+ req.log.error({ err }, 'fetching github user failed');
90
+ return reply.redirect('/app?auth=error');
90
91
  }
91
92
  const account = await upsertCloudAccount({
92
93
  githubUserId: user.node_id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pierre-review",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Dashboard for tracking your team's GitHub PR activity across repos — local (SQLite + gh) or self-hosted multi-tenant cloud (Postgres + GitHub App).",
5
5
  "type": "module",
6
6
  "author": "Alex Wakeman",