@harperfast/template-react-ts-studio 0.10.0 → 0.11.0
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.
- package/AGENTS.md +1 -1
- package/package.json +1 -1
- package/skills/checking-authentication.md +165 -31
package/AGENTS.md
CHANGED
|
@@ -15,4 +15,4 @@ This repository contains "skills" that guide AI agents in developing Harper appl
|
|
|
15
15
|
- [TypeScript Type Stripping](skills/typescript-type-stripping.md): Using TypeScript directly without build tools via Node.js Type Stripping.
|
|
16
16
|
- [Handling Binary Data](skills/handling-binary-data.md): How to store and serve binary data like images or MP3s.
|
|
17
17
|
- [Serving Web Content](skills/serving-web-content): Two ways to serve web content from a Harper application.
|
|
18
|
-
- [Checking Authentication](skills/checking-authentication.md): How to use
|
|
18
|
+
- [Checking Authentication](skills/checking-authentication.md): How to use sessions to verify user identity and roles.
|
package/package.json
CHANGED
|
@@ -1,52 +1,186 @@
|
|
|
1
|
-
# Checking Authentication in HarperDB
|
|
1
|
+
# Checking Authentication and Sessions in this app (HarperDB Resources)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This project uses HarperDB Resource classes with cookie-backed sessions to enforce authentication and authorization. Below are the concrete patterns used across resources like `resources/me.ts`, `resources/signIn.ts`, `resources/signOut.ts`, and protected endpoints such as `resources/downloadAlbumArtwork.ts`.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Important: To actually enforce sessions (even on localhost), HarperDB must not auto-authorize the local loopback as the superuser. Ensure the following in your HarperDB config (see `~/hdb/harperdb-config.yaml`):
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```yaml
|
|
8
|
+
authentication:
|
|
9
|
+
authorizeLocal: false
|
|
10
|
+
enableSessions: true
|
|
11
|
+
```
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
With `authorizeLocal: true`, all local requests would be auto-authorized as the superuser, bypassing these checks. We keep it off to ensure session checks are respected.
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
## Public vs protected routes
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
- Public resources explicitly allow the method via `allowRead()` or `allowCreate()` or similara returning `true`.
|
|
18
|
+
- Protected handlers perform checks up-front using the current session user (and, for privileged actions, a helper like `ensureSuperUser`).
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
## Creating a session (sign in)
|
|
21
|
+
|
|
22
|
+
Pattern from `resources/signIn.ts`:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { type Context, type RequestTargetOrId, Resource } from 'harperdb';
|
|
26
|
+
|
|
27
|
+
export interface LoginBody {
|
|
28
|
+
username?: string;
|
|
29
|
+
password?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class SignIn extends Resource {
|
|
33
|
+
static loadAsInstance = false;
|
|
19
34
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
allowCreate() {
|
|
36
|
+
return true; // public endpoint
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async post(_target: RequestTargetOrId, data: LoginBody) {
|
|
40
|
+
const errors: string[] = [];
|
|
41
|
+
if (!data.username) { errors.push('username'); }
|
|
42
|
+
if (!data.password) { errors.push('password'); }
|
|
43
|
+
if (errors.length) {
|
|
44
|
+
return new Response(
|
|
45
|
+
`Please include the ${errors.join(' and ')} in your request.`,
|
|
46
|
+
{ status: 400 },
|
|
47
|
+
);
|
|
26
48
|
}
|
|
27
49
|
|
|
28
|
-
|
|
50
|
+
const context = this.getContext() as Context as any;
|
|
51
|
+
try {
|
|
52
|
+
await context.login(data.username, data.password);
|
|
53
|
+
} catch {
|
|
54
|
+
return new Response('Please check your credentials and try again.', {
|
|
55
|
+
status: 403,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return new Response('Welcome back!', { status: 200 });
|
|
29
59
|
}
|
|
30
60
|
}
|
|
31
61
|
```
|
|
32
62
|
|
|
33
|
-
|
|
63
|
+
- `context.login(username, password)` creates a session and sets the session cookie on the response.
|
|
64
|
+
- Missing fields → `400 Bad Request`.
|
|
65
|
+
- Invalid credentials → `403 Forbidden` (don’t leak which field was wrong).
|
|
66
|
+
|
|
67
|
+
## Reading the current user (who am I)
|
|
68
|
+
|
|
69
|
+
Pattern from `resources/me.ts`:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { Resource } from 'harperdb';
|
|
34
73
|
|
|
35
|
-
|
|
74
|
+
export class Me extends Resource {
|
|
75
|
+
static loadAsInstance = false;
|
|
36
76
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
77
|
+
allowRead() {
|
|
78
|
+
return true; // public: returns data only if session exists
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async get() {
|
|
82
|
+
const user = this.getCurrentUser?.();
|
|
83
|
+
if (!user?.username) {
|
|
84
|
+
// Not signed in; return 200 with no body to make polling simple on the client
|
|
85
|
+
return new Response(null, { status: 200 });
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
active: user.active,
|
|
89
|
+
role: user.role,
|
|
90
|
+
username: user.username,
|
|
91
|
+
created: user.__createdtime__,
|
|
92
|
+
updated: user.__updatedtime__,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
44
95
|
}
|
|
45
96
|
```
|
|
46
97
|
|
|
47
|
-
|
|
98
|
+
- Use `this.getCurrentUser?.()` to access the session’s user (if any).
|
|
99
|
+
- It may be `undefined` when unauthenticated. Handle that case explicitly.
|
|
100
|
+
|
|
101
|
+
## Destroying a session (sign out)
|
|
102
|
+
|
|
103
|
+
Pattern from `resources/signOut.ts`:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { type Context, Resource } from 'harperdb';
|
|
107
|
+
|
|
108
|
+
export class SignOut extends Resource {
|
|
109
|
+
static loadAsInstance = false;
|
|
110
|
+
|
|
111
|
+
allowCreate() {
|
|
112
|
+
return true; // public endpoint, but requires a session to actually act
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async post() {
|
|
116
|
+
const user = this.getCurrentUser();
|
|
117
|
+
if (!user?.username) {
|
|
118
|
+
return new Response('Not signed in.', { status: 401 });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const context = this.getContext() as Context as any;
|
|
122
|
+
await context.session?.delete?.(context.session.id);
|
|
123
|
+
return new Response('Signed out successfully.', { status: 200 });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
- If the request has no session, return `401 Unauthorized`.
|
|
129
|
+
- Otherwise delete the current session via `context.session.delete(sessionId)`.
|
|
130
|
+
|
|
131
|
+
## Protecting privileged endpoints
|
|
132
|
+
|
|
133
|
+
For admin-only or otherwise privileged actions, use the `ensureSuperUser` helper with the current user.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import {
|
|
137
|
+
RequestTarget,
|
|
138
|
+
type RequestTargetOrId,
|
|
139
|
+
Resource,
|
|
140
|
+
tables,
|
|
141
|
+
} from 'harperdb';
|
|
142
|
+
import { ensureSuperUser } from './common/ensureSuperUser.ts';
|
|
143
|
+
|
|
144
|
+
export class DoSomethingInteresting extends Resource {
|
|
145
|
+
static loadAsInstance = false;
|
|
146
|
+
|
|
147
|
+
async get(target: RequestTargetOrId) {
|
|
148
|
+
ensureSuperUser(this.getCurrentUser());
|
|
149
|
+
// … fetch and return the artwork
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`ensureSuperUser` throws a `403` if the user is not a super user:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import { type User } from 'harperdb';
|
|
158
|
+
|
|
159
|
+
export function ensureSuperUser(user: User | undefined) {
|
|
160
|
+
if (!user?.role?.permission?.super_user) {
|
|
161
|
+
let error = new Error('You do not have permission to perform this action.');
|
|
162
|
+
(error as any).statusCode = 403;
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Status code conventions used here
|
|
169
|
+
|
|
170
|
+
- 200: Successful operation. For `GET /me`, a `200` with empty body means “not signed in”.
|
|
171
|
+
- 400: Missing required fields (e.g., username/password on sign-in).
|
|
172
|
+
- 401: No current session for an action that requires one (e.g., sign out when not signed in).
|
|
173
|
+
- 403: Authenticated but not authorized (bad credentials on login attempt, or insufficient privileges).
|
|
174
|
+
|
|
175
|
+
## Client considerations
|
|
176
|
+
|
|
177
|
+
- Sessions are cookie-based; the server handles setting and reading the cookie via HarperDB. If you make cross-origin requests, ensure the appropriate `credentials` mode and CORS settings.
|
|
178
|
+
- If developing locally, double-check the server config still has `authentication.authorizeLocal: false` to avoid accidental superuser bypass.
|
|
48
179
|
|
|
49
|
-
|
|
180
|
+
## Quick checklist
|
|
50
181
|
|
|
51
|
-
-
|
|
52
|
-
-
|
|
182
|
+
- [ ] Public endpoints explicitly `allowRead`/`allowCreate` as needed.
|
|
183
|
+
- [ ] Sign-in uses `context.login` and handles 400/403 correctly.
|
|
184
|
+
- [ ] Protected routes call `ensureSuperUser(this.getCurrentUser())` (or another role check) before doing work.
|
|
185
|
+
- [ ] Sign-out verifies a session and deletes it.
|
|
186
|
+
- [ ] `authentication.authorizeLocal` is `false` and `enableSessions` is `true` in HarperDB config.
|