@harperfast/template-react-ts-studio 0.10.0 → 0.12.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 +2 -2
- package/package.json +1 -1
- package/resources/README.md +11 -0
- package/schemas/README.md +11 -0
- package/skills/checking-authentication.md +165 -31
- package/resources/examplePeople.ts +0 -20
- package/resources/exampleSocket.ts +0 -41
- package/resources/greeting.ts +0 -29
- package/schemas/examplePeople.graphql +0 -7
- /package/skills/{adding-tables.md → adding-tables-with-schemas.md} +0 -0
package/AGENTS.md
CHANGED
|
@@ -4,7 +4,7 @@ This repository contains "skills" that guide AI agents in developing Harper appl
|
|
|
4
4
|
|
|
5
5
|
## Available Skills
|
|
6
6
|
|
|
7
|
-
- [Adding Tables](skills/adding-tables.md): Learn how to define schemas and enable automatic REST APIs for your database tables.
|
|
7
|
+
- [Adding Tables with Schemas](skills/adding-tables-with-schemas.md): Learn how to define schemas and enable automatic REST APIs for your database tables with schema .graphql files in Harper.
|
|
8
8
|
- [Automatic REST APIs](skills/automatic-rest-apis.md): Details on the CRUD endpoints automatically generated for exported tables.
|
|
9
9
|
- [Querying REST APIs](skills/querying-rest-apis.md): How to use filters, operators, sorting, and pagination in REST requests.
|
|
10
10
|
- [Programmatic Table Requests](skills/programmatic-table-requests.md): How to use filters, operators, sorting, and pagination in programmatic table requests.
|
|
@@ -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
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Resources
|
|
2
|
+
|
|
3
|
+
The [schemas you define in .GraphQL files](../skills/adding-tables-with-schemas.md) will [automatically stand-up REST APIs](../skills/automatic-rest-apis.md).
|
|
4
|
+
|
|
5
|
+
But you can [extend your tables with custom logic](../skills/extending-tables.md) and [create your own resources](../skills/custom-resources.md) in this directory.
|
|
6
|
+
|
|
7
|
+
## Want to read more?
|
|
8
|
+
|
|
9
|
+
Check out the rest of the "skills" documentation!
|
|
10
|
+
|
|
11
|
+
[AGENTS.md](../AGENTS.md)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Schemas
|
|
2
|
+
|
|
3
|
+
Your schemas are defined in `.graphql` files within this `schemas` directory. These files contain the structure and types for your database tables, allowing Harper to automatically generate REST APIs for CRUD operations.
|
|
4
|
+
|
|
5
|
+
Take a look at the [Adding Tables with Schemas](../skills/adding-tables-with-schemas.md) to learn more!
|
|
6
|
+
|
|
7
|
+
## Want to read more?
|
|
8
|
+
|
|
9
|
+
Check out the rest of the "skills" documentation!
|
|
10
|
+
|
|
11
|
+
[AGENTS.md](../AGENTS.md)
|
|
@@ -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.
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { type RequestTargetOrId, tables } from 'harperdb';
|
|
2
|
-
|
|
3
|
-
export interface ExamplePerson {
|
|
4
|
-
id: string;
|
|
5
|
-
name: string;
|
|
6
|
-
tag: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export class ExamplePeople extends tables.ExamplePeople<ExamplePerson> {
|
|
10
|
-
// we can define our own custom POST handler
|
|
11
|
-
async post(target: RequestTargetOrId, newRecord: Omit<ExamplePerson, 'id'>) {
|
|
12
|
-
// do something with the incoming content;
|
|
13
|
-
return super.post(target, newRecord);
|
|
14
|
-
}
|
|
15
|
-
// or custom GET handler
|
|
16
|
-
async get(target: RequestTargetOrId): Promise<ExamplePerson> {
|
|
17
|
-
// we can modify this resource before returning
|
|
18
|
-
return super.get(target);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { type IterableEventQueue, RequestTarget, Resource, tables } from 'harperdb';
|
|
2
|
-
|
|
3
|
-
interface ExampleSocketRecord {
|
|
4
|
-
id: string;
|
|
5
|
-
type?: 'get' | 'put';
|
|
6
|
-
name: string;
|
|
7
|
-
tag: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export class ExampleSocket extends Resource<ExampleSocketRecord> {
|
|
11
|
-
static loadAsInstance = false;
|
|
12
|
-
|
|
13
|
-
// This customizes handling the socket connections; tables can have this method too!
|
|
14
|
-
async *connect(
|
|
15
|
-
target: RequestTarget,
|
|
16
|
-
incomingMessages: IterableEventQueue<ExampleSocketRecord>,
|
|
17
|
-
): AsyncIterable<ExampleSocketRecord> {
|
|
18
|
-
const subscription = await tables.ExamplePeople.subscribe(target);
|
|
19
|
-
if (!incomingMessages) {
|
|
20
|
-
// Server sent events, no incoming messages!
|
|
21
|
-
// Subscribe to changes to the table.
|
|
22
|
-
return subscription;
|
|
23
|
-
}
|
|
24
|
-
for await (let message of incomingMessages) {
|
|
25
|
-
const { type, id, name, tag } = message;
|
|
26
|
-
switch (type) {
|
|
27
|
-
case 'get':
|
|
28
|
-
const loaded = await tables.ExamplePeople.get(id);
|
|
29
|
-
yield {
|
|
30
|
-
type: 'get',
|
|
31
|
-
id,
|
|
32
|
-
...(loaded ? loaded : {}),
|
|
33
|
-
};
|
|
34
|
-
break;
|
|
35
|
-
case 'put':
|
|
36
|
-
await tables.ExamplePeople.put(id, { name, tag });
|
|
37
|
-
break;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
package/resources/greeting.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { type RecordObject, type RequestTargetOrId, Resource } from 'harperdb';
|
|
2
|
-
|
|
3
|
-
interface GreetingRecord {
|
|
4
|
-
greeting: string;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export class Greeting extends Resource<GreetingRecord> {
|
|
8
|
-
static loadAsInstance = false;
|
|
9
|
-
|
|
10
|
-
async post(target: RequestTargetOrId, newRecord: Partial<GreetingRecord & RecordObject>): Promise<GreetingRecord> {
|
|
11
|
-
return { greeting: 'Greetings, post!' };
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async get(target?: RequestTargetOrId): Promise<GreetingRecord> {
|
|
15
|
-
return { greeting: 'Greetings, get!' };
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async put(target: RequestTargetOrId, record: GreetingRecord & RecordObject): Promise<GreetingRecord> {
|
|
19
|
-
return { greeting: 'Greetings, put!' };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async patch(target: RequestTargetOrId, record: Partial<GreetingRecord & RecordObject>): Promise<GreetingRecord> {
|
|
23
|
-
return { greeting: 'Greetings, patch!' };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async delete(target: RequestTargetOrId): Promise<boolean> {
|
|
27
|
-
return true;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
## Here we can define any tables in our database. This example shows how we define a type as a table using
|
|
2
|
-
## the type name as the table name and specifying it is an "export" available in the REST and other external protocols.
|
|
3
|
-
type ExamplePeople @table @export {
|
|
4
|
-
id: ID @primaryKey # Here we define primary key (must be one)
|
|
5
|
-
name: String # we can define any other attributes here
|
|
6
|
-
tag: String @indexed # we can specify any attributes that should be indexed
|
|
7
|
-
}
|
|
File without changes
|