Skip to content

Commit 7119d3b

Browse files
scheidtdavJerryVincentjona159
authored
Feat/user registration api (#557)
* feat: add draft for port of user registration to resource route * feat: partly implement refresh token * docs: simplify contributing and add info about api routes and shared logic * feat(api): finalize user registration endpoint * fix(tests): get the tests to run be reconfiguring build steps * docs(db): readd db setup and seed scripts with README info for it * fix: wrong import of utils * refactor: remove leftover custom server stuff * fix(tests): add missing refresh token table * fix(tests): reenable remaining tests for registration * fix(ci): remove playwright and use correct node version * fix(ci): run the tests with a postgres container * feat(tests): add coverage report * fix(build): reorganize server modules to correctly split client/ server * fix(build): miss an import * fix(build): remove leftovers from custom server implementation * chore(deps): bump react-router dependencies * chore(deps): update react-router * feat/user me api (#559) * feat(api): add api routes for /users/me * fix(tests): api me PUT * feat(api): add delete me endpoint * feat(api): add root route (#560) * start * new commit * tested docs * added a route * Added API Docs * modified * removed unsupported packages * updated * Modified * script generation without using ts-node. * modified * fix: update package-lock.json * Updated (#575) * Updated README * Updated README * Removed duplicate Documentation section (#576) * Updated README * Updated README * Removed duplicate section. * Update README.md * Feat/api email and password (#561) * feat(api): add email-confirmation endpoint * feat(api): add request password reset * feat(api): add password reset * feat(api): implement resend email confirmation (without sending yet) * feat/api auth (#562) * feat(api): add email-confirmation endpoint * feat(api): add request password reset * feat(api): add password reset * feat(api): implement resend email confirmation (without sending yet) * feat(api): add sign-in, sign-out and refresh-routes to api * feat(api): implement refresh endpoint --------- Co-authored-by: jona159 <[email protected]> * feat(api): boxes for user endpoints (#573) * feat/api misc (#571) * feat(api): boxes for user endpoints * feat(api): add tags and stats route scaffold * feat(api): implement tags route * refactor: remove unnecessary imports * feat(api): implement statistics route --------- Co-authored-by: jona159 <[email protected]> * tests: disable integration related test --------- Co-authored-by: JerryVincent <[email protected]> Co-authored-by: Jerry Vincent <[email protected]> Co-authored-by: jona159 <[email protected]>
1 parent 855bc43 commit 7119d3b

File tree

89 files changed

+39714
-17038
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+39714
-17038
lines changed

.env.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
33
PG_CLIENT_SSL="false"
44

55
SESSION_SECRET="super-duper-s3cret"
6+
JWT_SECRET="OH GOD THIS IS SO INSECURE PLS CHANGE ME" # should be at least 32 characters
7+
JWT_ALGORITHM="HS256"
8+
JWT_ISSUER="https://api.my-opensensemap.com" # usually the base url of the api
9+
JWT_VALIDITY_MS=3600000 # 1 hour
10+
REFRESH_TOKEN_SECRET="I ALSO WANT TO BE CHANGED"
11+
REFRESH_TOKEN_ALGORITHM="sha256"
12+
REFRESH_TOKEN_VALIDITY_MS=604800000 # 1 week
613

714
MAPBOX_ACCESS_TOKEN=""
815
MAPBOX_GEOCODING_API="https://api.mapbox.com/geocoding/v5/mapbox.places/"

.github/workflows/deploy.yml

Lines changed: 15 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ permissions:
1717

1818
jobs:
1919
lint:
20-
name: ESLint
20+
name: Lint
2121
runs-on: ubuntu-latest
2222
steps:
2323
- name: ⬇️ Checkout repo
@@ -28,7 +28,7 @@ jobs:
2828
with:
2929
cache: npm
3030
cache-dependency-path: ./package.json
31-
node-version: 20
31+
node-version-file: .nvmrc
3232

3333
- name: 📥 Install deps
3434
run: npm install
@@ -48,7 +48,7 @@ jobs:
4848
with:
4949
cache: npm
5050
cache-dependency-path: ./package.json
51-
node-version: 20
51+
node-version-file: .nvmrc
5252

5353
- name: 📥 Install deps
5454
run: npm install
@@ -57,27 +57,7 @@ jobs:
5757
run: npm run typecheck --if-present
5858

5959
vitest:
60-
name: ⚡ Vitest
61-
runs-on: ubuntu-latest
62-
steps:
63-
- name: ⬇️ Checkout repo
64-
uses: actions/checkout@v4
65-
66-
- name: ⎔ Setup node
67-
uses: actions/setup-node@v4
68-
with:
69-
cache: npm
70-
cache-dependency-path: ./package.json
71-
node-version: 20
72-
73-
- name: 📥 Install deps
74-
run: npm install
75-
76-
- name: ⚡ Run vitest
77-
run: npm run test -- --coverage
78-
79-
playwright:
80-
name: 🎭 Playwright
60+
name: ⚡ Test
8161
runs-on: ubuntu-latest
8262
steps:
8363
- name: ⬇️ Checkout repo
@@ -91,14 +71,15 @@ jobs:
9171
with:
9272
cache: npm
9373
cache-dependency-path: ./package.json
94-
node-version: 20
74+
node-version-file: .nvmrc
9575

9676
- name: 📥 Install deps
9777
run: npm install
9878

9979
- name: 🐳 Docker compose
80+
# we need a postgres container for runnign the tests
10081
# the sleep is just there to give time for postgres to get started
101-
run: docker compose -f docker-compose.ci.yml up -d && sleep 60
82+
run: docker compose -f docker-compose.ci.yml up -d && sleep 30
10283
env:
10384
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/postgres"
10485

@@ -109,23 +90,17 @@ jobs:
10990
max_attempts: 5
11091
retry_wait_seconds: 45
11192
retry_on: error
112-
command: npm run drizzle:migrate
113-
114-
- name: ⚙️ Build
115-
run: npm run build
93+
command: npm run db:migrate
11694

117-
- name: Install Playwright Browsers
118-
run: npx playwright install --with-deps
95+
- name: ⚡ Run vitest
96+
run: npm run test:coverage
11997

120-
- name: Run Playwright tests
121-
run: npx playwright test
98+
- name: 🧐 Report Coverage
99+
uses: davelosert/vitest-coverage-report-action@v2
122100

123-
- uses: actions/upload-artifact@v4
124-
if: ${{ !cancelled() }}
125-
with:
126-
name: playwright-report
127-
path: playwright-report/
128-
retention-days: 30
101+
- name: 🗑️ Cleanup
102+
if: always()
103+
run: docker compose down
129104

130105
build:
131106
name: 🐳 Build

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
node_modules
66

77
/build
8-
/server-build
98
/public/build
9+
/public/openapi.json
1010
.env
1111
.cache
1212

CONTRIBUTING.md

Lines changed: 0 additions & 8 deletions
This file was deleted.

README.md

Lines changed: 139 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
![openSenseMap](https://github.com/openSenseMap/frontend/blob/dev/public/openSenseMap.png)
22

3-
This repository contains the code of the new *openSenseMap* frontend running at [https://beta.opensensemap.org](https://beta.opensensemap.org).
3+
This repository contains the code of the new _openSenseMap_ frontend running at [https://beta.opensensemap.org](https://beta.opensensemap.org).
44

5-
Originally, the *openSenseMap* was built as part of the bachelor thesis of [@mpfeil](https://github.com/mpfeil) at the ifgi (Institute for Geoinformatics, University of Münster). Between 2016 and 2022 development was partly funded by the German Ministry of Education and Research (BMBF) in the projets senseBox and senseBox Pro. This version has been developed by [@mpfeil](https://github.com/mpfeil) and [@freds-dev](https://github.com/freds-dev).
5+
Originally, the _openSenseMap_ was built as part of the bachelor thesis of [@mpfeil](https://github.com/mpfeil) at the ifgi (Institute for Geoinformatics, University of Münster). Between 2016 and 2022 development was partly funded by the German Ministry of Education and Research (BMBF) in the projets senseBox and senseBox Pro. This version has been developed by [@mpfeil](https://github.com/mpfeil) and [@freds-dev](https://github.com/freds-dev).
66

77
<img width="1438" alt="Screenshot OSeM" src="https://github.com/user-attachments/assets/a7bf16fb-44a2-4a21-9c0f-d4bf431ab9b5">
88

9-
109
## Project setup
1110

1211
If you do need to set the project up locally yourself, feel free to follow these instructions:
1312

1413
### System Requirements
14+
1515
- [Node.js](https://nodejs.org/) >= 22.0.0
1616
- [npm](https://npmjs.com/) >= 8.18.0
1717
- [git](https://git-scm.com/) >= 2.38.0
@@ -21,11 +21,11 @@ If you do need to set the project up locally yourself, feel free to follow these
2121

2222
You can configure the API endpoint and/or map tiles using the following environmental variables:
2323

24-
| ENV | Default value |
25-
| --------- | ----------------- |
26-
| OSEM_API_URL | https://api.testing.opensensemap.org |
27-
| DATABASE_URL | <YOUR_POSTGRES_URL> |
28-
| MAPBOX_ACCESS_TOKEN | <YOUR_MAPBOX_ACCESS_TOKEN> |
24+
| ENV | Default value |
25+
| ------------------- | ------------------------------------ |
26+
| OSEM_API_URL | https://api.testing.opensensemap.org |
27+
| DATABASE_URL | <YOUR_POSTGRES_URL> |
28+
| MAPBOX_ACCESS_TOKEN | <YOUR_MAPBOX_ACCESS_TOKEN> |
2929

3030
You can create a copy of `.env.example`, rename it to `.env` and set the values.
3131

@@ -34,13 +34,140 @@ You can create a copy of `.env.example`, rename it to `.env` and set the values.
3434
1. Clone the repo: `git clone https://github.com/openSenseMap/frontend`
3535
2. Copy `.env.example` into `.env`
3636
3. Run `npm install` to install dependencies
37-
4. Run `npm run docker` to start the docker container running your local postgres DB
38-
5. Run `npm run build`
39-
6. Run `npm run dev` to start the local server
37+
4. Optionally run `docker compose up` to start a docker container running your local postgres DB
38+
- If it is the first time doing this, you may need to bootstrap the database by running `npm run db:setup`
39+
- If you want some example data run `npm run db:seed`. **WARNING**: Do not run this on a production database. It will delete all existing data.
40+
5. Run `npm run dev` to start the local server
4041

4142
### Contributing
4243

43-
We welcome all kind of constructive contributions to this project. Please have a look at [CONTRIBUTING](.github/CONTRIBUTING.md) if you want to do so.
44+
We welcome all kind of constructive contributions to this project.
45+
If you are planning to implement a new feature or change something, please create an issue first.
46+
47+
Afterwards follow these steps:
48+
49+
1. Fork this repository
50+
2. Create your feature branch (`git checkout -b my-new-feature`)
51+
3. Make and commit your changes
52+
4. Push to the branch (`git push origin my-new-feature`)
53+
5. Create a new pull request against this repository's `dev` branch, linking your issue.
54+
55+
#### How the repository is organized
56+
57+
```shell
58+
├── app # main directory where most of the application code lives
59+
│ ├── components # reusable/ general purpose components
60+
│ ├── lib
61+
│ ├── models
62+
│ ├── routes # app/ api routes
63+
│ ├── schema
64+
│ └── utils
65+
├── db # code for seeding/ migration of database
66+
├── drizzle # database migrations
67+
├── other
68+
├── public # static assets
69+
├── server
70+
├── tests # automated tests, same structure as the app/ folder with tests placed according to the files they test
71+
│ ├── routes # tests for (resource/ api) routes
72+
├── types
73+
├── ...
74+
```
75+
76+
#### openSenseMap API
77+
78+
The api is implemented using [Remix resource routes](https://remix.run/docs/en/main/guides/resource-routes).
79+
Resource routes may not export a component but only [loaders](https://remix.run/docs/en/main/route/loader) (for `GET` requests) and [actions](https://remix.run/docs/en/main/route/action) (for `POST`, `PUT`, `DELETE` etc) and therefore live in `.ts` (not `.tsx`) files.
80+
All resource routes start with `api` (e.g. `api.user.ts` for `/api/user`).
81+
82+
The api logic is shared with the frontend. Therefore api routes should not implement the actual business logic of an endpoint. They are responsible for checking the request for validity and for transforming the data into the correct output format.
83+
Logic should be implemented in corresponding services, that may be used by loaders/ actions of page routes that access the same functionality.
84+
85+
For example: User registration is possible from both the api and the frontend. The logic for it is implemented in `lib/user.service.ts` and it is being used by both `api.user.ts` (resource route) as well as `explore.register.tsx` (page route), preventing duplication of common logic while also providing the flexibility to adjust the outputs to the needs of the respective use case.
86+
87+
##### Documenting an API Route
88+
89+
The [swaggerJsdoc Library](https://www.npmjs.com/package/swagger-jsdoc) reads the JSDoc-annotated source code in the api-routes and generates an openAPI(Swagger) specification and is rendered using [Swaggger UI](https://swagger.io/tools/swagger-ui/). The [JSDoc annotations](https://github.com/Surnet/swagger-jsdoc) is usually added before the loader or action function in the API Routes. The documentation will then be automatically generated from the JSDoc annotations in all the api routes. When testing the api during development do not forget to change the server to [Development Server](http://localhost:3000).
90+
91+
##### JSDoc Example
92+
93+
Here's an example of how to document an API route using JSDoc annotations:
94+
95+
```javascript
96+
/**
97+
* @openapi
98+
* /api/users/{id}:
99+
* get:
100+
* summary: Get user by ID
101+
* description: Retrieve a single user by their unique identifier
102+
* tags:
103+
* - Users
104+
* parameters:
105+
* - in: path
106+
* name: id
107+
* required: true
108+
* description: Unique identifier of the user
109+
* schema:
110+
* type: string
111+
* example: "12345"
112+
* responses:
113+
* 200:
114+
* description: User retrieved successfully
115+
* content:
116+
* application/json:
117+
* schema:
118+
* type: object
119+
* properties:
120+
* id:
121+
* type: string
122+
* example: "12345"
123+
* name:
124+
* type: string
125+
* example: "John Doe"
126+
* email:
127+
* type: string
128+
* example: "john.doe@example.com"
129+
* createdAt:
130+
* type: string
131+
* format: date-time
132+
* example: "2023-01-15T10:30:00Z"
133+
* 404:
134+
* description: User not found
135+
* content:
136+
* application/json:
137+
* schema:
138+
* type: object
139+
* properties:
140+
* error:
141+
* type: string
142+
* example: "User not found"
143+
* 500:
144+
* description: Internal server error
145+
*/
146+
export async function loader({ params }) {
147+
const { id } = params;
148+
149+
try {
150+
const user = await getUserById(id);
151+
if (!user) {
152+
throw new Response("User not found", { status: 404 });
153+
}
154+
return Response.json({ user });
155+
} catch (error) {
156+
throw new Response("Internal server error", { status: 500 });
157+
}
158+
}
159+
```
160+
161+
This JSDoc annotation will automatically generate comprehensive API documentation including endpoint details, parameters, response schemas, and example values.
162+
163+
164+
#### Testing
165+
166+
Tests are placed in the [tests/](./tests/) folder whose structure is similar to the [app/](./app/) folder.
167+
When adding a test, use the same name as the file you are testing but change the file extension to `.spec.ts`, e.g. when creating tests for [`./app/utils`](./app/utils.ts) name the test file [`./tests/utils.spec.ts`](./tests/utils.spec.ts).
168+
169+
To run the tests, make sure you have a working database connection (e.g. by running `docker compose up` with the corresponding environment variables to use your local database).
170+
Then simply run `npm test`.
44171

45172
## License
46173

app/entry.server.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import { resolve } from "node:path";
22
import { PassThrough } from "stream";
33
import { createReadableStreamFromReadable } from "@react-router/node";
44
import { createInstance } from "i18next";
5-
import I18NexFsBackend from "i18next-fs-backend";
5+
import backend from "i18next-fs-backend/cjs"; // Even though unintuitive, cjs is what we want https://github.com/i18next/i18next-fs-backend/issues/57
66
import { isbot } from "isbot";
77
import { renderToPipeableStream } from "react-dom/server";
88
import { I18nextProvider, initReactI18next } from "react-i18next";
9-
import { ServerRouter, type EntryContext } from "react-router";
9+
import { ServerRouter, type EntryContext } from "react-router";
1010
import i18nextOptions from "./i18next-options"; // our i18n configuration file
1111
import i18next from "./i18next.server";
1212
import { getEnv, init } from "./utils/env.server";
@@ -40,7 +40,7 @@ export default async function handleRequest(
4040
// completely unique instance and not share any state.
4141
await instance
4242
.use(initReactI18next) // Tell our instance to use react-i18next
43-
.use(I18NexFsBackend) // Setup our backend
43+
.use(backend) // Setup our backend
4444
.init({
4545
...i18nextOptions, // Spreact the configuration
4646
lng, // The locale we detected above

app/i18next.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { resolve } from "node:path";
2-
import Backend from "i18next-fs-backend";
2+
import Backend from "i18next-fs-backend/cjs"; // Even though unintuitive, cjs is what we want https://github.com/i18next/i18next-fs-backend/issues/57
33
import { RemixI18Next } from "remix-i18next/server";
44
import { i18nCookie } from "./cookies";
55
import i18nextOptions from "./i18next-options";

app/lib/device-service.server.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { sql } from "drizzle-orm";
2+
import { drizzleClient } from "~/db.server";
3+
4+
/**
5+
* Queries the database for a distinct list of all tags known to the
6+
* application across all registered devices.
7+
* @returns An array containing the names of the tags or an empty array if there are none
8+
*/
9+
export const getTags = async function findTags() {
10+
const tags = await drizzleClient.execute(
11+
sql`SELECT array_agg(DISTINCT u.val) tags FROM device d CROSS JOIN LATERAL unnest(d.tags) AS u(val);`,
12+
);
13+
return tags[0]?.tags ?? [];
14+
};

0 commit comments

Comments
 (0)