add basic repo setup, with user creation

This commit is contained in:
mrjvs 2023-10-28 16:49:02 +02:00
parent 9166c37aea
commit 94e1f9ebe1
26 changed files with 5762 additions and 374 deletions

1
.docker/development/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
config.env

View File

@ -0,0 +1,35 @@
# how to use docker development?
## How to setup?
1. have docker installed
2. create a `config.env` in `/.docker/development`. inspire its contents from `example.config.env`
## how to run?
1. while in directory `/.docker/development` run `docker compose up -d`
1.1 if running first time in docker, make sure you have no node_modules folder present in `/`.
## not working? try this:
1. while in directory `/.docker/development` run `docker compose down -v`
2. remove `node_modules` directory in `/` if it exists
3. remove `.env` and `config.json` file in `/` if any of them exist
4. while in directory `/.docker/development` run `docker compose up -d --build`
## how to stop?
1. while in directory `/.docker/development` run `docker compose down`
> NOTE: if you want also delete all saved data for a full reset, run `docker compose down -v` instead
## how do I access the terminal for the backend service?
make sure the docker services are running, then run `docker attach mw_backend-1`.
this will appear to show nothing at first, but all new logs will show up,
and anything you type in the terminal now affect the backend service.
> Warning: doing CTRL+C will shut down the backend service, it will not kick your terminal back to its original shell.
## how do I read logs?
1. while in directory `/.docker/development` run `docker compose ps`
2. note the name of the service you want to see the logs of
3. while in directory `/.docker/development` run `docker compose logs <NAME>`. fill in the name of the service without the brackets.
## Exposed ports
- http://localhost:8081 - backend API
- http://localhost:8082 - postgres web UI
- postgres://localhost:5432 - postgres

View File

@ -0,0 +1,47 @@
version: '3.8'
name: "mw_backend"
services:
# required services
postgres:
image: postgres
ports:
- '5432:5432'
environment:
POSTGRES_PASSWORD: postgres
volumes:
- 'postgres_data:/var/lib/postgresql/data'
# custom services
backend:
stdin_open: true
tty: true
build:
dockerfile: ./dev.Dockerfile
context: ../../
volumes:
- '../../:/app'
env_file:
- './config.env'
ports:
- '8081:8080'
depends_on:
- postgres
environment:
- WAIT_HOSTS=postgres:5432
# util services
pgweb:
image: sosedoff/pgweb
ports:
- "8082:8081"
links:
- postgres:postgres
environment:
- DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
depends_on:
- postgres
volumes:
postgres_data:

View File

@ -0,0 +1 @@
MWB_USE_PRESETS=dev,dockerdev

View File

@ -4,3 +4,4 @@ config.json
dist
.git
.vscode
.docker

View File

@ -1,4 +1,4 @@
FROM node:18-alpine
FROM node:20-alpine
WORKDIR /app
# install dependencies
@ -11,6 +11,6 @@ RUN npm run build
# start server
EXPOSE 80
ENV CONF_SERVER__PORT=80
ENV MWB_SERVER__PORT=80
ENV NODE_ENV=production
CMD ["npm", "run", "start"]

View File

@ -1,2 +1,11 @@
# backend
Backend for movie-web
## Todo list
- [ ] endpoint to consume provider metrics
- [ ] register endpoint
- [ ] remove user endpoint
- [ ] ratelimits for all endpoints
- [ ] metrics for all http requests
- [ ] session CRUD endpoints
- [ ] data save endpoints

9
dev.Dockerfile Normal file
View File

@ -0,0 +1,9 @@
FROM node:20-alpine
WORKDIR /app
# wait script for development
ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.7.3/wait /wait
RUN chmod +x /wait
VOLUME [ "/app" ]
CMD npm i && /wait && npm run dev

4663
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -32,6 +32,8 @@
},
"dependencies": {
"@fastify/cors": "^8.3.0",
"@mikro-orm/core": "^5.9.0",
"@mikro-orm/postgresql": "^5.9.0",
"fastify": "^4.21.0",
"fastify-type-provider-zod": "^1.1.9",
"neat-config": "^2.0.0",

View File

@ -9,4 +9,7 @@ export const devFragment: FragmentSchema = {
format: 'pretty',
debug: true,
},
postgres: {
syncSchema: true,
},
};

View File

@ -0,0 +1,7 @@
import { FragmentSchema } from '@/config/fragments/types';
export const dockerFragment: FragmentSchema = {
postgres: {
connection: 'postgres://postgres:postgres@postgres:5432/postgres',
},
};

View File

@ -1,9 +1,11 @@
import { devFragment } from '@/config/fragments/dev';
import { dockerFragment } from '@/config/fragments/docker';
import { configSchema } from '@/config/schema';
import { createConfigLoader } from 'neat-config';
const fragments = {
dev: devFragment,
dockerdev: dockerFragment,
};
export const conf = createConfigLoader()

