Skip to content

Commit dd6f65d

Browse files
committed
merge w main
2 parents d6c65ec + 943b574 commit dd6f65d

20 files changed

Lines changed: 933 additions & 52 deletions

.github/dependabot.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
version: 2
2+
updates:
3+
# Enable version updates for npm
4+
- package-ecosystem: "npm"
5+
directory: "/"
6+
schedule:
7+
interval: "weekly"
8+
assignees:
9+
- "aaronashby"
10+
- "thaninbew"
11+
open-pull-requests-limit: 3
12+
13+
# Enable version updates for Docker
14+
- package-ecosystem: "docker"
15+
directory: "/apps/backend"
16+
schedule:
17+
interval: "weekly"
18+
assignees:
19+
- "aaronashby"
20+
- "thaninbew"
21+
open-pull-requests-limit: 3
22+
23+
# Enable version updates for GitHub Actions
24+
- package-ecosystem: "github-actions"
25+
directory: "/"
26+
schedule:
27+
interval: "weekly"
28+
assignees:
29+
- "aaronashby"
30+
- "thaninbew"
31+
open-pull-requests-limit: 3

DEPENDABOT.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Dependabot Workflow
2+
3+
## Overview
4+
5+
Dependabot is a GitHub-native tool that automatically opens pull requests to keep dependencies up to date. Its configuration settings are located in `.github/dependabot.yml`, and runs weekly.
6+
7+
## What Dependabot Updates
8+
9+
- **Node.js dependencies**: Dependencies declared in `package.json` and the lockfile `yarn.lock`
10+
- **Docker dependencies**: Updates base image tags referenced by Dockerfiles (e.g. the one in `apps/backend`)
11+
- **GitHub Actions**: Updates action versions used in workflows in `.github/workflows`
12+
13+
## Schedule and Ownership
14+
15+
Dependabot creates PRs on a **weekly** basis, and automatically assigns the PRs to `aaronashby` and `thaninbew`
16+
17+
## How to Review Dependabot PRs
18+
19+
- Skim the PR title, release notes, and commits
20+
- Check the diff
21+
- Dependency updates often change `package.json` + `yarn.lock` (or only `yarn.lock`).
22+
- Docker updates typically change a `FROM …` line.
23+
- Actions updates usually change `uses: …@vX` pins in workflows.
24+
25+
## Merging Guidelines (suggested)
26+
27+
- **Patch/minor updates**: usually safe to merge once CI passes.
28+
- **Major updates**: prefer a quick manual smoke test and a scan for breaking changes.
29+
- **Lockfile-only updates**: merge if CI passes (these happen due to dependency resolution changes).
30+
31+
## Common Tweaks (edit `.github/dependabot.yml`)
32+
33+
- **Add a separate Docker entry for root compose files**
34+
- Dependabot currently only scans Docker in `/apps/backend`. If you want it to update `docker-compose.dev.yml` at the repo root, add another docker update with `directory: "/"`.
35+
- **Limit PR volume**
36+
- Add `open-pull-requests-limit: <number>` to an update block.
37+
- **Ignore versions**
38+
- Use `ignore:` to skip major versions or specific packages temporarily.
39+
- **Group updates**
40+
- Use `groups:` to bundle related packages (e.g., React, NestJS, Nx) into fewer PRs.
41+
42+
## Troubleshooting
43+
- **CI fails after a bump**
44+
- Check the package’s changelog/release notes and revert/ignore if needed.
45+
- If it’s a tooling bump (Nx/Vite/ESLint/TypeScript), failures often come from peer dependency changes or config deprecations.
46+
- **Dependabot isn’t opening PRs**
47+
- Confirm `.github/dependabot.yml` is on the default branch and syntactically valid.
48+
- Check the repo’s Dependabot alerts/PRs in GitHub for run history and errors.

apps/backend/src/auth/auth.controller.ts

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
1-
import { BadRequestException, Body, Controller, Post } from '@nestjs/common';
1+
import {
2+
BadRequestException,
3+
Body,
4+
Controller,
5+
ForbiddenException,
6+
Get,
7+
Post,
8+
Req,
9+
UseGuards,
10+
UseInterceptors,
11+
} from '@nestjs/common';
12+
import { AuthGuard } from '@nestjs/passport';
13+
import { CurrentUserInterceptor } from '../interceptors/current-user.interceptor';
14+
import { Status } from '../users/types';
215

316
import { SignInDto } from './dtos/sign-in.dto';
417
import { SignUpDto } from './dtos/sign-up.dto';
@@ -21,12 +34,63 @@ export class AuthController {
2134
private usersService: UsersService,
2235
) {}
2336