View File

@ -26,4 +26,16 @@ export const configSchema = z.object({
debug: z.coerce.boolean().default(false),
})
.default({}),
postgres: z.object({
// connection URL for postgres database
connection: z.string(),
// run all migrations on boot of the application
migrateOnBoot: z.coerce.boolean().default(false),
// try to sync the schema on boot, useful for development
// will always keep the database schema in sync with the connected database
// it is extremely destructive, do not use it EVER in production
syncSchema: z.coerce.boolean().default(false),
}),
});

31
src/db/models/User.ts Normal file
View File

@ -0,0 +1,31 @@
import { Entity, PrimaryKey, Property, types } from '@mikro-orm/core';
import { randomUUID } from 'crypto';
@Entity({ tableName: 'users' })
export class User {
@PrimaryKey({ name: 'id', type: 'uuid' })
id: string = randomUUID();
@Property({ type: 'date' })
createdAt: Date = new Date();
@Property({ type: 'text' })
name!: string;
@Property({ name: 'permissions', type: types.array })
roles: string[] = [];
}
export interface UserDTO {
id: string;
roles: string[];
createdAt: string;
}
export function formatUser(user: User): UserDTO {
return {
id: user.id,
roles: user.roles,
createdAt: user.createdAt.toISOString(),
};
}

View File

@ -1,4 +1,5 @@
import { setupFastify } from '@/modules/fastify';
import { setupMikroORM } from '@/modules/mikro';
import { scopedLogger } from '@/services/logger';
const log = scopedLogger('mw-backend');
@ -9,6 +10,7 @@ async function bootstrap(): Promise<void> {
});
await setupFastify();
await setupMikroORM();
log.info(`App setup, ready to accept connections`, {
evt: 'success',

View File

@ -1,6 +1,6 @@
import { helloRouter } from '@/routes/hello';
import { manageAuthRouter } from '@/routes/auth/manage';
import { FastifyInstance } from 'fastify';
export async function setupRoutes(app: FastifyInstance) {
app.register(helloRouter);
app.register(manageAuthRouter.register);
}

View File

@ -0,0 +1,44 @@
import { conf } from '@/config';
import { scopedLogger } from '@/services/logger';
import { PostgreSqlDriver } from '@mikro-orm/postgresql';
import { MikroORM } from '@mikro-orm/core';
import { createORM } from './orm';
const log = scopedLogger('orm');
let orm: MikroORM<PostgreSqlDriver> | null = null;
export function getORM() {
if (!orm) throw new Error('ORM not set');
return orm;
}
export async function setupMikroORM() {
log.info(`Connecting to postgres`, { evt: 'connecting' });
const mikro = await createORM(conf.postgres.connection, (msg) =>
log.info(msg),
);
if (conf.postgres.syncSchema) {
const generator = mikro.getSchemaGenerator();
try {
await generator.updateSchema();
} catch {
try {
await generator.clearDatabase();
await generator.updateSchema();
} catch {
await generator.clearDatabase();
await generator.dropSchema();
await generator.updateSchema();
}
}
}
if (conf.postgres.migrateOnBoot) {
const migrator = mikro.getMigrator();
await migrator.up();
}
orm = mikro;
log.info(`Connected to postgres - ORM is setup!`, { evt: 'success' });
}

17
src/modules/mikro/orm.ts Normal file
View File

@ -0,0 +1,17 @@
import { MikroORM, PostgreSqlDriver } from '@mikro-orm/postgresql';
import path from 'path';
export async function createORM(url: string, log: (msg: string) => void) {
return await MikroORM.init<PostgreSqlDriver>({
type: 'postgresql',
clientUrl: url,
entities: ['./models/**/*.js'],
entitiesTs: ['./models/**/*.ts'],
baseDir: path.join(__dirname, '../../db'),
migrations: {
pathTs: './migrations/**/*.ts',
path: './migrations/**/*.ts',
},
logger: log,
});
}

25
src/routes/auth/manage.ts Normal file
View File

@ -0,0 +1,25 @@
import { User, formatUser } from '@/db/models/User';
import { handle } from '@/services/handler';
import { makeRouter } from '@/services/router';
import { z } from 'zod';
const registerSchema = z.object({
name: z.string().max(500).min(1),
device: z.string().max(500).min(1),
});
export const manageAuthRouter = makeRouter((app) => {
app.post(
'/auth/register',
{ schema: { body: registerSchema } },
handle(({ em, body }) => {
const user = new User();
user.name = body.name;
em.persistAndFlush(user);
return {
user: formatUser(user),
};
}),
);
});

View File

@ -1,7 +0,0 @@
import { FastifyPluginAsync } from 'fastify';
export const helloRouter: FastifyPluginAsync = async (app) => {
app.get('/ping', (req, res) => {
res.send('pong!');
});
};

5
src/services/access.ts Normal file
View File

@ -0,0 +1,5 @@
export const roles = {
ADMIN: 'ADMIN', // has access to admin endpoints
} as const;
export type Roles = (typeof roles)[keyof typeof roles];

5
src/services/auth.ts Normal file
View File

@ -0,0 +1,5 @@
import { Roles } from '@/services/access';
export function assertHasRole(_role: Roles) {
throw new Error('requires role');
}

127
src/services/handler.ts Normal file
View File

@ -0,0 +1,127 @@
import { getORM } from '@/modules/mikro';
import { EntityManager } from '@mikro-orm/postgresql';
import {
ContextConfigDefault,
FastifyBaseLogger,
FastifyReply,
FastifyRequest,
FastifySchema,
RawReplyDefaultExpression,
RawRequestDefaultExpression,
RawServerBase,
RawServerDefault,
RouteGenericInterface,
RouteHandlerMethod,
} from 'fastify';
import { ZodTypeProvider } from 'fastify-type-provider-zod';
import { ResolveFastifyReplyReturnType } from 'fastify/types/type-provider';
export type RequestContext<
RawServer extends RawServerBase = RawServerDefault,
RawRequest extends
RawRequestDefaultExpression<RawServer> = RawRequestDefaultExpression<RawServer>,
RawReply extends
RawReplyDefaultExpression<RawServer> = RawReplyDefaultExpression<RawServer>,
RouteGeneric extends RouteGenericInterface = RouteGenericInterface,
ContextConfig = ContextConfigDefault,
SchemaCompiler extends FastifySchema = FastifySchema,
Logger extends FastifyBaseLogger = FastifyBaseLogger,
> = {
req: FastifyRequest<
RouteGeneric,
RawServer,
RawRequest,
SchemaCompiler,
ZodTypeProvider,
ContextConfig,
Logger
>;
res: FastifyReply<
RawServer,
RawRequest,
RawReply,
RouteGeneric,
ContextConfig,
SchemaCompiler,
ZodTypeProvider
>;
body: FastifyRequest<
RouteGeneric,
RawServer,
RawRequest,
SchemaCompiler,
ZodTypeProvider,
ContextConfig,
Logger
>['body'];
params: FastifyRequest<
RouteGeneric,
RawServer,
RawRequest,
SchemaCompiler,
ZodTypeProvider,
ContextConfig,
Logger
>['params'];
query: FastifyRequest<
RouteGeneric,
RawServer,
RawRequest,
SchemaCompiler,
ZodTypeProvider,
ContextConfig,
Logger
>['query'];
em: EntityManager;
};
export function handle<
RawServer extends RawServerBase = RawServerDefault,
RawRequest extends
RawRequestDefaultExpression<RawServer> = RawRequestDefaultExpression<RawServer>,
RawReply extends
RawReplyDefaultExpression<RawServer> = RawReplyDefaultExpression<RawServer>,
RouteGeneric extends RouteGenericInterface = RouteGenericInterface,
ContextConfig = ContextConfigDefault,
SchemaCompiler extends FastifySchema = FastifySchema,
Logger extends FastifyBaseLogger = FastifyBaseLogger,
>(
handler: (
ctx: RequestContext<
RawServer,
RawRequest,
RawReply,
RouteGeneric,
ContextConfig,
SchemaCompiler,
Logger
>,
) => ResolveFastifyReplyReturnType<
ZodTypeProvider,
SchemaCompiler,
RouteGeneric
>,
): RouteHandlerMethod<
RawServer,
RawRequest,
RawReply,
RouteGeneric,
ContextConfig,
SchemaCompiler,
ZodTypeProvider,
Logger
> {
const reqHandler: any = async (req: any, res: any) => {
res.send(
await handler({
req,
res,
body: req.body,
params: req.params,
query: req.query,
em: getORM().em.fork(),
}),
);
};
return reqHandler;
}

32
src/services/router.ts Normal file
View File

@ -0,0 +1,32 @@
import {
FastifyBaseLogger,
FastifyInstance,
FastifyPluginAsync,
RawReplyDefaultExpression,
RawRequestDefaultExpression,
RawServerBase,
} from 'fastify';
import { ZodTypeProvider } from 'fastify-type-provider-zod';
export type Instance = FastifyInstance<
RawServerBase,
RawRequestDefaultExpression<RawServerBase>,
RawReplyDefaultExpression<RawServerBase>,
FastifyBaseLogger,
ZodTypeProvider
>;
export type RegisterPlugin = FastifyPluginAsync<
Record<never, never>,
RawServerBase,
ZodTypeProvider
>;
export function makeRouter(cb: (app: Instance) => void): {
register: RegisterPlugin;
} {
return {
register: async (app) => {
cb(app);
},
};
}

1041
yarn.lock

File diff suppressed because it is too large Load Diff