37+
@Get('/me')
38+
@UseGuards(AuthGuard('jwt'))
39+
@UseInterceptors(CurrentUserInterceptor)
40+
async me(@Req() req: any) {
41+
return req.user;
42+
}
43+
44+
@Post('/admin-verify')
45+
@UseGuards(AuthGuard('jwt'))
46+
@UseInterceptors(CurrentUserInterceptor)
47+
async adminVerify(
48+
@Req() req: any,
49+
@Body() body: { email: string },
50+
): Promise<void> {
51+
if (req.user.status !== Status.ADMIN) {
52+
throw new ForbiddenException('Only admins can verify users');
53+
}
54+
try {
55+
await this.authService.adminConfirmUser(body.email);
56+
} catch (e) {
57+
console.error('Admin verify error:', e);
58+
throw new BadRequestException(e.message);
59+
}
60+
}
61+
62+
@Get('/users')
63+
@UseGuards(AuthGuard('jwt'))
64+
@UseInterceptors(CurrentUserInterceptor)
65+
async listUsers(@Req() req: any) {
66+
try {
67+
const cognitoUsers = await this.authService.listAllUsers();
68+
// Combine with DB users
69+
const results = await Promise.all(
70+
cognitoUsers.map(async (cu) => {
71+
const email = cu.Attributes.find((a) => a.Name === 'email')?.Value;
72+
const dbUsers = email ? await this.usersService.find(email) : [];
73+
return {
74+
username: cu.Username,
75+
status: cu.UserStatus, // UNCONFIRMED, CONFIRMED, etc.
76+
email,
77+
dbUser: dbUsers[0] || null,
78+
};
79+
}),
80+
);
81+
return results;
82+
} catch (e) {
83+
throw new BadRequestException(e.message);
84+
}
85+
}
86+
2487
@Post('/signup')
2588
async createUser(@Body() signUpDto: SignUpDto): Promise<User> {
2689
// By default, creates a standard user
2790
try {
2891
await this.authService.signup(signUpDto);
2992
} catch (e) {
93+
console.error('Signup error:', e);
3094
throw new BadRequestException(e.message);
3195
}
3296

@@ -45,13 +109,18 @@ export class AuthController {
45109
try {
46110
this.authService.verifyUser(body.email, body.verificationCode);
47111
} catch (e) {
112+
console.error('Verify error:', e);
48113
throw new BadRequestException(e.message);
49114
}
50115
}
51116

52117
@Post('/signin')
53-
signin(@Body() signInDto: SignInDto): Promise<SignInResponseDto> {
54-
return this.authService.signin(signInDto);
118+
async signin(@Body() signInDto: SignInDto): Promise<SignInResponseDto> {
119+
try {
120+
return await this.authService.signin(signInDto);
121+
} catch (e) {
122+
throw new BadRequestException(e.message);
123+
}
55124
}
56125

57126
@Post('/refresh')
@@ -76,6 +145,7 @@ export class AuthController {
76145
try {
77146
await this.authService.deleteUser(user.email);
78147
} catch (e) {
148+
console.error('Delete error:', e);
79149
throw new BadRequestException(e.message);
80150
}
81151

apps/backend/src/auth/auth.service.ts

Lines changed: 80 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Injectable } from '@nestjs/common';
22
import {
3+
AdminConfirmSignUpCommand,
34
AdminDeleteUserCommand,
45
AdminInitiateAuthCommand,
56
AttributeType,
@@ -41,7 +42,10 @@ export class AuthService {
4142
// Hash key is the Cognito client secret, message is username + client ID
4243
// Username value depends on the command
4344
// (see https://docs.aws.amazon.com/cognito/latest/developerguide/signing-up-users-in-your-app.html#cognito-user-pools-computing-secret-hash)
44-
calculateHash(username: string): string {
45+
calculateHash(username: string): string | undefined {
46+
if (!this.clientSecret) {
47+
return undefined;
48+
}
4549
const hmac = createHmac('sha256', this.clientSecret);
4650
hmac.update(username + CognitoAuthConfig.clientId);
4751
return hmac.digest('base64');
@@ -55,14 +59,32 @@ export class AuthService {
5559

5660
// TODO need error handling
5761
const { Users } = await this.providerClient.send(listUsersCommand);
58-
return Users[0].Attributes;
62+
return Users?.[0]?.Attributes || [];
63+
}
64+
65+
async listAllUsers() {
66+
const listUsersCommand = new ListUsersCommand({
67+
UserPoolId: CognitoAuthConfig.userPoolId,
68+
});
69+
70+
const { Users } = await this.providerClient.send(listUsersCommand);
71+
return Users || [];
72+
}
73+
74+
async adminConfirmUser(email: string): Promise<void> {
75+
const confirmCommand = new AdminConfirmSignUpCommand({
76+
UserPoolId: CognitoAuthConfig.userPoolId,
77+
Username: email,
78+
});
79+
80+
await this.providerClient.send(confirmCommand);
5981
}
6082

6183
async signup(
6284
{ firstName, lastName, email, password }: SignUpDto,
6385
status: Status = Status.STANDARD,
6486
): Promise<boolean> {
65-
// Needs error handling
87+
console.log(`Attempting signup for: ${email} with status: ${status}`);
6688
const signUpCommand = new SignUpCommand({
6789
ClientId: CognitoAuthConfig.clientId,
6890
SecretHash: this.calculateHash(email),
@@ -73,17 +95,21 @@ export class AuthService {
7395
Name: 'name',
7496
Value: `${firstName} ${lastName}`,
7597
},
76-
// Optional: add a custom Cognito attribute called "role" that also stores the user's status/role
77-
// If you choose to do so, you'll have to first add this custom attribute in your user pool
78-
{
79-
Name: 'custom:role',
80-
Value: status,
81-
},
98+
// Commented out as it might cause InvalidParameterException if not in User Pool
99+
// {
100+
// Name: 'custom:role',
101+
// Value: status,
102+
// },
82103
],
83104
});
84105

85-
const response = await this.providerClient.send(signUpCommand);
86-
return response.UserConfirmed;
106+
try {
107+
const response = await this.providerClient.send(signUpCommand);
108+
return response.UserConfirmed;
109+
} catch (err) {
110+
console.error('Cognito signup error:', err);
111+
throw err;
112+
}
87113
}
88114

89115
async verifyUser(email: string, verificationCode: string): Promise<void> {
@@ -98,39 +124,66 @@ export class AuthService {
98124
}
99125

100126
async signin({ email, password }: SignInDto): Promise<SignInResponseDto> {
127+
const authParameters: Record<string, string> = {
128+
USERNAME: email,
129+
PASSWORD: password,
130+
};
131+
132+
const secretHash = this.calculateHash(email);
133+
if (secretHash) {
134+
authParameters.SECRET_HASH = secretHash;
135+
}
136+
101137
const signInCommand = new AdminInitiateAuthCommand({
102138
AuthFlow: 'ADMIN_USER_PASSWORD_AUTH',
103139
ClientId: CognitoAuthConfig.clientId,
104140
UserPoolId: CognitoAuthConfig.userPoolId,
105-
AuthParameters: {
106-
USERNAME: email,
107-
PASSWORD: password,
108-
SECRET_HASH: this.calculateHash(email),
109-
},
141+
AuthParameters: authParameters,
110142
});
111143

112-
const response = await this.providerClient.send(signInCommand);
113-
114-
return {
115-
accessToken: response.AuthenticationResult.AccessToken,
116-
refreshToken: response.AuthenticationResult.RefreshToken,
117-
idToken: response.AuthenticationResult.IdToken,
118-
};
144+
try {
145+
const response = await this.providerClient.send(signInCommand);
146+
147+
return {
148+
accessToken: response.AuthenticationResult.AccessToken,
149+
refreshToken: response.AuthenticationResult.RefreshToken,
150+
idToken: response.AuthenticationResult.IdToken,
151+
};
152+
} catch (err: unknown) {
153+
if (
154+
err &&
155+
typeof err === 'object' &&
156+
'name' in err &&
157+
err.name === 'UserNotConfirmedException'
158+
) {
159+
throw new Error(
160+
'Your account is not confirmed yet. Please ask an admin to confirm your registration.',
161+
);
162+
}
163+
console.error('Signin error:', err);
164+
throw err;
165+
}
119166
}
120167

121168
// Refresh token hash uses a user's sub (unique ID), not their username (typically their email)
122169
async refreshToken({
123170
refreshToken,
124171
userSub,
125172
}: RefreshTokenDto): Promise<SignInResponseDto> {
173+
const authParameters: Record<string, string> = {
174+
REFRESH_TOKEN: refreshToken,
175+
};
176+
177+
const secretHash = this.calculateHash(userSub);
178+
if (secretHash) {
179+
authParameters.SECRET_HASH = secretHash;
180+
}
181+
126182
const refreshCommand = new AdminInitiateAuthCommand({
127183
AuthFlow: 'REFRESH_TOKEN_AUTH',
128184
ClientId: CognitoAuthConfig.clientId,
129185
UserPoolId: CognitoAuthConfig.userPoolId,
130-
AuthParameters: {
131-
REFRESH_TOKEN: refreshToken,
132-
SECRET_HASH: this.calculateHash(userSub),
133-
},
186+
AuthParameters: authParameters,
134187
});
135188

136189
const response = await this.providerClient.send(refreshCommand);
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const CognitoAuthConfig = {
2-
userPoolId: 'USER POOL ID HERE',
3-
clientId: 'CLIENT ID HERE',
4-
region: 'us-east-2',
2+
userPoolId: process.env.COGNITO_USER_POOL_ID,
3+
clientId: process.env.COGNITO_CLIENT_ID,
4+
region: process.env.COGNITO_REGION || 'us-east-2',
55
};
66

77
export default CognitoAuthConfig;

0 commit comments

Comments
 (0)