diff --git a/.changeset/all-goats-hammer.md b/.changeset/all-goats-hammer.md new file mode 100644 index 000000000..babf1c0e4 --- /dev/null +++ b/.changeset/all-goats-hammer.md @@ -0,0 +1,5 @@ +--- +'@baseplate-dev/sync': patch +--- + +Add support for globs in onlyIfChanged command filter diff --git a/.changeset/brave-parts-see.md b/.changeset/brave-parts-see.md new file mode 100644 index 000000000..6385fdffa --- /dev/null +++ b/.changeset/brave-parts-see.md @@ -0,0 +1,5 @@ +--- +'@baseplate-dev/plugin-auth': patch +--- + +Support front and backend of new auth plugin diff --git a/.changeset/easy-donuts-smoke.md b/.changeset/easy-donuts-smoke.md new file mode 100644 index 000000000..3af970c33 --- /dev/null +++ b/.changeset/easy-donuts-smoke.md @@ -0,0 +1,5 @@ +--- +'@baseplate-dev/project-builder-web': patch +--- + +Fix bug in app creation that was re-using the same ID diff --git a/.changeset/plenty-actors-help.md b/.changeset/plenty-actors-help.md new file mode 100644 index 000000000..913648beb --- /dev/null +++ b/.changeset/plenty-actors-help.md @@ -0,0 +1,7 @@ +--- +'@baseplate-dev/project-builder-web': patch +'@baseplate-dev/react-generators': patch +'@baseplate-dev/ui-components': patch +--- + +Upgrade react-hook-form to 7.60.0 diff --git a/package.json b/package.json index 34f1db453..cc04fb512 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "watch:tsc:root": "tsc -b tsconfig.build.json --preserveWatchOutput -w" }, "lint-staged": { - "*.(js|ts|tsx|jsx|json|md|mdx|css|scss|yaml|yml|toml|html|svg|yml|yaml|json)": "prettier --write" + "*.(js|ts|tsx|jsx|json|md|mdx|css|scss|yaml|yml|toml|html|yml|yaml|json)": "prettier --write" }, "dependencies": { "@changesets/cli": "2.28.1" diff --git a/packages/fastify-generators/src/generators/auth/password-hasher-service/extractor.json b/packages/fastify-generators/src/generators/auth/password-hasher-service/extractor.json index f0eefc856..3383483fa 100644 --- a/packages/fastify-generators/src/generators/auth/password-hasher-service/extractor.json +++ b/packages/fastify-generators/src/generators/auth/password-hasher-service/extractor.json @@ -9,7 +9,6 @@ "importMapProviders": {}, "pathRootRelativePath": "{module-root}/services/password-hasher.service.ts", "projectExports": { "createPasswordHash": {}, "verifyPasswordHash": {} }, - "template": "password-hasher.service.ts", "variables": {} } } diff --git a/packages/fastify-generators/src/generators/auth/password-hasher-service/generated/index.ts b/packages/fastify-generators/src/generators/auth/password-hasher-service/generated/index.ts index 8b1228848..eca933d8d 100644 --- a/packages/fastify-generators/src/generators/auth/password-hasher-service/generated/index.ts +++ b/packages/fastify-generators/src/generators/auth/password-hasher-service/generated/index.ts @@ -1,9 +1,11 @@ import { AUTH_PASSWORD_HASHER_SERVICE_PATHS } from './template-paths.js'; +import { AUTH_PASSWORD_HASHER_SERVICE_RENDERERS } from './template-renderers.js'; import { AUTH_PASSWORD_HASHER_SERVICE_IMPORTS } from './ts-import-providers.js'; import { AUTH_PASSWORD_HASHER_SERVICE_TEMPLATES } from './typed-templates.js'; export const AUTH_PASSWORD_HASHER_SERVICE_GENERATED = { imports: AUTH_PASSWORD_HASHER_SERVICE_IMPORTS, paths: AUTH_PASSWORD_HASHER_SERVICE_PATHS, + renderers: AUTH_PASSWORD_HASHER_SERVICE_RENDERERS, templates: AUTH_PASSWORD_HASHER_SERVICE_TEMPLATES, }; diff --git a/packages/fastify-generators/src/generators/auth/password-hasher-service/generated/template-renderers.ts b/packages/fastify-generators/src/generators/auth/password-hasher-service/generated/template-renderers.ts new file mode 100644 index 000000000..7cafb741c --- /dev/null +++ b/packages/fastify-generators/src/generators/auth/password-hasher-service/generated/template-renderers.ts @@ -0,0 +1,59 @@ +import type { RenderTsTemplateFileActionInput } from '@baseplate-dev/core-generators'; +import type { BuilderAction } from '@baseplate-dev/sync'; + +import { typescriptFileProvider } from '@baseplate-dev/core-generators'; +import { createGeneratorTask, createProviderType } from '@baseplate-dev/sync'; + +import { AUTH_PASSWORD_HASHER_SERVICE_PATHS } from './template-paths.js'; +import { AUTH_PASSWORD_HASHER_SERVICE_TEMPLATES } from './typed-templates.js'; + +export interface AuthPasswordHasherServiceRenderers { + passwordHasherService: { + render: ( + options: Omit< + RenderTsTemplateFileActionInput< + typeof AUTH_PASSWORD_HASHER_SERVICE_TEMPLATES.passwordHasherService + >, + 'destination' | 'importMapProviders' | 'template' + >, + ) => BuilderAction; + }; +} + +const authPasswordHasherServiceRenderers = + createProviderType( + 'auth-password-hasher-service-renderers', + ); + +const authPasswordHasherServiceRenderersTask = createGeneratorTask({ + dependencies: { + paths: AUTH_PASSWORD_HASHER_SERVICE_PATHS.provider, + typescriptFile: typescriptFileProvider, + }, + exports: { + authPasswordHasherServiceRenderers: + authPasswordHasherServiceRenderers.export(), + }, + run({ paths, typescriptFile }) { + return { + providers: { + authPasswordHasherServiceRenderers: { + passwordHasherService: { + render: (options) => + typescriptFile.renderTemplateFile({ + template: + AUTH_PASSWORD_HASHER_SERVICE_TEMPLATES.passwordHasherService, + destination: paths.passwordHasherService, + ...options, + }), + }, + }, + }, + }; + }, +}); + +export const AUTH_PASSWORD_HASHER_SERVICE_RENDERERS = { + provider: authPasswordHasherServiceRenderers, + task: authPasswordHasherServiceRenderersTask, +}; diff --git a/packages/fastify-generators/src/generators/core/error-handler-service/templates/src/utils/zod.ts b/packages/fastify-generators/src/generators/core/error-handler-service/templates/src/utils/zod.ts index 3b1dafd76..190ae607f 100644 --- a/packages/fastify-generators/src/generators/core/error-handler-service/templates/src/utils/zod.ts +++ b/packages/fastify-generators/src/generators/core/error-handler-service/templates/src/utils/zod.ts @@ -20,8 +20,14 @@ import { BadRequestError } from './http-errors.js'; */ export const handleZodRequestValidationError = (error: unknown): never => { if (error instanceof ZodError) { - throw new BadRequestError('Validation failed', 'ZOD_VALIDATION_ERROR', { - errors: error.errors, + const formattedErrors = error.errors.map((err) => ({ + path: err.path.join('.'), + message: err.message, + code: err.code, + })); + + throw new BadRequestError('Validation failed', 'VALIDATION_ERROR', { + errors: formattedErrors, }); } diff --git a/packages/fastify-generators/src/generators/pothos/pothos-prisma-find-query/pothos-prisma-find-query.generator.ts b/packages/fastify-generators/src/generators/pothos/pothos-prisma-find-query/pothos-prisma-find-query.generator.ts index 5c7240373..a0bddeff5 100644 --- a/packages/fastify-generators/src/generators/pothos/pothos-prisma-find-query/pothos-prisma-find-query.generator.ts +++ b/packages/fastify-generators/src/generators/pothos/pothos-prisma-find-query/pothos-prisma-find-query.generator.ts @@ -137,7 +137,7 @@ export const pothosPrismaFindQueryGenerator = createGenerator({ );`, { QUERY_EXPORT: `${lowerFirstModelName}Query`, - BUILDER: 'builder', + BUILDER: pothosTypesFile.getBuilderFragment(), QUERY_NAME: quot(lowerFirstModelName), OPTIONS: TsCodeUtils.mergeFragmentsAsObject(options, { disableSort: true, diff --git a/packages/project-builder-lib/package.json b/packages/project-builder-lib/package.json index 6be411be7..9b5bf1596 100644 --- a/packages/project-builder-lib/package.json +++ b/packages/project-builder-lib/package.json @@ -58,7 +58,7 @@ "immer": "10.1.1", "inflection": "3.0.0", "react": "catalog:", - "react-hook-form": "7.56.3", + "react-hook-form": "7.60.0", "zod": "catalog:", "zustand": "5.0.3" }, diff --git a/packages/project-builder-lib/src/definition/project-definition-container.ts b/packages/project-builder-lib/src/definition/project-definition-container.ts index 28def60d7..090ec1423 100644 --- a/packages/project-builder-lib/src/definition/project-definition-container.ts +++ b/packages/project-builder-lib/src/definition/project-definition-container.ts @@ -18,7 +18,7 @@ import { import { deserializeSchemaWithTransformedReferences, fixRefDeletions, - serializeSchemaFromRefPayload, + serializeSchema, } from '#src/references/index.js'; import { createProjectDefinitionSchema } from '#src/schema/index.js'; @@ -100,9 +100,15 @@ export class ProjectDefinitionContainer { * @returns The serialized contents of the project definition */ toSerializedContents(): string { - return stringifyPrettyStable( - serializeSchemaFromRefPayload(this.refPayload), + const serializedContents = serializeSchema( + createProjectDefinitionSchema, + this.definition, + { + defaultMode: 'strip', + plugins: this.pluginStore, + }, ); + return stringifyPrettyStable(serializedContents); } /** diff --git a/packages/project-builder-lib/src/plugins/metadata/types.ts b/packages/project-builder-lib/src/plugins/metadata/types.ts index f5ff6fd2e..d53ea02be 100644 --- a/packages/project-builder-lib/src/plugins/metadata/types.ts +++ b/packages/project-builder-lib/src/plugins/metadata/types.ts @@ -63,6 +63,12 @@ export const pluginMetadataSchema = z.object({ dependencies: z.array(pluginSpecDependencySchema).optional(), }) .optional(), + /** + * Whether the plugin should be hidden in the project builder UI + * + * (It can be used once in the definition but cannot be added) + */ + hidden: z.boolean().optional(), }); export type PluginMetadata = z.infer; diff --git a/packages/project-builder-lib/src/references/extract-definition-refs.ts b/packages/project-builder-lib/src/references/extract-definition-refs.ts index bd6ef4109..490cc1eb6 100644 --- a/packages/project-builder-lib/src/references/extract-definition-refs.ts +++ b/packages/project-builder-lib/src/references/extract-definition-refs.ts @@ -124,6 +124,15 @@ export function extractDefinitionRefs(value: T): ZodRefPayload { const cleanData = extractDefinitionRefsRecursive(value, refContext, []); + // Simple sanity check to make sure we don't have duplicate IDs + const idSet = new Set(); + for (const entity of refContext.entitiesWithNameResolver) { + if (idSet.has(entity.id)) { + throw new Error(`Duplicate ID found: ${entity.id}`); + } + idSet.add(entity.id); + } + return { data: cleanData as T, references: refContext.references, diff --git a/packages/project-builder-lib/src/references/extract-definition-refs.unit.test.ts b/packages/project-builder-lib/src/references/extract-definition-refs.unit.test.ts index 0365fb6a7..64cf1b8b6 100644 --- a/packages/project-builder-lib/src/references/extract-definition-refs.unit.test.ts +++ b/packages/project-builder-lib/src/references/extract-definition-refs.unit.test.ts @@ -8,7 +8,10 @@ import type { ZodRefContext } from './extract-definition-refs.js'; import { createDefinitionEntityNameResolver } from './definition-ref-builder.js'; import { deserializeSchemaWithTransformedReferences } from './deserialize-schema.js'; -import { extractDefinitionRefsRecursive } from './extract-definition-refs.js'; +import { + extractDefinitionRefs, + extractDefinitionRefsRecursive, +} from './extract-definition-refs.js'; import { DefinitionReferenceMarker, REF_ANNOTATIONS_MARKER_SYMBOL, @@ -860,5 +863,125 @@ describe('extract-definition-refs', () => { }); }); }); + + describe('Duplicate ID Detection', () => { + it('should throw error when duplicate entity IDs are found', () => { + const testEntityType = new DefinitionEntityType('test', 'test'); + + const inputWithDuplicateIds = { + entity1: { + id: 'test:duplicate-id', + name: 'Entity One', + [REF_ANNOTATIONS_MARKER_SYMBOL]: { + entities: [ + { + type: testEntityType, + getNameResolver: () => ({ resolveName: () => 'Entity One' }), + }, + ], + references: [], + contextPaths: [], + }, + }, + entity2: { + id: 'test:duplicate-id', // Same ID as entity1 + name: 'Entity Two', + [REF_ANNOTATIONS_MARKER_SYMBOL]: { + entities: [ + { + type: testEntityType, + getNameResolver: () => ({ resolveName: () => 'Entity Two' }), + }, + ], + references: [], + contextPaths: [], + }, + }, + }; + + expect(() => extractDefinitionRefs(inputWithDuplicateIds)).toThrow( + 'Duplicate ID found: test:duplicate-id', + ); + }); + }); + }); + + describe('Non-Recursive Function Tests (extractDefinitionRefs)', () => { + it('should process simple object with entity annotations', () => { + const testEntityType = new DefinitionEntityType('test', 'test'); + + const input = { + id: 'test:test-id', + name: 'Test Entity', + [REF_ANNOTATIONS_MARKER_SYMBOL]: { + entities: [ + { + type: testEntityType, + getNameResolver: () => ({ resolveName: () => 'Test Entity' }), + }, + ], + references: [], + contextPaths: [], + }, + }; + + const result = extractDefinitionRefs(input); + + expect(result.data).toEqual({ + id: 'test:test-id', + name: 'Test Entity', + }); + expect(result.entitiesWithNameResolver).toHaveLength(1); + expect(result.entitiesWithNameResolver[0]).toMatchObject({ + id: 'test:test-id', + type: testEntityType, + path: [], + idPath: ['id'], + }); + expect(result.references).toHaveLength(0); + }); + + it('should process object with both entities and references', () => { + const testEntityType = new DefinitionEntityType('test', 'test'); + const refEntityType = new DefinitionEntityType('ref', 'ref'); + + const input = { + entity: { + id: 'test:entity-id', + name: 'Test Entity', + [REF_ANNOTATIONS_MARKER_SYMBOL]: { + entities: [ + { + type: testEntityType, + getNameResolver: () => ({ resolveName: () => 'Test Entity' }), + }, + ], + references: [], + contextPaths: [], + }, + }, + ref: new DefinitionReferenceMarker('ref:ref-id', { + type: refEntityType, + onDelete: 'RESTRICT', + }), + }; + + const result = extractDefinitionRefs(input); + + expect(result.data).toEqual({ + entity: { + id: 'test:entity-id', + name: 'Test Entity', + }, + ref: 'ref:ref-id', + }); + expect(result.entitiesWithNameResolver).toHaveLength(1); + expect(result.references).toHaveLength(1); + expect(result.references[0]).toMatchObject({ + type: refEntityType, + path: ['ref'], + onDelete: 'RESTRICT', + }); + }); }); }); diff --git a/packages/project-builder-lib/src/schema/creator/extend-parser-context-with-defaults.ts b/packages/project-builder-lib/src/schema/creator/extend-parser-context-with-defaults.ts new file mode 100644 index 000000000..d64775a05 --- /dev/null +++ b/packages/project-builder-lib/src/schema/creator/extend-parser-context-with-defaults.ts @@ -0,0 +1,73 @@ +import { z } from 'zod'; + +import type { DefinitionSchemaCreatorOptions } from './types.js'; + +function isEmpty(value: unknown): boolean { + if (Array.isArray(value)) { + return value.length === 0; + } + if (typeof value === 'object' && value !== null) { + return Object.values(value).every((val) => val === undefined); + } + return value === false || value === ''; +} + +export type WithDefaultType = ( + schema: T, + defaultValue: z.infer, +) => z.ZodEffects< + z.ZodOptional, + z.output>, + z.input> +>; + +/** + * Extends the parser context with default value handling functionality. + * + * @param options - The schema creator options containing the defaultMode + * @returns An object containing the withDefault method + */ +export function extendParserContextWithDefaults( + options: DefinitionSchemaCreatorOptions, +): { + withDefault: WithDefaultType; +} { + const mode = options.defaultMode ?? 'populate'; + + return { + withDefault: function withDefault( + schema: T, + defaultValue: z.infer, + ): z.ZodEffects, z.output>, z.input> { + // Auto-add .optional() to the schema + const optionalSchema = schema.optional(); + + switch (mode) { + case 'populate': { + // Use preprocess to inject defaults before validation + return z.preprocess((value: z.input>) => { + if (value === undefined) { + return defaultValue; + } + return value; + }, optionalSchema); + } + case 'strip': { + // Use transform to remove values matching defaults after validation + return optionalSchema.transform((value) => { + if (isEmpty(value)) { + return undefined; + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- it's typed to a generic + return value; + }); + } + case 'preserve': { + // Return schema with .optional() added + // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- it's typed to a generic + return optionalSchema.transform((x) => x); + } + } + }, + }; +} diff --git a/packages/project-builder-lib/src/schema/creator/schema-creator.ts b/packages/project-builder-lib/src/schema/creator/schema-creator.ts index e07f21116..5b5971cba 100644 --- a/packages/project-builder-lib/src/schema/creator/schema-creator.ts +++ b/packages/project-builder-lib/src/schema/creator/schema-creator.ts @@ -8,12 +8,15 @@ import type { DefinitionSchemaParserContext, } from './types.js'; +import { extendParserContextWithDefaults } from './extend-parser-context-with-defaults.js'; + export function createDefinitionSchemaParserContext( options: DefinitionSchemaCreatorOptions, ): DefinitionSchemaParserContext { return { ...options, ...extendParserContextWithRefs(options), + ...extendParserContextWithDefaults(options), }; } diff --git a/packages/project-builder-lib/src/schema/creator/types.ts b/packages/project-builder-lib/src/schema/creator/types.ts index 12dc4acd0..7dfafc6f9 100644 --- a/packages/project-builder-lib/src/schema/creator/types.ts +++ b/packages/project-builder-lib/src/schema/creator/types.ts @@ -7,6 +7,8 @@ import type { WithRefType, } from '#src/references/extend-parser-context-with-refs.js'; +import type { WithDefaultType } from './extend-parser-context-with-defaults.js'; + /** * Options for creating a definition schema. */ @@ -23,6 +25,16 @@ export interface DefinitionSchemaCreatorOptions { * to convert the parsed data to the correct type. */ transformReferences?: boolean; + /** + * How to handle default values in the schema. + * + * - 'populate': Ensure defaults are present (useful for React Hook Form) + * - 'strip': Remove values that match their defaults (useful for clean JSON serialization) + * - 'preserve': Keep values as-is without transformation + * + * @default 'populate' + */ + defaultMode?: 'populate' | 'strip' | 'preserve'; } export interface DefinitionSchemaParserContext { @@ -34,6 +46,10 @@ export interface DefinitionSchemaParserContext { * If true, the schema will be transformed to include references. */ transformReferences?: boolean; + /** + * How to handle default values in the schema. + */ + defaultMode?: 'populate' | 'strip' | 'preserve'; /** * Adds a reference to the schema. */ @@ -46,6 +62,13 @@ export interface DefinitionSchemaParserContext { * Provides access to the reference builder functions for the schema. */ withRefBuilder: WithRefBuilder; + /** + * Wraps a schema with default value handling based on the defaultMode. + * - 'populate': Uses preprocess to ensure defaults are present + * - 'strip': Uses transform to remove values matching defaults + * - 'preserve': Returns schema unchanged + */ + withDefault: WithDefaultType; } export type DefinitionSchemaCreator = ( diff --git a/packages/project-builder-lib/src/schema/models/graphql.ts b/packages/project-builder-lib/src/schema/models/graphql.ts index 0399bf1fc..33a776478 100644 --- a/packages/project-builder-lib/src/schema/models/graphql.ts +++ b/packages/project-builder-lib/src/schema/models/graphql.ts @@ -12,107 +12,100 @@ import { } from './types.js'; const createRoleArray = definitionSchema((ctx) => - z - .array( + ctx.withDefault( + z.array( ctx.withRef({ type: authRoleEntityType, onDelete: 'DELETE', }), - ) - .optional(), + ), + [], + ), ); export const createModelGraphqlSchema = definitionSchema((ctx) => z.object({ - objectType: z - .object({ - enabled: z.boolean().default(false), - fields: z.array( - ctx.withRef({ - type: modelScalarFieldEntityType, - onDelete: 'DELETE', - parentPath: { context: 'model' }, - }), + objectType: ctx.withDefault( + z.object({ + enabled: ctx.withDefault(z.boolean(), false), + fields: ctx.withDefault( + z.array( + ctx.withRef({ + type: modelScalarFieldEntityType, + onDelete: 'DELETE', + parentPath: { context: 'model' }, + }), + ), + [], ), - localRelations: z - .array( + localRelations: ctx.withDefault( + z.array( ctx.withRef({ type: modelLocalRelationEntityType, onDelete: 'DELETE', parentPath: { context: 'model' }, }), - ) - .optional(), - foreignRelations: z - .array( + ), + [], + ), + foreignRelations: ctx.withDefault( + z.array( ctx.withRef({ type: modelForeignRelationEntityType, onDelete: 'DELETE', parentPath: { context: 'model' }, }), - ) - .optional(), - }) - .default({ - enabled: false, - fields: [], + ), + [], + ), }), - queries: z - .object({ - get: z - .object({ - enabled: z.boolean().optional(), + {}, + ), + queries: ctx.withDefault( + z.object({ + get: ctx.withDefault( + z.object({ + enabled: ctx.withDefault(z.boolean(), false), roles: createRoleArray(ctx), - }) - .optional(), - list: z - .object({ - enabled: z.boolean().optional(), + }), + {}, + ), + list: ctx.withDefault( + z.object({ + enabled: ctx.withDefault(z.boolean(), false), roles: createRoleArray(ctx), - }) - .optional(), - }) - .default({ - get: { - enabled: false, - roles: [], - }, - list: { - enabled: false, - roles: [], - }, + }), + {}, + ), }), - mutations: z - .object({ - create: z - .object({ - enabled: z.boolean().optional(), + {}, + ), + mutations: ctx.withDefault( + z.object({ + create: ctx.withDefault( + z.object({ + enabled: ctx.withDefault(z.boolean(), false), roles: createRoleArray(ctx), - }) - .default({ - enabled: false, - roles: [], }), - update: z - .object({ - enabled: z.boolean().optional(), + {}, + ), + update: ctx.withDefault( + z.object({ + enabled: ctx.withDefault(z.boolean(), false), roles: createRoleArray(ctx), - }) - .default({ - enabled: false, - roles: [], }), - delete: z - .object({ - enabled: z.boolean().optional(), + {}, + ), + delete: ctx.withDefault( + z.object({ + enabled: ctx.withDefault(z.boolean(), false), roles: createRoleArray(ctx), - }) - .default({ - enabled: false, - roles: [], }), - }) - .default({}), + {}, + ), + }), + {}, + ), }), ); diff --git a/packages/project-builder-lib/src/tools/model-merger/model-merger.ts b/packages/project-builder-lib/src/tools/model-merger/model-merger.ts index 111fd975e..4de5482d3 100644 --- a/packages/project-builder-lib/src/tools/model-merger/model-merger.ts +++ b/packages/project-builder-lib/src/tools/model-merger/model-merger.ts @@ -172,11 +172,14 @@ function serializeModelMergerModelInput( ...input.graphql, objectType: { ...input.graphql?.objectType, - fields: input.graphql?.objectType?.fields.map(fieldNameFromId) ?? [], + fields: input.graphql?.objectType?.fields?.map(fieldNameFromId) ?? [], localRelations: - input.graphql?.objectType?.localRelations?.map(relationNameFromId), + input.graphql?.objectType?.localRelations?.map(relationNameFromId) ?? + [], foreignRelations: - input.graphql?.objectType?.foreignRelations?.map(relationNameFromId), + input.graphql?.objectType?.foreignRelations?.map( + relationNameFromId, + ) ?? [], }, }, }; @@ -273,7 +276,7 @@ function deserializeModelMergerModelInput( objectType: { ...inputWithIds.graphql?.objectType, fields: - inputWithIds.graphql?.objectType?.fields.map((fieldRef) => + inputWithIds.graphql?.objectType?.fields?.map((fieldRef) => resolveLocalFieldName(fieldRef), ) ?? [], localRelations: @@ -369,6 +372,15 @@ export function createModelMergerResult( }; } +export function doesModelMergerResultsHaveChanges( + results: Record< + keyof ModelMergerModelsInput, + ModelMergerModelDiffResult | undefined + >, +): boolean { + return Object.values(results).some((result) => result?.changes); +} + /** * Creates a model merger result for a set of models. * diff --git a/packages/project-builder-lib/src/tools/model-merger/model-merger.unit.test.ts b/packages/project-builder-lib/src/tools/model-merger/model-merger.unit.test.ts index a7423c8c1..c0803d12a 100644 --- a/packages/project-builder-lib/src/tools/model-merger/model-merger.unit.test.ts +++ b/packages/project-builder-lib/src/tools/model-merger/model-merger.unit.test.ts @@ -570,7 +570,7 @@ describe('GraphQL support', () => { current, desired, ); - expect(serializedDefinition.models[0].graphql?.objectType.fields).toEqual([ + expect(serializedDefinition.models[0].graphql?.objectType?.fields).toEqual([ 'id', 'name', ]); @@ -813,18 +813,18 @@ describe('GraphQL support', () => { (m) => m.name === 'Post', ); - expect(authorModelResult?.graphql?.objectType.fields).toEqual([ + expect(authorModelResult?.graphql?.objectType?.fields).toEqual([ 'id', 'name', ]); - expect(authorModelResult?.graphql?.objectType.foreignRelations).toEqual([ + expect(authorModelResult?.graphql?.objectType?.foreignRelations).toEqual([ 'posts', ]); - expect(postModelResult?.graphql?.objectType.fields).toEqual([ + expect(postModelResult?.graphql?.objectType?.fields).toEqual([ 'id', 'title', ]); - expect(postModelResult?.graphql?.objectType.localRelations).toEqual([ + expect(postModelResult?.graphql?.objectType?.localRelations).toEqual([ 'author', ]); }); diff --git a/packages/project-builder-server/src/compiler/backend/graphql.ts b/packages/project-builder-server/src/compiler/backend/graphql.ts index 732562480..ed9d5fbbf 100644 --- a/packages/project-builder-server/src/compiler/backend/graphql.ts +++ b/packages/project-builder-server/src/compiler/backend/graphql.ts @@ -30,15 +30,19 @@ function buildObjectTypeFile( const buildQuery = queries?.get?.enabled ?? queries?.list?.enabled; const buildMutations = - mutations?.create.enabled ?? - mutations?.update.enabled ?? - mutations?.delete.enabled; + !!mutations?.create?.enabled || + !!mutations?.update?.enabled || + !!mutations?.delete?.enabled; if (!objectType?.enabled) { return undefined; } - const { fields, localRelations = [], foreignRelations = [] } = objectType; + const { + fields = [], + localRelations = [], + foreignRelations = [], + } = objectType; return pothosTypesFileGenerator({ id: `${model.id}-object-type`, @@ -130,9 +134,9 @@ function buildMutationsFileForModel( const buildMutations = !!mutations && - (!!mutations.create.enabled || - !!mutations.update.enabled || - !!mutations.delete.enabled); + (!!mutations.create?.enabled || + !!mutations.update?.enabled || + !!mutations.delete?.enabled); if (!buildMutations) { return undefined; @@ -157,7 +161,7 @@ function buildMutationsFileForModel( id: `${model.id}-mutations`, fileName: `${kebabCase(model.name)}.mutations`, children: { - create: create.enabled + create: create?.enabled ? pothosPrismaCrudMutationGenerator({ ...sharedMutationConfig, order: 0, @@ -172,7 +176,7 @@ function buildMutationsFileForModel( }, }) : undefined, - update: update.enabled + update: update?.enabled ? pothosPrismaCrudMutationGenerator({ ...sharedMutationConfig, order: 1, @@ -187,7 +191,7 @@ function buildMutationsFileForModel( }, }) : undefined, - delete: del.enabled + delete: del?.enabled ? pothosPrismaCrudMutationGenerator({ ...sharedMutationConfig, order: 2, diff --git a/packages/project-builder-web/package.json b/packages/project-builder-web/package.json index 62d1736b7..2bc988cad 100644 --- a/packages/project-builder-web/package.json +++ b/packages/project-builder-web/package.json @@ -79,7 +79,7 @@ "react": "catalog:", "react-dom": "catalog:", "react-error-boundary": "6.0.0", - "react-hook-form": "7.56.3", + "react-hook-form": "7.60.0", "react-icons": "5.5.0", "react-timeago": "8.2.0", "semver": "^7.5.4", diff --git a/packages/project-builder-web/src/components/blocker-dialog/blocker-dialog.tsx b/packages/project-builder-web/src/components/blocker-dialog/blocker-dialog.tsx index ca1103494..d0ee0be07 100644 --- a/packages/project-builder-web/src/components/blocker-dialog/blocker-dialog.tsx +++ b/packages/project-builder-web/src/components/blocker-dialog/blocker-dialog.tsx @@ -94,8 +94,7 @@ export function BlockerDialog(): React.JSX.Element { disabled={isContinuing} variant="secondary" > - {activeBlocker.buttonContinueWithoutSaveText ?? - 'Continue without saving'} + {activeBlocker.buttonContinueWithoutSaveText ?? 'Discard changes'} ); diff --git a/packages/project-builder-web/src/routes/apps/-components/new-app-dialog.tsx b/packages/project-builder-web/src/routes/apps/-components/new-app-dialog.tsx index e875374fb..7a3a450e9 100644 --- a/packages/project-builder-web/src/routes/apps/-components/new-app-dialog.tsx +++ b/packages/project-builder-web/src/routes/apps/-components/new-app-dialog.tsx @@ -46,7 +46,7 @@ function NewAppDialog({ const { control, handleSubmit, reset } = useForm({ resolver: zodResolver(baseAppSchema), defaultValues: { - id: appEntityType.generateNewId(), + id: '', name: '', type: 'backend' as const, }, @@ -58,10 +58,17 @@ function NewAppDialog({ { label: 'Admin App', value: 'admin' }, ]; - const onSubmit = handleSubmit((data) => - saveDefinitionWithFeedback( + const onSubmit = handleSubmit((data) => { + const newId = appEntityType.generateNewId(); + return saveDefinitionWithFeedback( (draftConfig) => { - const newApps = [...draftConfig.apps, data]; + const newApps = [ + ...draftConfig.apps, + { + ...data, + id: newId, + }, + ]; draftConfig.apps = sortBy(newApps, [(app) => app.name]) as AppConfig[]; }, { @@ -70,12 +77,12 @@ function NewAppDialog({ setIsOpen(false); reset(); navigate({ - to: `/apps/edit/${appEntityType.keyFromId(data.id)}`, + to: `/apps/edit/${appEntityType.keyFromId(newId)}`, }).catch(logAndFormatError); }, }, - ), - ); + ); + }); const handleOpenChange = (newOpen: boolean): void => { setIsOpen(newOpen); diff --git a/packages/project-builder-web/src/routes/data/models/edit.$key/-components/fields/model-field-form.tsx b/packages/project-builder-web/src/routes/data/models/edit.$key/-components/fields/model-field-form.tsx index 85bebb3fb..ec0cd6930 100644 --- a/packages/project-builder-web/src/routes/data/models/edit.$key/-components/fields/model-field-form.tsx +++ b/packages/project-builder-web/src/routes/data/models/edit.$key/-components/fields/model-field-form.tsx @@ -110,7 +110,7 @@ function ModelFieldForm({ const [relationId, setRelationId] = useState(); return ( -
+
+ !plugin.hidden && !pluginConfig.some( (config) => config.packageName === plugin.packageName && diff --git a/packages/project-builder-web/src/routes/settings/template-extractor.tsx b/packages/project-builder-web/src/routes/settings/template-extractor.tsx index a822b9d3f..e4f41d8cf 100644 --- a/packages/project-builder-web/src/routes/settings/template-extractor.tsx +++ b/packages/project-builder-web/src/routes/settings/template-extractor.tsx @@ -39,7 +39,10 @@ export const Route = createFileRoute('/settings/template-extractor')({ */ function TemplateExtractorSettingsPage(): React.JSX.Element { const { definition, saveDefinitionWithFeedback } = useProjectDefinition(); - const defaultValues = definition.settings.templateExtractor; + const defaultValues = definition.settings.templateExtractor ?? { + writeMetadata: false, + fileIdRegexWhitelist: '', + }; const templateExtractorSchema = useDefinitionSchema( createTemplateExtractorSchema, ); diff --git a/packages/react-generators/src/constants/react-packages.ts b/packages/react-generators/src/constants/react-packages.ts index b32b91da2..07bd3b4f2 100644 --- a/packages/react-generators/src/constants/react-packages.ts +++ b/packages/react-generators/src/constants/react-packages.ts @@ -24,7 +24,7 @@ export const REACT_PACKAGES = { '@headlessui/react': '2.2.2', '@hookform/resolvers': '5.0.1', clsx: '2.1.1', - 'react-hook-form': '7.56.3', + 'react-hook-form': '7.60.0', 'react-icons': '5.5.0', 'react-select': '5.10.1', zustand: '5.0.3', diff --git a/packages/react-generators/src/generators/admin/admin-bull-board/admin-bull-board.generator.ts b/packages/react-generators/src/generators/admin/admin-bull-board/admin-bull-board.generator.ts index 3f8b3c8ca..290317615 100644 --- a/packages/react-generators/src/generators/admin/admin-bull-board/admin-bull-board.generator.ts +++ b/packages/react-generators/src/generators/admin/admin-bull-board/admin-bull-board.generator.ts @@ -10,10 +10,7 @@ import { import { quot } from '@baseplate-dev/utils'; import { z } from 'zod'; -import { - generatedGraphqlImportsProvider, - reactApolloProvider, -} from '#src/generators/apollo/react-apollo/index.js'; +import { generatedGraphqlImportsProvider } from '#src/generators/apollo/react-apollo/index.js'; import { reactComponentsImportsProvider } from '#src/generators/core/react-components/index.js'; import { reactConfigImportsProvider, @@ -47,7 +44,6 @@ export const adminBullBoardGenerator = createGenerator({ reactComponentsImports: reactComponentsImportsProvider, reactConfigImports: reactConfigImportsProvider, reactErrorImports: reactErrorImportsProvider, - reactApollo: reactApolloProvider, generatedGraphqlImports: generatedGraphqlImportsProvider, paths: ADMIN_ADMIN_BULL_BOARD_GENERATED.paths.provider, reactRoutes: reactRoutesProvider, @@ -57,7 +53,6 @@ export const adminBullBoardGenerator = createGenerator({ reactComponentsImports, reactConfigImports, reactErrorImports, - reactApollo, generatedGraphqlImports, paths, reactRoutes, @@ -65,8 +60,6 @@ export const adminBullBoardGenerator = createGenerator({ const routeFilePath = reactRoutes.getRouteFilePath(); return { build: async (builder) => { - reactApollo.registerGqlFile(paths.bullBoard); - await builder.apply( typescriptFile.renderTemplateFile({ template: diff --git a/packages/react-generators/src/generators/admin/admin-crud-queries/admin-crud-queries.generator.ts b/packages/react-generators/src/generators/admin/admin-crud-queries/admin-crud-queries.generator.ts index e46ded582..f54b23d08 100644 --- a/packages/react-generators/src/generators/admin/admin-crud-queries/admin-crud-queries.generator.ts +++ b/packages/react-generators/src/generators/admin/admin-crud-queries/admin-crud-queries.generator.ts @@ -321,7 +321,6 @@ export const adminCrudQueriesGenerator = createGenerator({ reactRoutes.getOutputRelativePath(), 'queries.gql', ); - reactApollo.registerGqlFile(filePath); builder.writeFile({ id: `${modelId}-crud-queries`, destination: filePath, diff --git a/packages/react-generators/src/generators/admin/admin-layout/admin-layout.generator.ts b/packages/react-generators/src/generators/admin/admin-layout/admin-layout.generator.ts index e7bd55637..ce8fd8726 100644 --- a/packages/react-generators/src/generators/admin/admin-layout/admin-layout.generator.ts +++ b/packages/react-generators/src/generators/admin/admin-layout/admin-layout.generator.ts @@ -3,7 +3,7 @@ import { createGenerator, createGeneratorTask } from '@baseplate-dev/sync'; import { quot } from '@baseplate-dev/utils'; import { z } from 'zod'; -import { reactAuthProvider } from '#src/generators/auth/index.js'; +import { reactAuthRoutesProvider } from '#src/generators/auth/index.js'; import { reactComponentsImportsProvider } from '#src/generators/core/react-components/index.js'; import { reactRoutesProvider } from '#src/providers/index.js'; @@ -43,7 +43,7 @@ export const adminLayoutGenerator = createGenerator({ dependencies: { renderers: ADMIN_ADMIN_LAYOUT_GENERATED.renderers.provider, reactRoutes: reactRoutesProvider, - reactAuth: reactAuthProvider, + reactAuth: reactAuthRoutesProvider, }, run({ renderers, reactRoutes, reactAuth }) { return { diff --git a/packages/react-generators/src/generators/apollo/react-apollo/react-apollo.generator.ts b/packages/react-generators/src/generators/apollo/react-apollo/react-apollo.generator.ts index 110cc6160..f08aef150 100644 --- a/packages/react-generators/src/generators/apollo/react-apollo/react-apollo.generator.ts +++ b/packages/react-generators/src/generators/apollo/react-apollo/react-apollo.generator.ts @@ -158,13 +158,6 @@ const [setupTask, reactApolloConfigProvider, reactApolloConfigValuesProvider] = export { reactApolloConfigProvider }; export interface ReactApolloProvider { - /** - * Register a gql file so that any changes to this file will - * trigger a regeneration of the generated graphql file - * - * @param filePath - The path to the gql file - */ - registerGqlFile(filePath: string): void; /** * Get the path to the generated graphql file * @@ -306,14 +299,9 @@ export const reactApolloGenerator = createGenerator({ }, paths, }) { - const gqlFiles: string[] = []; - return { providers: { reactApollo: { - registerGqlFile(filePath) { - gqlFiles.push(filePath); - }, getGeneratedFilePath() { return paths.graphql; }, @@ -586,7 +574,7 @@ export const reactApolloGenerator = createGenerator({ builder.addPostWriteCommand('pnpm generate', { priority: POST_WRITE_COMMAND_PRIORITY.CODEGEN, - onlyIfChanged: [...gqlFiles, 'codegen.ts'], + onlyIfChanged: ['codegen.ts', 'src/**/*.gql'], }); }, }; diff --git a/packages/react-generators/src/generators/auth/_providers/auth-components.ts b/packages/react-generators/src/generators/auth/_providers/auth-components.ts deleted file mode 100644 index 46a58bf5d..000000000 --- a/packages/react-generators/src/generators/auth/_providers/auth-components.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { TsImportMapProviderFromSchema } from '@baseplate-dev/core-generators'; - -import { createTsImportMapSchema } from '@baseplate-dev/core-generators'; -import { createReadOnlyProviderType } from '@baseplate-dev/sync'; - -export const authComponentsImportsSchema = createTsImportMapSchema({ - RequireAuth: {}, -}); - -export type AuthComponentImportsProvider = TsImportMapProviderFromSchema< - typeof authComponentsImportsSchema ->; - -export const authComponentsImportsProvider = - createReadOnlyProviderType( - 'auth-components-imports', - ); diff --git a/packages/react-generators/src/generators/auth/_providers/index.ts b/packages/react-generators/src/generators/auth/_providers/index.ts index 609085110..ec66161dd 100644 --- a/packages/react-generators/src/generators/auth/_providers/index.ts +++ b/packages/react-generators/src/generators/auth/_providers/index.ts @@ -1,3 +1,2 @@ -export * from './auth-components.js'; export * from './auth-hooks.js'; -export * from './react-auth.js'; +export * from './react-auth-routes.js'; diff --git a/packages/react-generators/src/generators/auth/_providers/providers.json b/packages/react-generators/src/generators/auth/_providers/providers.json index 41d95a8e3..368a2f828 100644 --- a/packages/react-generators/src/generators/auth/_providers/providers.json +++ b/packages/react-generators/src/generators/auth/_providers/providers.json @@ -12,15 +12,5 @@ "useSession": {} } } - }, - "auth-components.ts": { - "authComponentsImportsProvider": { - "type": "ts-imports", - "providerExport": "authComponentsImportsProvider", - "schemaExport": "authComponentsImportsSchema", - "projectExports": { - "RequireAuth": {} - } - } } } diff --git a/packages/react-generators/src/generators/auth/_providers/react-auth.ts b/packages/react-generators/src/generators/auth/_providers/react-auth-routes.ts similarity index 67% rename from packages/react-generators/src/generators/auth/_providers/react-auth.ts rename to packages/react-generators/src/generators/auth/_providers/react-auth-routes.ts index 2ae47282d..ba28634c6 100644 --- a/packages/react-generators/src/generators/auth/_providers/react-auth.ts +++ b/packages/react-generators/src/generators/auth/_providers/react-auth-routes.ts @@ -1,6 +1,6 @@ import { createReadOnlyProviderType } from '@baseplate-dev/sync'; -export interface ReactAuthProvider { +export interface ReactAuthRoutesProvider { /** Gets the URL path for the login page, e.g. `/auth/login` */ getLoginUrlPath: () => string; /** Gets the URL path for the register page, e.g. `/auth/register` */ @@ -10,5 +10,5 @@ export interface ReactAuthProvider { /** * A generic provider for using React */ -export const reactAuthProvider = - createReadOnlyProviderType('react-auth'); +export const reactAuthRoutesProvider = + createReadOnlyProviderType('react-auth-routes'); diff --git a/packages/react-generators/src/generators/auth/auth-identify/auth-identify.generator.ts b/packages/react-generators/src/generators/auth/auth-identify/auth-identify.generator.ts index 77aaef15f..5cc05fdfd 100644 --- a/packages/react-generators/src/generators/auth/auth-identify/auth-identify.generator.ts +++ b/packages/react-generators/src/generators/auth/auth-identify/auth-identify.generator.ts @@ -17,6 +17,8 @@ import { z } from 'zod'; import { reactRouterConfigProvider } from '#src/generators/core/react-router/index.js'; +import { authContextTask } from '../_tasks/auth-context.js'; + const descriptorSchema = z.object({}); const configSchema = createFieldMapSchemaBuilder((t) => ({ @@ -35,6 +37,7 @@ export const authIdentifyGenerator = createGenerator({ generatorFileUrl: import.meta.url, descriptorSchema, buildTasks: () => ({ + authContext: authContextTask, main: createGeneratorTask({ dependencies: { reactRouterConfig: reactRouterConfigProvider, diff --git a/packages/react-generators/src/generators/auth/index.ts b/packages/react-generators/src/generators/auth/index.ts index 2e1ff714b..e8ae6e8e7 100644 --- a/packages/react-generators/src/generators/auth/index.ts +++ b/packages/react-generators/src/generators/auth/index.ts @@ -1,4 +1,3 @@ export * from './_providers/index.js'; export * from './_tasks/index.js'; export * from './auth-identify/index.js'; -export * from './placeholder-auth-hooks/index.js'; diff --git a/packages/react-generators/src/generators/auth/placeholder-auth-hooks/extractor.json b/packages/react-generators/src/generators/auth/placeholder-auth-hooks/extractor.json deleted file mode 100644 index d4937d6d3..000000000 --- a/packages/react-generators/src/generators/auth/placeholder-auth-hooks/extractor.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "name": "auth/placeholder-auth-hooks", - "extractors": { - "ts": { - "importProviders": [ - "@baseplate-dev/react-generators:authHooksImportsProvider" - ], - "skipDefaultImportMap": true - } - }, - "templates": { - "src/hooks/useCurrentUser.ts": { - "name": "use-current-user", - "type": "ts", - "fileOptions": { "kind": "singleton" }, - "generator": "@baseplate-dev/react-generators#auth/placeholder-auth-hooks", - "group": "hooks", - "importMapProviders": {}, - "pathRootRelativePath": "{src-root}/hooks/useCurrentUser.ts", - "projectExports": { "useCurrentUser": {} }, - "variables": {} - }, - "src/hooks/useLogOut.ts": { - "name": "use-log-out", - "type": "ts", - "fileOptions": { "kind": "singleton" }, - "generator": "@baseplate-dev/react-generators#auth/placeholder-auth-hooks", - "group": "hooks", - "importMapProviders": {}, - "pathRootRelativePath": "{src-root}/hooks/useLogOut.ts", - "projectExports": { "useLogOut": {} }, - "variables": {} - }, - "src/hooks/useRequiredUserId.ts": { - "name": "use-required-user-id", - "type": "ts", - "fileOptions": { "kind": "singleton" }, - "generator": "@baseplate-dev/react-generators#auth/placeholder-auth-hooks", - "group": "hooks", - "importMapProviders": {}, - "pathRootRelativePath": "{src-root}/hooks/useRequiredUserId.ts", - "projectExports": { "useRequiredUserId": {} }, - "variables": {} - }, - "src/hooks/useSession.ts": { - "name": "use-session", - "type": "ts", - "fileOptions": { "kind": "singleton" }, - "generator": "@baseplate-dev/react-generators#auth/placeholder-auth-hooks", - "group": "hooks", - "importMapProviders": {}, - "pathRootRelativePath": "{src-root}/hooks/useSession.ts", - "projectExports": { - "SessionData": { "isTypeOnly": true }, - "useSession": {} - }, - "variables": {} - } - } -} diff --git a/packages/react-generators/src/generators/auth/placeholder-auth-hooks/generated/index.ts b/packages/react-generators/src/generators/auth/placeholder-auth-hooks/generated/index.ts deleted file mode 100644 index 5350a508e..000000000 --- a/packages/react-generators/src/generators/auth/placeholder-auth-hooks/generated/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AUTH_PLACEHOLDER_AUTH_HOOKS_PATHS } from './template-paths.js'; -import { AUTH_PLACEHOLDER_AUTH_HOOKS_IMPORTS } from './ts-import-providers.js'; -import { AUTH_PLACEHOLDER_AUTH_HOOKS_TEMPLATES } from './typed-templates.js'; - -export const AUTH_PLACEHOLDER_AUTH_HOOKS_GENERATED = { - imports: AUTH_PLACEHOLDER_AUTH_HOOKS_IMPORTS, - paths: AUTH_PLACEHOLDER_AUTH_HOOKS_PATHS, - templates: AUTH_PLACEHOLDER_AUTH_HOOKS_TEMPLATES, -}; diff --git a/packages/react-generators/src/generators/auth/placeholder-auth-hooks/generated/template-paths.ts b/packages/react-generators/src/generators/auth/placeholder-auth-hooks/generated/template-paths.ts deleted file mode 100644 index eb0d1267a..000000000 --- a/packages/react-generators/src/generators/auth/placeholder-auth-hooks/generated/template-paths.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { packageInfoProvider } from '@baseplate-dev/core-generators'; -import { createGeneratorTask, createProviderType } from '@baseplate-dev/sync'; - -export interface AuthPlaceholderAuthHooksPaths { - useCurrentUser: string; - useLogOut: string; - useRequiredUserId: string; - useSession: string; -} - -const authPlaceholderAuthHooksPaths = - createProviderType( - 'auth-placeholder-auth-hooks-paths', - ); - -const authPlaceholderAuthHooksPathsTask = createGeneratorTask({ - dependencies: { packageInfo: packageInfoProvider }, - exports: { - authPlaceholderAuthHooksPaths: authPlaceholderAuthHooksPaths.export(), - }, - run({ packageInfo }) { - const srcRoot = packageInfo.getPackageSrcPath(); - - return { - providers: { - authPlaceholderAuthHooksPaths: { - useCurrentUser: `${srcRoot}/hooks/useCurrentUser.ts`, - useLogOut: `${srcRoot}/hooks/useLogOut.ts`, - useRequiredUserId: `${srcRoot}/hooks/useRequiredUserId.ts`, - useSession: `${srcRoot}/hooks/useSession.ts`, - }, - }, - }; - }, -}); - -export const AUTH_PLACEHOLDER_AUTH_HOOKS_PATHS = { - provider: authPlaceholderAuthHooksPaths, - task: authPlaceholderAuthHooksPathsTask, -}; diff --git a/packages/react-generators/src/generators/auth/placeholder-auth-hooks/placeholder-auth-hooks.generator.ts b/packages/react-generators/src/generators/auth/placeholder-auth-hooks/placeholder-auth-hooks.generator.ts deleted file mode 100644 index 3fccda888..000000000 --- a/packages/react-generators/src/generators/auth/placeholder-auth-hooks/placeholder-auth-hooks.generator.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { typescriptFileProvider } from '@baseplate-dev/core-generators'; -import { createGenerator, createGeneratorTask } from '@baseplate-dev/sync'; -import { z } from 'zod'; - -import { AUTH_PLACEHOLDER_AUTH_HOOKS_GENERATED } from './generated/index.js'; - -const descriptorSchema = z.object({}); - -/** - * Placeholder generator for auth hooks. - * - * Useful for creating a test auth implementation. - */ -export const placeholderAuthHooksGenerator = createGenerator({ - name: 'auth/placeholder-auth-hooks', - generatorFileUrl: import.meta.url, - descriptorSchema, - buildTasks: () => ({ - paths: AUTH_PLACEHOLDER_AUTH_HOOKS_GENERATED.paths.task, - imports: AUTH_PLACEHOLDER_AUTH_HOOKS_GENERATED.imports.task, - main: createGeneratorTask({ - dependencies: { - typescriptFile: typescriptFileProvider, - paths: AUTH_PLACEHOLDER_AUTH_HOOKS_GENERATED.paths.provider, - }, - run({ typescriptFile, paths }) { - return { - build: async (builder) => { - await builder.apply( - typescriptFile.renderTemplateGroup({ - group: - AUTH_PLACEHOLDER_AUTH_HOOKS_GENERATED.templates.hooksGroup, - paths, - variables: {}, - }), - ); - }, - }; - }, - }), - }), -}); diff --git a/packages/react-generators/src/generators/auth/placeholder-auth-hooks/templates/src/hooks/useCurrentUser.ts b/packages/react-generators/src/generators/auth/placeholder-auth-hooks/templates/src/hooks/useCurrentUser.ts deleted file mode 100644 index 08773cdb9..000000000 --- a/packages/react-generators/src/generators/auth/placeholder-auth-hooks/templates/src/hooks/useCurrentUser.ts +++ /dev/null @@ -1,9 +0,0 @@ -// @ts-nocheck - -export function useCurrentUser(): { - user: any; - loading: boolean; - error: Error | undefined; -} { - throw new Error('Not implemented'); -} diff --git a/packages/react-generators/src/generators/auth/placeholder-auth-hooks/templates/src/hooks/useLogOut.ts b/packages/react-generators/src/generators/auth/placeholder-auth-hooks/templates/src/hooks/useLogOut.ts deleted file mode 100644 index 8f0d98492..000000000 --- a/packages/react-generators/src/generators/auth/placeholder-auth-hooks/templates/src/hooks/useLogOut.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck - -export function useLogOut(): () => Promise { - throw new Error('Not implemented'); -} diff --git a/packages/react-generators/src/generators/auth/placeholder-auth-hooks/templates/src/hooks/useRequiredUserId.ts b/packages/react-generators/src/generators/auth/placeholder-auth-hooks/templates/src/hooks/useRequiredUserId.ts deleted file mode 100644 index d61741ba5..000000000 --- a/packages/react-generators/src/generators/auth/placeholder-auth-hooks/templates/src/hooks/useRequiredUserId.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck - -export function useRequiredUserId(): string { - throw new Error('Not implemented'); -} diff --git a/packages/react-generators/src/generators/core/react-components/extractor.json b/packages/react-generators/src/generators/core/react-components/extractor.json index 5b19b5fe8..775c15078 100644 --- a/packages/react-generators/src/generators/core/react-components/extractor.json +++ b/packages/react-generators/src/generators/core/react-components/extractor.json @@ -185,6 +185,11 @@ "Button": {}, "Calendar": {}, "Card": {}, + "CardContent": {}, + "CardDescription": {}, + "CardFooter": {}, + "CardHeader": {}, + "CardTitle": {}, "Checkbox": {}, "CheckboxField": {}, "CheckboxFieldController": {}, diff --git a/packages/react-generators/src/generators/core/react-components/generated/ts-import-providers.ts b/packages/react-generators/src/generators/core/react-components/generated/ts-import-providers.ts index 39efc4d8f..b245c87ca 100644 --- a/packages/react-generators/src/generators/core/react-components/generated/ts-import-providers.ts +++ b/packages/react-generators/src/generators/core/react-components/generated/ts-import-providers.ts @@ -21,6 +21,11 @@ const reactComponentsImportsSchema = createTsImportMapSchema({ buttonVariants: {}, Calendar: {}, Card: {}, + CardContent: {}, + CardDescription: {}, + CardFooter: {}, + CardHeader: {}, + CardTitle: {}, Checkbox: {}, CheckboxField: {}, CheckboxFieldController: {}, @@ -151,6 +156,11 @@ const coreReactComponentsImportsTask = createGeneratorTask({ buttonVariants: paths.stylesButton, Calendar: paths.index, Card: paths.index, + CardContent: paths.index, + CardDescription: paths.index, + CardFooter: paths.index, + CardHeader: paths.index, + CardTitle: paths.index, Checkbox: paths.index, CheckboxField: paths.index, CheckboxFieldController: paths.index, diff --git a/packages/react-generators/src/generators/core/react-components/generated/typed-templates.ts b/packages/react-generators/src/generators/core/react-components/generated/typed-templates.ts index 24ab43c28..593895272 100644 --- a/packages/react-generators/src/generators/core/react-components/generated/typed-templates.ts +++ b/packages/react-generators/src/generators/core/react-components/generated/typed-templates.ts @@ -580,6 +580,11 @@ const index = createTsTemplateFile({ Button: {}, Calendar: {}, Card: {}, + CardContent: {}, + CardDescription: {}, + CardFooter: {}, + CardHeader: {}, + CardTitle: {}, Checkbox: {}, CheckboxField: {}, CheckboxFieldController: {}, diff --git a/packages/react-generators/src/generators/core/react-error/extractor.json b/packages/react-generators/src/generators/core/react-error/extractor.json index ab84bf634..befd802bf 100644 --- a/packages/react-generators/src/generators/core/react-error/extractor.json +++ b/packages/react-generators/src/generators/core/react-error/extractor.json @@ -9,7 +9,7 @@ "importMapProviders": {}, "pathRootRelativePath": "{src-root}/services/error-formatter.ts", "projectExports": { "formatError": {}, "logAndFormatError": {} }, - "variables": { "TPL_ERROR_FORMATTERS": {} } + "variables": { "TPL_GET_FORMATTED_ERROR_SUFFIX": {} } }, "src/services/error-logger.ts": { "name": "error-logger", diff --git a/packages/react-generators/src/generators/core/react-error/generated/typed-templates.ts b/packages/react-generators/src/generators/core/react-error/generated/typed-templates.ts index ab4c49a5b..19197bcf6 100644 --- a/packages/react-generators/src/generators/core/react-error/generated/typed-templates.ts +++ b/packages/react-generators/src/generators/core/react-error/generated/typed-templates.ts @@ -14,7 +14,7 @@ const errorFormatter = createTsTemplateFile({ '../templates/src/services/error-formatter.ts', ), }, - variables: { TPL_ERROR_FORMATTERS: {} }, + variables: { TPL_GET_FORMATTED_ERROR_SUFFIX: {} }, }); const errorLogger = createTsTemplateFile({ diff --git a/packages/react-generators/src/generators/core/react-error/react-error.generator.ts b/packages/react-generators/src/generators/core/react-error/react-error.generator.ts index 8cf715a6f..f1777406f 100644 --- a/packages/react-generators/src/generators/core/react-error/react-error.generator.ts +++ b/packages/react-generators/src/generators/core/react-error/react-error.generator.ts @@ -3,6 +3,7 @@ import type { TsCodeFragment } from '@baseplate-dev/core-generators'; import { packageScope, TsCodeUtils, + tsTemplate, typescriptFileProvider, } from '@baseplate-dev/core-generators'; import { @@ -74,13 +75,20 @@ export const reactErrorGenerator = createGenerator({ }, }), ); + const getFormattedErrorSuffix = tsTemplate` + ${errorFormatters.size === 0 ? '// eslint-disable-next-line @typescript-eslint/no-unused-vars' : ''} + function getFormattedErrorSuffix(${errorFormatters.size > 0 ? 'error' : '_error'}: unknown): string { + ${TsCodeUtils.mergeFragments(errorFormatters)}; + + return 'Please try again later.'; + } + `; await builder.apply( typescriptFile.renderTemplateFile({ template: CORE_REACT_ERROR_GENERATED.templates.errorFormatter, destination: paths.errorFormatter, variables: { - TPL_ERROR_FORMATTERS: - TsCodeUtils.mergeFragments(errorFormatters), + TPL_GET_FORMATTED_ERROR_SUFFIX: getFormattedErrorSuffix, }, }), ); diff --git a/packages/react-generators/src/generators/core/react-error/templates/src/services/error-formatter.ts b/packages/react-generators/src/generators/core/react-error/templates/src/services/error-formatter.ts index a0da7c5bf..f33f5cee9 100644 --- a/packages/react-generators/src/generators/core/react-error/templates/src/services/error-formatter.ts +++ b/packages/react-generators/src/generators/core/react-error/templates/src/services/error-formatter.ts @@ -2,11 +2,7 @@ import { logError } from './error-logger.js'; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function getFormattedErrorSuffix(error: unknown): string { - TPL_ERROR_FORMATTERS; - return 'Please try again later.'; -} +TPL_GET_FORMATTED_ERROR_SUFFIX; export function formatError( error: unknown, diff --git a/packages/react-generators/src/generators/core/react-utils/generated/index.ts b/packages/react-generators/src/generators/core/react-utils/generated/index.ts index bd4d48d7c..595049beb 100644 --- a/packages/react-generators/src/generators/core/react-utils/generated/index.ts +++ b/packages/react-generators/src/generators/core/react-utils/generated/index.ts @@ -1,9 +1,11 @@ import { CORE_REACT_UTILS_PATHS } from './template-paths.js'; +import { CORE_REACT_UTILS_RENDERERS } from './template-renderers.js'; import { CORE_REACT_UTILS_IMPORTS } from './ts-import-providers.js'; import { CORE_REACT_UTILS_TEMPLATES } from './typed-templates.js'; export const CORE_REACT_UTILS_GENERATED = { imports: CORE_REACT_UTILS_IMPORTS, paths: CORE_REACT_UTILS_PATHS, + renderers: CORE_REACT_UTILS_RENDERERS, templates: CORE_REACT_UTILS_TEMPLATES, }; diff --git a/packages/react-generators/src/generators/core/react-utils/generated/template-renderers.ts b/packages/react-generators/src/generators/core/react-utils/generated/template-renderers.ts new file mode 100644 index 000000000..61562bd6b --- /dev/null +++ b/packages/react-generators/src/generators/core/react-utils/generated/template-renderers.ts @@ -0,0 +1,54 @@ +import type { RenderTsTemplateFileActionInput } from '@baseplate-dev/core-generators'; +import type { BuilderAction } from '@baseplate-dev/sync'; + +import { typescriptFileProvider } from '@baseplate-dev/core-generators'; +import { createGeneratorTask, createProviderType } from '@baseplate-dev/sync'; + +import { CORE_REACT_UTILS_PATHS } from './template-paths.js'; +import { CORE_REACT_UTILS_TEMPLATES } from './typed-templates.js'; + +export interface CoreReactUtilsRenderers { + safeLocalStorage: { + render: ( + options: Omit< + RenderTsTemplateFileActionInput< + typeof CORE_REACT_UTILS_TEMPLATES.safeLocalStorage + >, + 'destination' | 'importMapProviders' | 'template' + >, + ) => BuilderAction; + }; +} + +const coreReactUtilsRenderers = createProviderType( + 'core-react-utils-renderers', +); + +const coreReactUtilsRenderersTask = createGeneratorTask({ + dependencies: { + paths: CORE_REACT_UTILS_PATHS.provider, + typescriptFile: typescriptFileProvider, + }, + exports: { coreReactUtilsRenderers: coreReactUtilsRenderers.export() }, + run({ paths, typescriptFile }) { + return { + providers: { + coreReactUtilsRenderers: { + safeLocalStorage: { + render: (options) => + typescriptFile.renderTemplateFile({ + template: CORE_REACT_UTILS_TEMPLATES.safeLocalStorage, + destination: paths.safeLocalStorage, + ...options, + }), + }, + }, + }, + }; + }, +}); + +export const CORE_REACT_UTILS_RENDERERS = { + provider: coreReactUtilsRenderers, + task: coreReactUtilsRenderersTask, +}; diff --git a/packages/sync/package.json b/packages/sync/package.json index 21ddd4657..304e7e1cf 100644 --- a/packages/sync/package.json +++ b/packages/sync/package.json @@ -58,6 +58,7 @@ "execa": "9.3.0", "fast-json-patch": "^3.1.1", "globby": "^14.0.2", + "micromatch": "4.0.8", "ms": "2.1.3", "node-diff3": "3.1.2", "p-limit": "6.1.0", @@ -66,6 +67,7 @@ }, "devDependencies": { "@baseplate-dev/tools": "workspace:*", + "@types/micromatch": "4.0.9", "@types/ms": "0.7.34", "@types/node": "catalog:", "eslint": "catalog:", diff --git a/packages/sync/src/output/post-write-commands/filter-commands.ts b/packages/sync/src/output/post-write-commands/filter-commands.ts index 8061d4e51..ecb1ac933 100644 --- a/packages/sync/src/output/post-write-commands/filter-commands.ts +++ b/packages/sync/src/output/post-write-commands/filter-commands.ts @@ -1,3 +1,5 @@ +import micromatch from 'micromatch'; + import { normalizePathToOutputPath } from '#src/utils/canonical-path.js'; import type { PostWriteCommand } from './types.js'; @@ -29,8 +31,11 @@ export function filterPostWriteCommands( return ( command.options?.onlyIfChanged == null || - onlyIfChangedArr.some((file) => - modifiedRelativePaths.has(normalizePathToOutputPath(file)), + onlyIfChangedArr.some((pattern) => + // Check if any modified path matches the pattern (supports globs) + [...modifiedRelativePaths].some((modifiedPath) => + micromatch.isMatch(normalizePathToOutputPath(modifiedPath), pattern), + ), ) || rerunCommands.includes(command.command) ); diff --git a/packages/sync/src/output/post-write-commands/filter-commands.unit.test.ts b/packages/sync/src/output/post-write-commands/filter-commands.unit.test.ts index 8bb74de36..ec1384187 100644 --- a/packages/sync/src/output/post-write-commands/filter-commands.unit.test.ts +++ b/packages/sync/src/output/post-write-commands/filter-commands.unit.test.ts @@ -91,4 +91,45 @@ describe('filterPostWriteCommands', () => { expect(filtered).toEqual([]); }); + + it('should support glob patterns in onlyIfChanged', () => { + const commands: PostWriteCommand[] = [ + { + command: 'run on any src file', + options: { onlyIfChanged: 'src/**/*.ts' }, + }, + { + command: 'run on root files', + options: { onlyIfChanged: '*.json' }, + }, + { command: 'always run' }, + ]; + + const filtered = filterPostWriteCommands(commands, { + modifiedRelativePaths: new Set(['src/utils/helper.ts', 'package.json']), + rerunCommands: [], + }); + + expect(filtered.map((c) => c.command)).toEqual([ + 'run on any src file', + 'run on root files', + 'always run', + ]); + }); + + it('should support a mix of globs and exact paths', () => { + const commands: PostWriteCommand[] = [ + { + command: 'run on src or lockfile', + options: { onlyIfChanged: ['src/**/*.ts', 'yarn.lock'] }, + }, + ]; + + const filtered = filterPostWriteCommands(commands, { + modifiedRelativePaths: new Set(['src/index.ts', 'yarn.lock']), + rerunCommands: [], + }); + + expect(filtered).toEqual(commands); + }); }); diff --git a/packages/sync/src/output/post-write-commands/types.ts b/packages/sync/src/output/post-write-commands/types.ts index 7c2755c64..fca5e999c 100644 --- a/packages/sync/src/output/post-write-commands/types.ts +++ b/packages/sync/src/output/post-write-commands/types.ts @@ -30,6 +30,8 @@ export interface PostWriteCommandOptions { priority?: keyof typeof POST_WRITE_COMMAND_PRIORITY | number; /** * Only run command if the provided files were changed + * + * Supports glob patterns (micromatch syntax) */ onlyIfChanged?: string | string[]; /** diff --git a/packages/sync/src/runner/errors.ts b/packages/sync/src/runner/errors.ts deleted file mode 100644 index c41f5bde8..000000000 --- a/packages/sync/src/runner/errors.ts +++ /dev/null @@ -1,12 +0,0 @@ -export class GeneratorTaskStepError extends Error { - constructor( - public cause: unknown, - public taskId: string, - public step: string, - public generatorName: string, - ) { - super( - `Error in the ${step} step of the ${generatorName} generator task: ${String(cause)}`, - ); - } -} diff --git a/packages/sync/src/runner/generator-runner.ts b/packages/sync/src/runner/generator-runner.ts index 6d457d41d..081f50a52 100644 --- a/packages/sync/src/runner/generator-runner.ts +++ b/packages/sync/src/runner/generator-runner.ts @@ -1,4 +1,4 @@ -import { mapGroupBy } from '@baseplate-dev/utils'; +import { enhanceErrorWithContext, mapGroupBy } from '@baseplate-dev/utils'; import { keyBy, mapValues } from 'es-toolkit'; import { sortTaskPhases } from '#src/phases/sort-task-phases.js'; @@ -24,7 +24,6 @@ import { resolveTaskDependenciesForPhase, } from './dependency-map.js'; import { getSortedRunSteps } from './dependency-sort.js'; -import { GeneratorTaskStepError } from './errors.js'; import { runInRunnerContext } from './runner-context.js'; import { flattenGeneratorTaskEntriesAndPhases } from './utils.js'; @@ -267,11 +266,9 @@ export async function executeGeneratorEntry( } } catch (error) { const { generatorInfo } = taskEntriesById[taskId]; - throw new GeneratorTaskStepError( + throw enhanceErrorWithContext( error, - taskId, - action, - generatorInfo.name, + `Error in the ${action} step of the ${generatorInfo.name} generator task`, ); } } diff --git a/packages/sync/src/runner/index.ts b/packages/sync/src/runner/index.ts index 12f2fdd1c..5593f6783 100644 --- a/packages/sync/src/runner/index.ts +++ b/packages/sync/src/runner/index.ts @@ -1,3 +1,2 @@ -export * from './errors.js'; export * from './generator-runner.js'; export * from './runner-context.js'; diff --git a/packages/sync/src/utils/parse-generator-name.ts b/packages/sync/src/utils/parse-generator-name.ts index 4343448c2..1711cb0dc 100644 --- a/packages/sync/src/utils/parse-generator-name.ts +++ b/packages/sync/src/utils/parse-generator-name.ts @@ -1,4 +1,4 @@ -const generatorNameRegex = /^([^#]+)#([^/]+\/)?([^#]+)$/; +const generatorNameRegex = /^([^#]+)#(.+\/)?([^#/]+)$/; /** * A parsed generator name. diff --git a/packages/sync/src/utils/parse-generator-name.unit.test.ts b/packages/sync/src/utils/parse-generator-name.unit.test.ts index 38a786449..5ef3d6125 100644 --- a/packages/sync/src/utils/parse-generator-name.unit.test.ts +++ b/packages/sync/src/utils/parse-generator-name.unit.test.ts @@ -34,6 +34,22 @@ describe('parseGeneratorName', () => { }); }); + it('parses generator with nested subdirectory', () => { + // Arrange + const input = + '@baseplate-dev/plugin-storage#fastify/core/prisma-file-transformer'; + + // Act + const result = parseGeneratorName(input); + + // Assert + expect(result).toEqual({ + packageName: '@baseplate-dev/plugin-storage', + generatorPath: 'fastify/core/prisma-file-transformer', + generatorBasename: 'prisma-file-transformer', + }); + }); + it('throws on invalid input', () => { // Arrange const input = '@baseplate-dev/core-generators'; diff --git a/packages/ui-components/package.json b/packages/ui-components/package.json index 8ad6741dc..70f504583 100644 --- a/packages/ui-components/package.json +++ b/packages/ui-components/package.json @@ -79,7 +79,7 @@ "react-colorful": "5.6.1", "react-day-picker": "9.7.0", "react-dom": "catalog:", - "react-hook-form": "7.56.3", + "react-hook-form": "7.60.0", "react-icons": "5.5.0", "sonner": "2.0.3", "zod": "catalog:", diff --git a/packages/ui-components/src/components/form-action-bar/form-action-bar.tsx b/packages/ui-components/src/components/form-action-bar/form-action-bar.tsx index fb37a3de1..ac9213734 100644 --- a/packages/ui-components/src/components/form-action-bar/form-action-bar.tsx +++ b/packages/ui-components/src/components/form-action-bar/form-action-bar.tsx @@ -14,9 +14,13 @@ interface FormActionBarProps extends React.ComponentProps<'div'> { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- need to support any form return form?: UseFormReturn; /** - * Whether the form actions should be disabled + * Whether the form actions should be disabled (if undefined, will use (form.isSubmitting || !form.isDirty)) */ disabled?: boolean; + /** + * Whether to enable the save button even if the form is not dirty (useful when submitting data where defaults are allowed) + */ + allowSaveWithoutDirty?: boolean; /** * Optional custom reset handler (defaults to form.reset) */ @@ -38,19 +42,24 @@ interface FormActionBarProps extends React.ComponentProps<'div'> { function FormActionBar(props: FormActionBarProps): React.ReactElement { const { className, - disabled = false, + disabled, children, form, onReset, + allowSaveWithoutDirty, ...rest } = props; // Determine values based on whether form prop is provided const { formState } = form ?? {}; - const formIsDisabled = formState + const defaultSaveIsDisabled = formState + ? formState.isSubmitting || (!allowSaveWithoutDirty && !formState.isDirty) + : false; + const defaultResetIsDisabled = formState ? formState.isSubmitting || !formState.isDirty : false; - const isDisabled = formIsDisabled || disabled; + const isSaveDisabled = disabled ?? defaultSaveIsDisabled; + const isResetDisabled = disabled ?? defaultResetIsDisabled; const handleReset = (): void => { form?.reset(); @@ -73,7 +82,7 @@ function FormActionBar(props: FormActionBarProps): React.ReactElement { size="sm" type="button" onClick={handleReset} - disabled={isDisabled} + disabled={isResetDisabled} > Reset @@ -81,7 +90,7 @@ function FormActionBar(props: FormActionBarProps): React.ReactElement { variant="default" size="sm" type="submit" - disabled={isDisabled} + disabled={isSaveDisabled} > Save diff --git a/plugins/plugin-auth/manifest.json b/plugins/plugin-auth/manifest.json index 92ea490c1..e0b34a42a 100644 --- a/plugins/plugin-auth/manifest.json +++ b/plugins/plugin-auth/manifest.json @@ -1,4 +1,4 @@ { - "plugins": ["dist/auth", "dist/auth0"], + "plugins": ["dist/auth", "dist/auth0", "dist/placeholder-auth"], "webBuild": "dist/web" } diff --git a/plugins/plugin-auth/package.json b/plugins/plugin-auth/package.json index 4ed5fb38e..9ad5d97ba 100644 --- a/plugins/plugin-auth/package.json +++ b/plugins/plugin-auth/package.json @@ -48,10 +48,11 @@ "@baseplate-dev/fastify-generators": "workspace:*", "@baseplate-dev/react-generators": "workspace:*", "@baseplate-dev/ui-components": "workspace:*", + "@hookform/lenses": "0.7.1", "@hookform/resolvers": "5.0.1", "react": "catalog:", "react-dom": "catalog:", - "react-hook-form": "7.56.3", + "react-hook-form": "7.60.0", "react-icons": "5.5.0", "zod": "catalog:" }, diff --git a/plugins/plugin-auth/src/auth/core/components/auth-definition-editor.tsx b/plugins/plugin-auth/src/auth/core/components/auth-definition-editor.tsx index 3a85c673f..58be6d174 100644 --- a/plugins/plugin-auth/src/auth/core/components/auth-definition-editor.tsx +++ b/plugins/plugin-auth/src/auth/core/components/auth-definition-editor.tsx @@ -4,6 +4,7 @@ import type React from 'react'; import { createAndApplyModelMergerResults, createModelMergerResults, + doesModelMergerResultsHaveChanges, FeatureUtils, ModelUtils, PluginUtils, @@ -18,23 +19,25 @@ import { useResettableForm, } from '@baseplate-dev/project-builder-lib/web'; import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, FormActionBar, + SectionList, + SectionListSection, + SectionListSectionContent, + SectionListSectionDescription, + SectionListSectionHeader, + SectionListSectionTitle, } from '@baseplate-dev/ui-components'; +import { useLens } from '@hookform/lenses'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMemo } from 'react'; -import { createDefaultAuthRoles } from '#src/roles/index.js'; +import { RoleEditorForm } from '#src/common/roles/components/index.js'; +import { createDefaultAuthRoles } from '#src/common/roles/index.js'; import type { AuthPluginDefinitionInput } from '../schema/plugin-definition.js'; import { createAuthModels } from '../schema/models.js'; import { createAuthPluginDefinitionSchema } from '../schema/plugin-definition.js'; -import RoleEditorForm from './role-editor-form.js'; import '#src/styles.css'; @@ -130,70 +133,84 @@ export function AuthDefinitionEditor({ useBlockUnsavedChangesNavigate({ control, reset, onSubmit }); + const lens = useLens({ control }); + return (
-
- - - Local Authentication Configuration - - Configure your local authentication settings, user models, and - role definitions. - - - - - -
- - - - -
- -
- + + + + + Local Authentication Configuration + + + Configure your local authentication settings, user models, and + role definitions. + + + + -
-
-
- +
+ + + + +
+ +
+ +
+ + + + +
- + ); } diff --git a/plugins/plugin-auth/src/auth/core/components/role-editor-form.tsx b/plugins/plugin-auth/src/auth/core/components/role-editor-form.tsx deleted file mode 100644 index d01aff029..000000000 --- a/plugins/plugin-auth/src/auth/core/components/role-editor-form.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import type React from 'react'; -import type { Control } from 'react-hook-form'; - -import { authRoleEntityType } from '@baseplate-dev/project-builder-lib'; -import { Button, InputFieldController } from '@baseplate-dev/ui-components'; -import { useFieldArray } from 'react-hook-form'; - -import { AUTH_DEFAULT_ROLES } from '#src/roles/index.js'; -import { cn } from '#src/utils/cn.js'; - -import type { AuthPluginDefinitionInput } from '../schema/plugin-definition.js'; - -interface Props { - className?: string; - control: Control; -} - -function isFixedRole(name: string): boolean { - return AUTH_DEFAULT_ROLES.some((role) => role.name === name); -} - -function RoleEditorForm({ className, control }: Props): React.JSX.Element { - const { fields, append, remove } = useFieldArray({ - control, - name: 'roles', - }); - - return ( -
-

Roles

- {fields.map((field, idx) => ( -
- - - {!isFixedRole(field.name) && ( - - )} -
- ))} - - -
- ); -} - -export default RoleEditorForm; diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-email-password/auth-email-password.generator.ts b/plugins/plugin-auth/src/auth/core/generators/auth-email-password/auth-email-password.generator.ts new file mode 100644 index 000000000..f7d608b2a --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-email-password/auth-email-password.generator.ts @@ -0,0 +1,42 @@ +import { appModuleProvider } from '@baseplate-dev/fastify-generators'; +import { createGenerator, createGeneratorTask } from '@baseplate-dev/sync'; +import { z } from 'zod'; + +import { AUTH_CORE_AUTH_EMAIL_PASSWORD_GENERATED as GENERATED_TEMPLATES } from './generated'; + +const descriptorSchema = z.object({}); + +/** + * Sets up email / password authentication + */ +export const authEmailPasswordGenerator = createGenerator({ + name: 'auth/core/auth-email-password', + generatorFileUrl: import.meta.url, + descriptorSchema, + buildTasks: () => ({ + paths: GENERATED_TEMPLATES.paths.task, + imports: GENERATED_TEMPLATES.imports.task, + renderers: GENERATED_TEMPLATES.renderers.task, + appModule: createGeneratorTask({ + dependencies: { + paths: GENERATED_TEMPLATES.paths.provider, + appModule: appModuleProvider, + }, + run({ paths, appModule }) { + appModule.moduleImports.push(paths.schemaUserPasswordMutations); + }, + }), + main: createGeneratorTask({ + dependencies: { + renderers: GENERATED_TEMPLATES.renderers.provider, + }, + run({ renderers }) { + return { + build: async (builder) => { + await builder.apply(renderers.moduleGroup.render({})); + }, + }; + }, + }), + }), +}); diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-email-password/extractor.json b/plugins/plugin-auth/src/auth/core/generators/auth-email-password/extractor.json new file mode 100644 index 000000000..c4ddb6283 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-email-password/extractor.json @@ -0,0 +1,74 @@ +{ + "name": "auth/core/auth-email-password", + "templates": { + "module/constants/password.constants.ts": { + "name": "constants-password", + "type": "ts", + "fileOptions": { "kind": "singleton" }, + "generator": "@baseplate-dev/plugin-auth#auth/core/auth-email-password", + "group": "module", + "importMapProviders": {}, + "pathRootRelativePath": "{module-root}/constants/password.constants.ts", + "projectExports": { "PASSWORD_MIN_LENGTH": {} }, + "variables": {} + }, + "module/schema/user-password.mutations.ts": { + "name": "schema-user-password-mutations", + "type": "ts", + "fileOptions": { "kind": "singleton" }, + "generator": "@baseplate-dev/plugin-auth#auth/core/auth-email-password", + "group": "module", + "importMapProviders": { + "authModuleImportsProvider": { + "importName": "authModuleImportsProvider", + "packagePathSpecifier": "@baseplate-dev/plugin-auth:src/auth/core/generators/auth-module/generated/ts-import-providers.ts" + }, + "pothosImportsProvider": { + "importName": "pothosImportsProvider", + "packagePathSpecifier": "@baseplate-dev/fastify-generators:src/generators/pothos/pothos/generated/ts-import-providers.ts" + } + }, + "pathRootRelativePath": "{module-root}/schema/user-password.mutations.ts", + "variables": {} + }, + "module/services/user-password.service.ts": { + "name": "services-user-password", + "type": "ts", + "fileOptions": { "kind": "singleton" }, + "generator": "@baseplate-dev/plugin-auth#auth/core/auth-email-password", + "group": "module", + "importMapProviders": { + "errorHandlerServiceImportsProvider": { + "importName": "errorHandlerServiceImportsProvider", + "packagePathSpecifier": "@baseplate-dev/fastify-generators:src/generators/core/error-handler-service/generated/ts-import-providers.ts" + }, + "passwordHasherServiceImportsProvider": { + "importName": "passwordHasherServiceImportsProvider", + "packagePathSpecifier": "@baseplate-dev/fastify-generators:src/generators/auth/password-hasher-service/generated/ts-import-providers.ts" + }, + "prismaImportsProvider": { + "importName": "prismaImportsProvider", + "packagePathSpecifier": "@baseplate-dev/fastify-generators:src/generators/prisma/prisma/generated/ts-import-providers.ts" + }, + "requestServiceContextImportsProvider": { + "importName": "requestServiceContextImportsProvider", + "packagePathSpecifier": "@baseplate-dev/fastify-generators:src/generators/core/request-service-context/generated/ts-import-providers.ts" + }, + "userSessionServiceImportsProvider": { + "importName": "userSessionServiceImportsProvider", + "packagePathSpecifier": "@baseplate-dev/fastify-generators:src/generators/auth/_providers/user-session.ts" + }, + "userSessionTypesImportsProvider": { + "importName": "userSessionTypesImportsProvider", + "packagePathSpecifier": "@baseplate-dev/fastify-generators:src/generators/auth/user-session-types/generated/ts-import-providers.ts" + } + }, + "pathRootRelativePath": "{module-root}/services/user-password.service.ts", + "projectExports": { + "authenticateUserWithEmailAndPassword": {}, + "createUserWithEmailAndPassword": {} + }, + "variables": {} + } + } +} diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-email-password/generated/index.ts b/plugins/plugin-auth/src/auth/core/generators/auth-email-password/generated/index.ts new file mode 100644 index 000000000..855ca08de --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-email-password/generated/index.ts @@ -0,0 +1,11 @@ +import { AUTH_CORE_AUTH_EMAIL_PASSWORD_PATHS } from './template-paths.js'; +import { AUTH_CORE_AUTH_EMAIL_PASSWORD_RENDERERS } from './template-renderers.js'; +import { AUTH_CORE_AUTH_EMAIL_PASSWORD_IMPORTS } from './ts-import-providers.js'; +import { AUTH_CORE_AUTH_EMAIL_PASSWORD_TEMPLATES } from './typed-templates.js'; + +export const AUTH_CORE_AUTH_EMAIL_PASSWORD_GENERATED = { + imports: AUTH_CORE_AUTH_EMAIL_PASSWORD_IMPORTS, + paths: AUTH_CORE_AUTH_EMAIL_PASSWORD_PATHS, + renderers: AUTH_CORE_AUTH_EMAIL_PASSWORD_RENDERERS, + templates: AUTH_CORE_AUTH_EMAIL_PASSWORD_TEMPLATES, +}; diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-email-password/generated/template-paths.ts b/plugins/plugin-auth/src/auth/core/generators/auth-email-password/generated/template-paths.ts new file mode 100644 index 000000000..825bc7f05 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-email-password/generated/template-paths.ts @@ -0,0 +1,38 @@ +import { appModuleProvider } from '@baseplate-dev/fastify-generators'; +import { createGeneratorTask, createProviderType } from '@baseplate-dev/sync'; + +export interface AuthCoreAuthEmailPasswordPaths { + constantsPassword: string; + schemaUserPasswordMutations: string; + servicesUserPassword: string; +} + +const authCoreAuthEmailPasswordPaths = + createProviderType( + 'auth-core-auth-email-password-paths', + ); + +const authCoreAuthEmailPasswordPathsTask = createGeneratorTask({ + dependencies: { appModule: appModuleProvider }, + exports: { + authCoreAuthEmailPasswordPaths: authCoreAuthEmailPasswordPaths.export(), + }, + run({ appModule }) { + const moduleRoot = appModule.getModuleFolder(); + + return { + providers: { + authCoreAuthEmailPasswordPaths: { + constantsPassword: `${moduleRoot}/constants/password.constants.ts`, + schemaUserPasswordMutations: `${moduleRoot}/schema/user-password.mutations.ts`, + servicesUserPassword: `${moduleRoot}/services/user-password.service.ts`, + }, + }, + }; + }, +}); + +export const AUTH_CORE_AUTH_EMAIL_PASSWORD_PATHS = { + provider: authCoreAuthEmailPasswordPaths, + task: authCoreAuthEmailPasswordPathsTask, +}; diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-email-password/generated/template-renderers.ts b/plugins/plugin-auth/src/auth/core/generators/auth-email-password/generated/template-renderers.ts new file mode 100644 index 000000000..9d8570ec0 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-email-password/generated/template-renderers.ts @@ -0,0 +1,98 @@ +import type { RenderTsTemplateGroupActionInput } from '@baseplate-dev/core-generators'; +import type { BuilderAction } from '@baseplate-dev/sync'; + +import { typescriptFileProvider } from '@baseplate-dev/core-generators'; +import { + errorHandlerServiceImportsProvider, + passwordHasherServiceImportsProvider, + pothosImportsProvider, + prismaImportsProvider, + requestServiceContextImportsProvider, + userSessionServiceImportsProvider, + userSessionTypesImportsProvider, +} from '@baseplate-dev/fastify-generators'; +import { createGeneratorTask, createProviderType } from '@baseplate-dev/sync'; + +import { authModuleImportsProvider } from '#src/auth/core/generators/auth-module/generated/ts-import-providers.js'; + +import { AUTH_CORE_AUTH_EMAIL_PASSWORD_PATHS } from './template-paths.js'; +import { AUTH_CORE_AUTH_EMAIL_PASSWORD_TEMPLATES } from './typed-templates.js'; + +export interface AuthCoreAuthEmailPasswordRenderers { + moduleGroup: { + render: ( + options: Omit< + RenderTsTemplateGroupActionInput< + typeof AUTH_CORE_AUTH_EMAIL_PASSWORD_TEMPLATES.moduleGroup + >, + 'importMapProviders' | 'group' | 'paths' + >, + ) => BuilderAction; + }; +} + +const authCoreAuthEmailPasswordRenderers = + createProviderType( + 'auth-core-auth-email-password-renderers', + ); + +const authCoreAuthEmailPasswordRenderersTask = createGeneratorTask({ + dependencies: { + authModuleImports: authModuleImportsProvider, + errorHandlerServiceImports: errorHandlerServiceImportsProvider, + passwordHasherServiceImports: passwordHasherServiceImportsProvider, + paths: AUTH_CORE_AUTH_EMAIL_PASSWORD_PATHS.provider, + pothosImports: pothosImportsProvider, + prismaImports: prismaImportsProvider, + requestServiceContextImports: requestServiceContextImportsProvider, + typescriptFile: typescriptFileProvider, + userSessionServiceImports: userSessionServiceImportsProvider, + userSessionTypesImports: userSessionTypesImportsProvider, + }, + exports: { + authCoreAuthEmailPasswordRenderers: + authCoreAuthEmailPasswordRenderers.export(), + }, + run({ + authModuleImports, + errorHandlerServiceImports, + passwordHasherServiceImports, + paths, + pothosImports, + prismaImports, + requestServiceContextImports, + typescriptFile, + userSessionServiceImports, + userSessionTypesImports, + }) { + return { + providers: { + authCoreAuthEmailPasswordRenderers: { + moduleGroup: { + render: (options) => + typescriptFile.renderTemplateGroup({ + group: AUTH_CORE_AUTH_EMAIL_PASSWORD_TEMPLATES.moduleGroup, + paths, + importMapProviders: { + authModuleImports, + errorHandlerServiceImports, + passwordHasherServiceImports, + pothosImports, + prismaImports, + requestServiceContextImports, + userSessionServiceImports, + userSessionTypesImports, + }, + ...options, + }), + }, + }, + }, + }; + }, +}); + +export const AUTH_CORE_AUTH_EMAIL_PASSWORD_RENDERERS = { + provider: authCoreAuthEmailPasswordRenderers, + task: authCoreAuthEmailPasswordRenderersTask, +}; diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-email-password/generated/ts-import-providers.ts b/plugins/plugin-auth/src/auth/core/generators/auth-email-password/generated/ts-import-providers.ts new file mode 100644 index 000000000..14056b4a1 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-email-password/generated/ts-import-providers.ts @@ -0,0 +1,56 @@ +import type { TsImportMapProviderFromSchema } from '@baseplate-dev/core-generators'; + +import { + createTsImportMap, + createTsImportMapSchema, + packageScope, +} from '@baseplate-dev/core-generators'; +import { + createGeneratorTask, + createReadOnlyProviderType, +} from '@baseplate-dev/sync'; + +import { AUTH_CORE_AUTH_EMAIL_PASSWORD_PATHS } from './template-paths.js'; + +const authEmailPasswordImportsSchema = createTsImportMapSchema({ + authenticateUserWithEmailAndPassword: {}, + createUserWithEmailAndPassword: {}, + PASSWORD_MIN_LENGTH: {}, +}); + +export type AuthEmailPasswordImportsProvider = TsImportMapProviderFromSchema< + typeof authEmailPasswordImportsSchema +>; + +export const authEmailPasswordImportsProvider = + createReadOnlyProviderType( + 'auth-email-password-imports', + ); + +const authCoreAuthEmailPasswordImportsTask = createGeneratorTask({ + dependencies: { + paths: AUTH_CORE_AUTH_EMAIL_PASSWORD_PATHS.provider, + }, + exports: { + authEmailPasswordImports: + authEmailPasswordImportsProvider.export(packageScope), + }, + run({ paths }) { + return { + providers: { + authEmailPasswordImports: createTsImportMap( + authEmailPasswordImportsSchema, + { + authenticateUserWithEmailAndPassword: paths.servicesUserPassword, + createUserWithEmailAndPassword: paths.servicesUserPassword, + PASSWORD_MIN_LENGTH: paths.constantsPassword, + }, + ), + }, + }; + }, +}); + +export const AUTH_CORE_AUTH_EMAIL_PASSWORD_IMPORTS = { + task: authCoreAuthEmailPasswordImportsTask, +}; diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-email-password/generated/typed-templates.ts b/plugins/plugin-auth/src/auth/core/generators/auth-email-password/generated/typed-templates.ts new file mode 100644 index 000000000..45c169131 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-email-password/generated/typed-templates.ts @@ -0,0 +1,78 @@ +import { createTsTemplateFile } from '@baseplate-dev/core-generators'; +import { + errorHandlerServiceImportsProvider, + passwordHasherServiceImportsProvider, + pothosImportsProvider, + prismaImportsProvider, + requestServiceContextImportsProvider, + userSessionServiceImportsProvider, + userSessionTypesImportsProvider, +} from '@baseplate-dev/fastify-generators'; +import path from 'node:path'; + +import { authModuleImportsProvider } from '#src/auth/core/generators/auth-module/generated/ts-import-providers.js'; + +const constantsPassword = createTsTemplateFile({ + fileOptions: { kind: 'singleton' }, + group: 'module', + importMapProviders: {}, + name: 'constants-password', + projectExports: { PASSWORD_MIN_LENGTH: {} }, + source: { + path: path.join( + import.meta.dirname, + '../templates/module/constants/password.constants.ts', + ), + }, + variables: {}, +}); + +const schemaUserPasswordMutations = createTsTemplateFile({ + fileOptions: { kind: 'singleton' }, + group: 'module', + importMapProviders: { + authModuleImports: authModuleImportsProvider, + pothosImports: pothosImportsProvider, + }, + name: 'schema-user-password-mutations', + source: { + path: path.join( + import.meta.dirname, + '../templates/module/schema/user-password.mutations.ts', + ), + }, + variables: {}, +}); + +const servicesUserPassword = createTsTemplateFile({ + fileOptions: { kind: 'singleton' }, + group: 'module', + importMapProviders: { + errorHandlerServiceImports: errorHandlerServiceImportsProvider, + passwordHasherServiceImports: passwordHasherServiceImportsProvider, + prismaImports: prismaImportsProvider, + requestServiceContextImports: requestServiceContextImportsProvider, + userSessionServiceImports: userSessionServiceImportsProvider, + userSessionTypesImports: userSessionTypesImportsProvider, + }, + name: 'services-user-password', + projectExports: { + authenticateUserWithEmailAndPassword: {}, + createUserWithEmailAndPassword: {}, + }, + source: { + path: path.join( + import.meta.dirname, + '../templates/module/services/user-password.service.ts', + ), + }, + variables: {}, +}); + +export const moduleGroup = { + constantsPassword, + schemaUserPasswordMutations, + servicesUserPassword, +}; + +export const AUTH_CORE_AUTH_EMAIL_PASSWORD_TEMPLATES = { moduleGroup }; diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-email-password/index.ts b/plugins/plugin-auth/src/auth/core/generators/auth-email-password/index.ts new file mode 100644 index 000000000..8f548cfd5 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-email-password/index.ts @@ -0,0 +1,3 @@ +export * from './auth-email-password.generator.js'; +export type { AuthEmailPasswordImportsProvider } from './generated/ts-import-providers.js'; +export { authEmailPasswordImportsProvider } from './generated/ts-import-providers.js'; diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-email-password/templates/module/constants/password.constants.ts b/plugins/plugin-auth/src/auth/core/generators/auth-email-password/templates/module/constants/password.constants.ts new file mode 100644 index 000000000..fd1f1b341 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-email-password/templates/module/constants/password.constants.ts @@ -0,0 +1,3 @@ +// @ts-nocheck + +export const PASSWORD_MIN_LENGTH = 8; diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-email-password/templates/module/schema/user-password.mutations.ts b/plugins/plugin-auth/src/auth/core/generators/auth-email-password/templates/module/schema/user-password.mutations.ts new file mode 100644 index 000000000..80deabd4d --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-email-password/templates/module/schema/user-password.mutations.ts @@ -0,0 +1,45 @@ +// @ts-nocheck + +import { userSessionPayload } from '%authModuleImports'; +import { builder } from '%pothosImports'; + +import { + authenticateUserWithEmailAndPassword, + createUserWithEmailAndPassword, +} from '../services/user-password.service.js'; + +builder.mutationField('registerWithEmailPassword', (t) => + t.fieldWithInputPayload({ + authorize: ['public'], + payload: { + session: t.payload.field({ type: userSessionPayload }), + }, + input: { + email: t.input.field({ required: true, type: 'String' }), + password: t.input.field({ required: true, type: 'String' }), + }, + resolve: async (root, { input }, context) => + createUserWithEmailAndPassword({ + input, + context, + }), + }), +); + +builder.mutationField('loginWithEmailPassword', (t) => + t.fieldWithInputPayload({ + authorize: ['public'], + payload: { + session: t.payload.field({ type: userSessionPayload }), + }, + input: { + email: t.input.field({ required: true, type: 'String' }), + password: t.input.field({ required: true, type: 'String' }), + }, + resolve: async (root, { input }, context) => + authenticateUserWithEmailAndPassword({ + input, + context, + }), + }), +); diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-email-password/templates/module/services/user-password.service.ts b/plugins/plugin-auth/src/auth/core/generators/auth-email-password/templates/module/services/user-password.service.ts new file mode 100644 index 000000000..9664df982 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-email-password/templates/module/services/user-password.service.ts @@ -0,0 +1,122 @@ +// @ts-nocheck + +import type { RequestServiceContext } from '%requestServiceContextImports'; +import type { UserSessionPayload } from '%userSessionTypesImports'; + +import { + BadRequestError, + handleZodRequestValidationError, +} from '%errorHandlerServiceImports'; +import { + createPasswordHash, + verifyPasswordHash, +} from '%passwordHasherServiceImports'; +import { prisma } from '%prismaImports'; +import { userSessionService } from '%userSessionServiceImports'; +import z from 'zod'; + +import { PASSWORD_MIN_LENGTH } from '../constants/password.constants.js'; + +const PROVIDER_ID = 'email-password'; + +const MAX_VALUE_LENGTH = 255; + +const emailPasswordSchema = z.object({ + email: z + .string() + .email() + .max(MAX_VALUE_LENGTH) + .transform((value) => value.toLowerCase()), + password: z.string().min(PASSWORD_MIN_LENGTH).max(MAX_VALUE_LENGTH), +}); + +export async function createUserWithEmailAndPassword({ + input, + context, +}: { + input: { + email: string; + password: string; + }; + context: RequestServiceContext; +}): Promise<{ session: UserSessionPayload }> { + const { email, password } = await emailPasswordSchema + .parseAsync(input) + .catch(handleZodRequestValidationError); + // check if user with that email already exists + const existingUser = await prisma.userAccount.findUnique({ + where: { + accountId_providerId: { + accountId: email, + providerId: PROVIDER_ID, + }, + }, + }); + + if (existingUser !== null) { + throw new BadRequestError('Email already taken', 'email-taken'); + } + + // create user + const user = await prisma.user.create({ + data: { + email, + accounts: { + create: { + accountId: email, + providerId: PROVIDER_ID, + password: await createPasswordHash(password), + }, + }, + }, + }); + + const session = await userSessionService.createSession(user.id, context); + + return { session }; +} + +export async function authenticateUserWithEmailAndPassword({ + input, + context, +}: { + input: { + email: string; + password: string; + }; + context: RequestServiceContext; +}): Promise<{ session: UserSessionPayload }> { + const { email, password } = await emailPasswordSchema + .parseAsync(input) + .catch(handleZodRequestValidationError); + + // check if user with that email exists + const userAccount = await prisma.userAccount.findUnique({ + where: { + accountId_providerId: { + accountId: email, + providerId: PROVIDER_ID, + }, + }, + }); + + if (userAccount === null) { + throw new BadRequestError('Invalid email', 'invalid-email'); + } + + // check for password match + const isValid = await verifyPasswordHash( + userAccount.password ?? '', + password, + ); + if (!isValid) { + throw new BadRequestError('Invalid password', 'invalid-password'); + } + + const session = await userSessionService.createSession( + userAccount.userId, + context, + ); + + return { session }; +} diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-hooks/auth-hooks.generator.ts b/plugins/plugin-auth/src/auth/core/generators/auth-hooks/auth-hooks.generator.ts new file mode 100644 index 000000000..7890a5332 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-hooks/auth-hooks.generator.ts @@ -0,0 +1,42 @@ +import { renderTextTemplateFileAction } from '@baseplate-dev/core-generators'; +import { createGenerator, createGeneratorTask } from '@baseplate-dev/sync'; +import { z } from 'zod'; + +import { AUTH_CORE_AUTH_HOOKS_GENERATED as GENERATED_TEMPLATES } from './generated/index.js'; + +const descriptorSchema = z.object({}); + +/** + * Placeholder generator for auth hooks. + * + * Useful for creating a test auth implementation. + */ +export const authHooksGenerator = createGenerator({ + name: 'auth/core/auth-hooks', + generatorFileUrl: import.meta.url, + descriptorSchema, + buildTasks: () => ({ + paths: GENERATED_TEMPLATES.paths.task, + imports: GENERATED_TEMPLATES.imports.task, + renderers: GENERATED_TEMPLATES.renderers.task, + main: createGeneratorTask({ + dependencies: { + paths: GENERATED_TEMPLATES.paths.provider, + renderers: GENERATED_TEMPLATES.renderers.provider, + }, + run({ paths, renderers }) { + return { + build: async (builder) => { + await builder.apply(renderers.hooksGroup.render({})); + await builder.apply( + renderTextTemplateFileAction({ + template: GENERATED_TEMPLATES.templates.useCurrentUserGql, + destination: paths.useCurrentUserGql, + }), + ); + }, + }; + }, + }), + }), +}); diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-hooks/extractor.json b/plugins/plugin-auth/src/auth/core/generators/auth-hooks/extractor.json new file mode 100644 index 000000000..0a3d8fb86 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-hooks/extractor.json @@ -0,0 +1,90 @@ +{ + "name": "auth/core/auth-hooks", + "extractors": { + "ts": { + "importProviders": [ + "@baseplate-dev/react-generators:authHooksImportsProvider" + ], + "skipDefaultImportMap": true + } + }, + "templates": { + "src/hooks/use-current-user.gql": { + "name": "use-current-user-gql", + "type": "text", + "fileOptions": { "kind": "singleton" }, + "pathRootRelativePath": "{src-root}/hooks/use-current-user.gql", + "variables": {} + }, + "src/hooks/use-current-user.ts": { + "name": "use-current-user", + "type": "ts", + "fileOptions": { "kind": "singleton" }, + "generator": "@baseplate-dev/plugin-auth#auth/core/auth-hooks", + "group": "hooks", + "importMapProviders": { + "generatedGraphqlImportsProvider": { + "importName": "generatedGraphqlImportsProvider", + "packagePathSpecifier": "@baseplate-dev/react-generators:src/generators/apollo/react-apollo/providers/generated-graphql.ts" + } + }, + "pathRootRelativePath": "{src-root}/hooks/use-current-user.ts", + "projectExports": { "useCurrentUser": {} }, + "variables": {} + }, + "src/hooks/use-log-out.ts": { + "name": "use-log-out", + "type": "ts", + "fileOptions": { "kind": "singleton" }, + "generator": "@baseplate-dev/plugin-auth#auth/core/auth-hooks", + "group": "hooks", + "importMapProviders": { + "generatedGraphqlImportsProvider": { + "importName": "generatedGraphqlImportsProvider", + "packagePathSpecifier": "@baseplate-dev/react-generators:src/generators/apollo/react-apollo/providers/generated-graphql.ts" + }, + "reactErrorImportsProvider": { + "importName": "reactErrorImportsProvider", + "packagePathSpecifier": "@baseplate-dev/react-generators:src/generators/core/react-error/generated/ts-import-providers.ts" + }, + "reactSessionImportsProvider": { + "importName": "reactSessionImportsProvider", + "packagePathSpecifier": "@baseplate-dev/plugin-auth:src/auth/core/generators/react-session/generated/ts-import-providers.ts" + } + }, + "pathRootRelativePath": "{src-root}/hooks/use-log-out.ts", + "projectExports": { "useLogOut": {} }, + "variables": {} + }, + "src/hooks/use-session.ts": { + "name": "use-session", + "type": "ts", + "fileOptions": { "kind": "singleton" }, + "generator": "@baseplate-dev/plugin-auth#auth/core/auth-hooks", + "group": "hooks", + "importMapProviders": { + "reactSessionImportsProvider": { + "importName": "reactSessionImportsProvider", + "packagePathSpecifier": "@baseplate-dev/plugin-auth:src/auth/core/generators/react-session/generated/ts-import-providers.ts" + } + }, + "pathRootRelativePath": "{src-root}/hooks/use-session.ts", + "projectExports": { + "SessionData": { "isTypeOnly": true }, + "useSession": {} + }, + "variables": {} + }, + "src/hooks/use-user-id-or-throw.ts": { + "name": "use-required-user-id", + "type": "ts", + "fileOptions": { "kind": "singleton" }, + "generator": "@baseplate-dev/plugin-auth#auth/core/auth-hooks", + "group": "hooks", + "importMapProviders": {}, + "pathRootRelativePath": "{src-root}/hooks/use-user-id-or-throw.ts", + "projectExports": { "useRequiredUserId": {} }, + "variables": {} + } + } +} diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-hooks/generated/index.ts b/plugins/plugin-auth/src/auth/core/generators/auth-hooks/generated/index.ts new file mode 100644 index 000000000..0af3e8f97 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-hooks/generated/index.ts @@ -0,0 +1,11 @@ +import { AUTH_CORE_AUTH_HOOKS_PATHS } from './template-paths.js'; +import { AUTH_CORE_AUTH_HOOKS_RENDERERS } from './template-renderers.js'; +import { AUTH_CORE_AUTH_HOOKS_IMPORTS } from './ts-import-providers.js'; +import { AUTH_CORE_AUTH_HOOKS_TEMPLATES } from './typed-templates.js'; + +export const AUTH_CORE_AUTH_HOOKS_GENERATED = { + imports: AUTH_CORE_AUTH_HOOKS_IMPORTS, + paths: AUTH_CORE_AUTH_HOOKS_PATHS, + renderers: AUTH_CORE_AUTH_HOOKS_RENDERERS, + templates: AUTH_CORE_AUTH_HOOKS_TEMPLATES, +}; diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-hooks/generated/template-paths.ts b/plugins/plugin-auth/src/auth/core/generators/auth-hooks/generated/template-paths.ts new file mode 100644 index 000000000..e5e3408da --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-hooks/generated/template-paths.ts @@ -0,0 +1,39 @@ +import { packageInfoProvider } from '@baseplate-dev/core-generators'; +import { createGeneratorTask, createProviderType } from '@baseplate-dev/sync'; + +export interface AuthCoreAuthHooksPaths { + useCurrentUserGql: string; + useCurrentUser: string; + useLogOut: string; + useSession: string; + useRequiredUserId: string; +} + +const authCoreAuthHooksPaths = createProviderType( + 'auth-core-auth-hooks-paths', +); + +const authCoreAuthHooksPathsTask = createGeneratorTask({ + dependencies: { packageInfo: packageInfoProvider }, + exports: { authCoreAuthHooksPaths: authCoreAuthHooksPaths.export() }, + run({ packageInfo }) { + const srcRoot = packageInfo.getPackageSrcPath(); + + return { + providers: { + authCoreAuthHooksPaths: { + useCurrentUser: `${srcRoot}/hooks/use-current-user.ts`, + useCurrentUserGql: `${srcRoot}/hooks/use-current-user.gql`, + useLogOut: `${srcRoot}/hooks/use-log-out.ts`, + useRequiredUserId: `${srcRoot}/hooks/use-user-id-or-throw.ts`, + useSession: `${srcRoot}/hooks/use-session.ts`, + }, + }, + }; + }, +}); + +export const AUTH_CORE_AUTH_HOOKS_PATHS = { + provider: authCoreAuthHooksPaths, + task: authCoreAuthHooksPathsTask, +}; diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-hooks/generated/template-renderers.ts b/plugins/plugin-auth/src/auth/core/generators/auth-hooks/generated/template-renderers.ts new file mode 100644 index 000000000..701e8f178 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-hooks/generated/template-renderers.ts @@ -0,0 +1,75 @@ +import type { RenderTsTemplateGroupActionInput } from '@baseplate-dev/core-generators'; +import type { BuilderAction } from '@baseplate-dev/sync'; + +import { typescriptFileProvider } from '@baseplate-dev/core-generators'; +import { + generatedGraphqlImportsProvider, + reactErrorImportsProvider, +} from '@baseplate-dev/react-generators'; +import { createGeneratorTask, createProviderType } from '@baseplate-dev/sync'; + +import { reactSessionImportsProvider } from '#src/auth/core/generators/react-session/generated/ts-import-providers.js'; + +import { AUTH_CORE_AUTH_HOOKS_PATHS } from './template-paths.js'; +import { AUTH_CORE_AUTH_HOOKS_TEMPLATES } from './typed-templates.js'; + +export interface AuthCoreAuthHooksRenderers { + hooksGroup: { + render: ( + options: Omit< + RenderTsTemplateGroupActionInput< + typeof AUTH_CORE_AUTH_HOOKS_TEMPLATES.hooksGroup + >, + 'importMapProviders' | 'group' | 'paths' + >, + ) => BuilderAction; + }; +} + +const authCoreAuthHooksRenderers = + createProviderType( + 'auth-core-auth-hooks-renderers', + ); + +const authCoreAuthHooksRenderersTask = createGeneratorTask({ + dependencies: { + generatedGraphqlImports: generatedGraphqlImportsProvider, + paths: AUTH_CORE_AUTH_HOOKS_PATHS.provider, + reactErrorImports: reactErrorImportsProvider, + reactSessionImports: reactSessionImportsProvider, + typescriptFile: typescriptFileProvider, + }, + exports: { authCoreAuthHooksRenderers: authCoreAuthHooksRenderers.export() }, + run({ + generatedGraphqlImports, + paths, + reactErrorImports, + reactSessionImports, + typescriptFile, + }) { + return { + providers: { + authCoreAuthHooksRenderers: { + hooksGroup: { + render: (options) => + typescriptFile.renderTemplateGroup({ + group: AUTH_CORE_AUTH_HOOKS_TEMPLATES.hooksGroup, + paths, + importMapProviders: { + generatedGraphqlImports, + reactErrorImports, + reactSessionImports, + }, + ...options, + }), + }, + }, + }, + }; + }, +}); + +export const AUTH_CORE_AUTH_HOOKS_RENDERERS = { + provider: authCoreAuthHooksRenderers, + task: authCoreAuthHooksRenderersTask, +}; diff --git a/packages/react-generators/src/generators/auth/placeholder-auth-hooks/generated/ts-import-providers.ts b/plugins/plugin-auth/src/auth/core/generators/auth-hooks/generated/ts-import-providers.ts similarity index 66% rename from packages/react-generators/src/generators/auth/placeholder-auth-hooks/generated/ts-import-providers.ts rename to plugins/plugin-auth/src/auth/core/generators/auth-hooks/generated/ts-import-providers.ts index 278c0e5d3..e280d23bb 100644 --- a/packages/react-generators/src/generators/auth/placeholder-auth-hooks/generated/ts-import-providers.ts +++ b/plugins/plugin-auth/src/auth/core/generators/auth-hooks/generated/ts-import-providers.ts @@ -2,18 +2,17 @@ import { createTsImportMap, packageScope, } from '@baseplate-dev/core-generators'; -import { createGeneratorTask } from '@baseplate-dev/sync'; - import { authHooksImportsProvider, authHooksImportsSchema, -} from '#src/generators/auth/_providers/auth-hooks.js'; +} from '@baseplate-dev/react-generators'; +import { createGeneratorTask } from '@baseplate-dev/sync'; -import { AUTH_PLACEHOLDER_AUTH_HOOKS_PATHS } from './template-paths.js'; +import { AUTH_CORE_AUTH_HOOKS_PATHS } from './template-paths.js'; -const authPlaceholderAuthHooksImportsTask = createGeneratorTask({ +const authCoreAuthHooksImportsTask = createGeneratorTask({ dependencies: { - paths: AUTH_PLACEHOLDER_AUTH_HOOKS_PATHS.provider, + paths: AUTH_CORE_AUTH_HOOKS_PATHS.provider, }, exports: { authHooksImports: authHooksImportsProvider.export(packageScope) }, run({ paths }) { @@ -31,6 +30,6 @@ const authPlaceholderAuthHooksImportsTask = createGeneratorTask({ }, }); -export const AUTH_PLACEHOLDER_AUTH_HOOKS_IMPORTS = { - task: authPlaceholderAuthHooksImportsTask, +export const AUTH_CORE_AUTH_HOOKS_IMPORTS = { + task: authCoreAuthHooksImportsTask, }; diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-hooks/generated/typed-templates.ts b/plugins/plugin-auth/src/auth/core/generators/auth-hooks/generated/typed-templates.ts new file mode 100644 index 000000000..e61039242 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-hooks/generated/typed-templates.ts @@ -0,0 +1,98 @@ +import { + createTextTemplateFile, + createTsTemplateFile, +} from '@baseplate-dev/core-generators'; +import { + generatedGraphqlImportsProvider, + reactErrorImportsProvider, +} from '@baseplate-dev/react-generators'; +import path from 'node:path'; + +import { reactSessionImportsProvider } from '#src/auth/core/generators/react-session/generated/ts-import-providers.js'; + +const useCurrentUser = createTsTemplateFile({ + fileOptions: { kind: 'singleton' }, + group: 'hooks', + importMapProviders: { + generatedGraphqlImports: generatedGraphqlImportsProvider, + }, + name: 'use-current-user', + projectExports: { useCurrentUser: {} }, + source: { + path: path.join( + import.meta.dirname, + '../templates/src/hooks/use-current-user.ts', + ), + }, + variables: {}, +}); + +const useLogOut = createTsTemplateFile({ + fileOptions: { kind: 'singleton' }, + group: 'hooks', + importMapProviders: { + generatedGraphqlImports: generatedGraphqlImportsProvider, + reactErrorImports: reactErrorImportsProvider, + reactSessionImports: reactSessionImportsProvider, + }, + name: 'use-log-out', + projectExports: { useLogOut: {} }, + source: { + path: path.join( + import.meta.dirname, + '../templates/src/hooks/use-log-out.ts', + ), + }, + variables: {}, +}); + +const useRequiredUserId = createTsTemplateFile({ + fileOptions: { kind: 'singleton' }, + group: 'hooks', + importMapProviders: {}, + name: 'use-required-user-id', + projectExports: { useRequiredUserId: {} }, + source: { + path: path.join( + import.meta.dirname, + '../templates/src/hooks/use-user-id-or-throw.ts', + ), + }, + variables: {}, +}); + +const useSession = createTsTemplateFile({ + fileOptions: { kind: 'singleton' }, + group: 'hooks', + importMapProviders: { reactSessionImports: reactSessionImportsProvider }, + name: 'use-session', + projectExports: { SessionData: { isTypeOnly: true }, useSession: {} }, + source: { + path: path.join( + import.meta.dirname, + '../templates/src/hooks/use-session.ts', + ), + }, + variables: {}, +}); + +export const hooksGroup = { + useCurrentUser, + useLogOut, + useRequiredUserId, + useSession, +}; + +const useCurrentUserGql = createTextTemplateFile({ + fileOptions: { kind: 'singleton' }, + name: 'use-current-user-gql', + source: { + path: path.join( + import.meta.dirname, + '../templates/src/hooks/use-current-user.gql', + ), + }, + variables: {}, +}); + +export const AUTH_CORE_AUTH_HOOKS_TEMPLATES = { useCurrentUserGql, hooksGroup }; diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-hooks/index.ts b/plugins/plugin-auth/src/auth/core/generators/auth-hooks/index.ts new file mode 100644 index 000000000..a29d229b6 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-hooks/index.ts @@ -0,0 +1 @@ +export * from './auth-hooks.generator.js'; diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-hooks/templates/src/hooks/use-current-user.gql b/plugins/plugin-auth/src/auth/core/generators/auth-hooks/templates/src/hooks/use-current-user.gql new file mode 100644 index 000000000..903173c05 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-hooks/templates/src/hooks/use-current-user.gql @@ -0,0 +1,10 @@ +fragment CurrentUser on User { + id + email +} + +query getUserById($id: Uuid!) { + user(id: $id) { + ...CurrentUser + } +} diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-hooks/templates/src/hooks/use-current-user.ts b/plugins/plugin-auth/src/auth/core/generators/auth-hooks/templates/src/hooks/use-current-user.ts new file mode 100644 index 000000000..2b337c904 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-hooks/templates/src/hooks/use-current-user.ts @@ -0,0 +1,40 @@ +// @ts-nocheck + +import type { CurrentUserFragment } from '%generatedGraphqlImports'; + +import { GetUserByIdDocument } from '%generatedGraphqlImports'; +import { useQuery } from '@apollo/client'; + +import { useSession } from './use-session.js'; + +/** + * Result returned by useCurrentUser hook + */ +export interface UseCurrentUserResult { + /** Current user data from GraphQL */ + user?: CurrentUserFragment; + /** Whether user data is loading */ + loading: boolean; + /** Any error that occurred while fetching user data */ + error?: Error | undefined; +} + +/** + * Fetches information about the current user via GraphQL + * @returns A result containing the current user or an error if the user is not authenticated + */ +export function useCurrentUser(): UseCurrentUserResult { + const session = useSession(); + const { data, loading, error } = useQuery(GetUserByIdDocument, { + variables: { id: session?.userId ?? '' }, + skip: !session, + }); + + const noUserError = !session ? new Error('No user logged in') : undefined; + + return { + user: data?.user, + loading, + error: error ?? noUserError, + }; +} diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-hooks/templates/src/hooks/use-log-out.ts b/plugins/plugin-auth/src/auth/core/generators/auth-hooks/templates/src/hooks/use-log-out.ts new file mode 100644 index 000000000..f55f90b14 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-hooks/templates/src/hooks/use-log-out.ts @@ -0,0 +1,29 @@ +// @ts-nocheck + +import { LogOutDocument } from '%generatedGraphqlImports'; +import { logAndFormatError, logError } from '%reactErrorImports'; +import { useUserSessionClient } from '%reactSessionImports'; +import { useApolloClient, useMutation } from '@apollo/client'; +import { useNavigate } from '@tanstack/react-router'; +import { toast } from 'sonner'; + +export function useLogOut(): () => void { + const [logOut] = useMutation(LogOutDocument); + const { client } = useUserSessionClient(); + const apolloClient = useApolloClient(); + const navigate = useNavigate(); + + return () => { + logOut() + .then(() => { + client.signOut(); + // Make sure to reset the Apollo client to clear any cached data + apolloClient.clearStore().catch(logError); + toast.success('You have been successfully logged out!'); + navigate({ to: '/' }).catch(logError); + }) + .catch((err: unknown) => { + toast.error(logAndFormatError(err, 'Sorry, we could not log you out.')); + }); + }; +} diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-hooks/templates/src/hooks/use-session.ts b/plugins/plugin-auth/src/auth/core/generators/auth-hooks/templates/src/hooks/use-session.ts new file mode 100644 index 000000000..2883b9fc9 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-hooks/templates/src/hooks/use-session.ts @@ -0,0 +1,16 @@ +// @ts-nocheck + +import type { UserSessionData } from '%reactSessionImports'; + +import { useUserSessionClient } from '%reactSessionImports'; + +/** + * Provides the current session data such as the user id and whether the user is authenticated + * This is the primary hook for accessing authentication state + * @returns Current session data with computed isAuthenticated + */ +export function useSession(): UserSessionData | undefined { + const { session } = useUserSessionClient(); + + return session; +} diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-hooks/templates/src/hooks/use-user-id-or-throw.ts b/plugins/plugin-auth/src/auth/core/generators/auth-hooks/templates/src/hooks/use-user-id-or-throw.ts new file mode 100644 index 000000000..5bd6c4a89 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-hooks/templates/src/hooks/use-user-id-or-throw.ts @@ -0,0 +1,16 @@ +// @ts-nocheck + +import { useSession } from './use-session.js'; + +/** + * Provides the current user id or throws an error if the user is not authenticated + * @returns Current user ID + * @throws Error if user is not authenticated + */ +export function useUserIdOrThrow(): string { + const session = useSession(); + if (!session) { + throw new Error('User not authenticated'); + } + return session.userId; +} diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-module/auth-module.generator.ts b/plugins/plugin-auth/src/auth/core/generators/auth-module/auth-module.generator.ts new file mode 100644 index 000000000..3541764ab --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-module/auth-module.generator.ts @@ -0,0 +1,82 @@ +import { tsCodeFragment } from '@baseplate-dev/core-generators'; +import { + configServiceProvider, + createPothosPrismaObjectTypeOutputName, + pothosTypeOutputProvider, + prismaOutputProvider, +} from '@baseplate-dev/fastify-generators'; +import { + createGenerator, + createGeneratorTask, + createProviderTask, +} from '@baseplate-dev/sync'; +import { z } from 'zod'; + +import { AUTH_CORE_AUTH_MODULE_GENERATED as GENERATED_TEMPLATES } from './generated'; + +const descriptorSchema = z.object({ + userSessionModelName: z.string().min(1), + userModelName: z.string().min(1), +}); + +export const authModuleGenerator = createGenerator({ + name: 'auth/core/auth-module', + generatorFileUrl: import.meta.url, + descriptorSchema, + buildTasks: ({ userSessionModelName, userModelName }) => ({ + paths: GENERATED_TEMPLATES.paths.task, + imports: GENERATED_TEMPLATES.imports.task, + renderers: GENERATED_TEMPLATES.renderers.task, + config: createProviderTask(configServiceProvider, (configService) => { + configService.configFields.set('AUTH_SECRET', { + validator: tsCodeFragment( + 'z.string().regex(/^[a-zA-Z0-9-_+=/]{20,}$/)', + ), + comment: + 'Secret key for signing auth cookie (at least 20 alphanumeric characters)', + seedValue: 'a-secret-key-1234567890', + exampleValue: '', + }); + }), + main: createGeneratorTask({ + dependencies: { + prismaOutput: prismaOutputProvider, + renderers: GENERATED_TEMPLATES.renderers.provider, + userObjectType: pothosTypeOutputProvider + .dependency() + .reference(createPothosPrismaObjectTypeOutputName(userModelName)), + }, + run({ prismaOutput, renderers, userObjectType }) { + return { + providers: { + authModule: {}, + }, + build: async (builder) => { + await builder.apply( + renderers.userSessionService.render({ + variables: { + TPL_PRISMA_USER_SESSION: + prismaOutput.getPrismaModelFragment(userSessionModelName), + }, + }), + ); + await builder.apply(renderers.constantsGroup.render({})); + await builder.apply(renderers.utilsGroup.render({})); + await builder.apply( + renderers.moduleGroup.render({ + variables: { + schemaUserSessionPayloadObjectType: { + TPL_PRISMA_USER: + prismaOutput.getPrismaModelFragment(userModelName), + TPL_USER_OBJECT_TYPE: + userObjectType.getTypeReference().fragment, + }, + }, + }), + ); + }, + }; + }, + }), + }), +}); diff --git a/plugins/plugin-auth/src/auth/generators/fastify/auth-module/extractor.json b/plugins/plugin-auth/src/auth/core/generators/auth-module/extractor.json similarity index 59% rename from plugins/plugin-auth/src/auth/generators/fastify/auth-module/extractor.json rename to plugins/plugin-auth/src/auth/core/generators/auth-module/extractor.json index d0023c2c5..461edb6d9 100644 --- a/plugins/plugin-auth/src/auth/generators/fastify/auth-module/extractor.json +++ b/plugins/plugin-auth/src/auth/core/generators/auth-module/extractor.json @@ -1,11 +1,10 @@ { - "name": "fastify/auth-module", + "name": "auth/core/auth-module", "extractors": { "ts": { "importProviders": [ "@baseplate-dev/fastify-generators:userSessionServiceImportsProvider" - ], - "skipDefaultImportMap": true + ] } }, "templates": { @@ -13,17 +12,63 @@ "name": "user-session-constants", "type": "ts", "fileOptions": { "kind": "singleton" }, - "generator": "@baseplate-dev/plugin-auth#fastify/auth-module", + "generator": "@baseplate-dev/plugin-auth#auth/core/auth-module", "group": "constants", "importMapProviders": {}, "pathRootRelativePath": "{module-root}/constants/user-session.constants.ts", "variables": {} }, + "module/schema/user-session-payload.object-type.ts": { + "name": "schema-user-session-payload-object-type", + "type": "ts", + "fileOptions": { "kind": "singleton" }, + "generator": "@baseplate-dev/plugin-auth#auth/core/auth-module", + "group": "module", + "importMapProviders": { + "pothosImportsProvider": { + "importName": "pothosImportsProvider", + "packagePathSpecifier": "@baseplate-dev/fastify-generators:src/generators/pothos/pothos/generated/ts-import-providers.ts" + } + }, + "pathRootRelativePath": "{module-root}/schema/user-session-payload.object-type.ts", + "projectExports": { "userSessionPayload": {} }, + "variables": { "TPL_PRISMA_USER": {}, "TPL_USER_OBJECT_TYPE": {} } + }, + "module/schema/user-session.mutations.ts": { + "name": "schema-user-session-mutations", + "type": "ts", + "fileOptions": { "kind": "singleton" }, + "generator": "@baseplate-dev/plugin-auth#auth/core/auth-module", + "group": "module", + "importMapProviders": { + "pothosImportsProvider": { + "importName": "pothosImportsProvider", + "packagePathSpecifier": "@baseplate-dev/fastify-generators:src/generators/pothos/pothos/generated/ts-import-providers.ts" + } + }, + "pathRootRelativePath": "{module-root}/schema/user-session.mutations.ts", + "variables": {} + }, + "module/schema/user-session.queries.ts": { + "name": "schema-user-session-queries", + "type": "ts", + "fileOptions": { "kind": "singleton" }, + "generator": "@baseplate-dev/plugin-auth#auth/core/auth-module", + "group": "module", + "importMapProviders": { + "pothosImportsProvider": { + "importName": "pothosImportsProvider", + "packagePathSpecifier": "@baseplate-dev/fastify-generators:src/generators/pothos/pothos/generated/ts-import-providers.ts" + } + }, + "pathRootRelativePath": "{module-root}/schema/user-session.queries.ts", + "variables": {} + }, "module/services/user-session.service.ts": { "name": "user-session-service", "type": "ts", "fileOptions": { "kind": "singleton" }, - "generator": "@baseplate-dev/plugin-auth#fastify/auth-module", + "generator": "@baseplate-dev/plugin-auth#auth/core/auth-module", "importMapProviders": { "authContextImportsProvider": { "importName": "authContextImportsProvider", @@ -58,7 +103,7 @@ "name": "cookie-signer", "type": "ts", "fileOptions": { "kind": "singleton" }, - "generator": "@baseplate-dev/plugin-auth#fastify/auth-module", + "generator": "@baseplate-dev/plugin-auth#auth/core/auth-module", "group": "utils", "importMapProviders": {}, "pathRootRelativePath": "{module-root}/utils/cookie-signer.ts", @@ -68,7 +113,7 @@ "name": "session-cookie", "type": "ts", "fileOptions": { "kind": "singleton" }, - "generator": "@baseplate-dev/plugin-auth#fastify/auth-module", + "generator": "@baseplate-dev/plugin-auth#auth/core/auth-module", "group": "utils", "importMapProviders": { "configServiceImportsProvider": { @@ -83,7 +128,7 @@ "name": "verify-request-origin", "type": "ts", "fileOptions": { "kind": "singleton" }, - "generator": "@baseplate-dev/plugin-auth#fastify/auth-module", + "generator": "@baseplate-dev/plugin-auth#auth/core/auth-module", "group": "utils", "importMapProviders": {}, "pathRootRelativePath": "{module-root}/utils/verify-request-origin.ts", diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-module/generated/index.ts b/plugins/plugin-auth/src/auth/core/generators/auth-module/generated/index.ts new file mode 100644 index 000000000..6c2f70c5e --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-module/generated/index.ts @@ -0,0 +1,11 @@ +import { AUTH_CORE_AUTH_MODULE_PATHS } from './template-paths.js'; +import { AUTH_CORE_AUTH_MODULE_RENDERERS } from './template-renderers.js'; +import { AUTH_CORE_AUTH_MODULE_IMPORTS } from './ts-import-providers.js'; +import { AUTH_CORE_AUTH_MODULE_TEMPLATES } from './typed-templates.js'; + +export const AUTH_CORE_AUTH_MODULE_GENERATED = { + imports: AUTH_CORE_AUTH_MODULE_IMPORTS, + paths: AUTH_CORE_AUTH_MODULE_PATHS, + renderers: AUTH_CORE_AUTH_MODULE_RENDERERS, + templates: AUTH_CORE_AUTH_MODULE_TEMPLATES, +}; diff --git a/plugins/plugin-auth/src/auth/generators/fastify/auth-module/generated/template-paths.ts b/plugins/plugin-auth/src/auth/core/generators/auth-module/generated/template-paths.ts similarity index 51% rename from plugins/plugin-auth/src/auth/generators/fastify/auth-module/generated/template-paths.ts rename to plugins/plugin-auth/src/auth/core/generators/auth-module/generated/template-paths.ts index 83af01a1b..09f6735c7 100644 --- a/plugins/plugin-auth/src/auth/generators/fastify/auth-module/generated/template-paths.ts +++ b/plugins/plugin-auth/src/auth/core/generators/auth-module/generated/template-paths.ts @@ -1,28 +1,34 @@ import { appModuleProvider } from '@baseplate-dev/fastify-generators'; import { createGeneratorTask, createProviderType } from '@baseplate-dev/sync'; -export interface FastifyAuthModulePaths { +export interface AuthCoreAuthModulePaths { userSessionConstants: string; + schemaUserSessionPayloadObjectType: string; + schemaUserSessionMutations: string; + schemaUserSessionQueries: string; userSessionService: string; cookieSigner: string; sessionCookie: string; verifyRequestOrigin: string; } -const fastifyAuthModulePaths = createProviderType( - 'fastify-auth-module-paths', +const authCoreAuthModulePaths = createProviderType( + 'auth-core-auth-module-paths', ); -const fastifyAuthModulePathsTask = createGeneratorTask({ +const authCoreAuthModulePathsTask = createGeneratorTask({ dependencies: { appModule: appModuleProvider }, - exports: { fastifyAuthModulePaths: fastifyAuthModulePaths.export() }, + exports: { authCoreAuthModulePaths: authCoreAuthModulePaths.export() }, run({ appModule }) { const moduleRoot = appModule.getModuleFolder(); return { providers: { - fastifyAuthModulePaths: { + authCoreAuthModulePaths: { cookieSigner: `${moduleRoot}/utils/cookie-signer.ts`, + schemaUserSessionMutations: `${moduleRoot}/schema/user-session.mutations.ts`, + schemaUserSessionPayloadObjectType: `${moduleRoot}/schema/user-session-payload.object-type.ts`, + schemaUserSessionQueries: `${moduleRoot}/schema/user-session.queries.ts`, sessionCookie: `${moduleRoot}/utils/session-cookie.ts`, userSessionConstants: `${moduleRoot}/constants/user-session.constants.ts`, userSessionService: `${moduleRoot}/services/user-session.service.ts`, @@ -33,7 +39,7 @@ const fastifyAuthModulePathsTask = createGeneratorTask({ }, }); -export const FASTIFY_AUTH_MODULE_PATHS = { - provider: fastifyAuthModulePaths, - task: fastifyAuthModulePathsTask, +export const AUTH_CORE_AUTH_MODULE_PATHS = { + provider: authCoreAuthModulePaths, + task: authCoreAuthModulePathsTask, }; diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-module/generated/template-renderers.ts b/plugins/plugin-auth/src/auth/core/generators/auth-module/generated/template-renderers.ts new file mode 100644 index 000000000..113b601d9 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-module/generated/template-renderers.ts @@ -0,0 +1,154 @@ +import type { + RenderTsTemplateFileActionInput, + RenderTsTemplateGroupActionInput, +} from '@baseplate-dev/core-generators'; +import type { BuilderAction } from '@baseplate-dev/sync'; + +import { typescriptFileProvider } from '@baseplate-dev/core-generators'; +import { + authContextImportsProvider, + authRolesImportsProvider, + configServiceImportsProvider, + errorHandlerServiceImportsProvider, + pothosImportsProvider, + requestServiceContextImportsProvider, + userSessionTypesImportsProvider, +} from '@baseplate-dev/fastify-generators'; +import { createGeneratorTask, createProviderType } from '@baseplate-dev/sync'; + +import { AUTH_CORE_AUTH_MODULE_PATHS } from './template-paths.js'; +import { AUTH_CORE_AUTH_MODULE_TEMPLATES } from './typed-templates.js'; + +export interface AuthCoreAuthModuleRenderers { + constantsGroup: { + render: ( + options: Omit< + RenderTsTemplateGroupActionInput< + typeof AUTH_CORE_AUTH_MODULE_TEMPLATES.constantsGroup + >, + 'importMapProviders' | 'group' | 'paths' + >, + ) => BuilderAction; + }; + moduleGroup: { + render: ( + options: Omit< + RenderTsTemplateGroupActionInput< + typeof AUTH_CORE_AUTH_MODULE_TEMPLATES.moduleGroup + >, + 'importMapProviders' | 'group' | 'paths' + >, + ) => BuilderAction; + }; + userSessionService: { + render: ( + options: Omit< + RenderTsTemplateFileActionInput< + typeof AUTH_CORE_AUTH_MODULE_TEMPLATES.userSessionService + >, + 'destination' | 'importMapProviders' | 'template' + >, + ) => BuilderAction; + }; + utilsGroup: { + render: ( + options: Omit< + RenderTsTemplateGroupActionInput< + typeof AUTH_CORE_AUTH_MODULE_TEMPLATES.utilsGroup + >, + 'importMapProviders' | 'group' | 'paths' + >, + ) => BuilderAction; + }; +} + +const authCoreAuthModuleRenderers = + createProviderType( + 'auth-core-auth-module-renderers', + ); + +const authCoreAuthModuleRenderersTask = createGeneratorTask({ + dependencies: { + authContextImports: authContextImportsProvider, + authRolesImports: authRolesImportsProvider, + configServiceImports: configServiceImportsProvider, + errorHandlerServiceImports: errorHandlerServiceImportsProvider, + paths: AUTH_CORE_AUTH_MODULE_PATHS.provider, + pothosImports: pothosImportsProvider, + requestServiceContextImports: requestServiceContextImportsProvider, + typescriptFile: typescriptFileProvider, + userSessionTypesImports: userSessionTypesImportsProvider, + }, + exports: { + authCoreAuthModuleRenderers: authCoreAuthModuleRenderers.export(), + }, + run({ + authContextImports, + authRolesImports, + configServiceImports, + errorHandlerServiceImports, + paths, + pothosImports, + requestServiceContextImports, + typescriptFile, + userSessionTypesImports, + }) { + return { + providers: { + authCoreAuthModuleRenderers: { + constantsGroup: { + render: (options) => + typescriptFile.renderTemplateGroup({ + group: AUTH_CORE_AUTH_MODULE_TEMPLATES.constantsGroup, + paths, + ...options, + }), + }, + moduleGroup: { + render: (options) => + typescriptFile.renderTemplateGroup({ + group: AUTH_CORE_AUTH_MODULE_TEMPLATES.moduleGroup, + paths, + importMapProviders: { + pothosImports, + }, + ...options, + }), + }, + userSessionService: { + render: (options) => + typescriptFile.renderTemplateFile({ + template: AUTH_CORE_AUTH_MODULE_TEMPLATES.userSessionService, + destination: paths.userSessionService, + importMapProviders: { + authContextImports, + authRolesImports, + configServiceImports, + errorHandlerServiceImports, + requestServiceContextImports, + userSessionTypesImports, + }, + ...options, + }), + }, + utilsGroup: { + render: (options) => + typescriptFile.renderTemplateGroup({ + group: AUTH_CORE_AUTH_MODULE_TEMPLATES.utilsGroup, + paths, + importMapProviders: { + configServiceImports, + }, + ...options, + }), + }, + }, + }, + }; + }, +}); + +export const AUTH_CORE_AUTH_MODULE_RENDERERS = { + provider: authCoreAuthModuleRenderers, + task: authCoreAuthModuleRenderersTask, +}; diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-module/generated/ts-import-providers.ts b/plugins/plugin-auth/src/auth/core/generators/auth-module/generated/ts-import-providers.ts new file mode 100644 index 000000000..f5bbee5cc --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-module/generated/ts-import-providers.ts @@ -0,0 +1,56 @@ +import type { TsImportMapProviderFromSchema } from '@baseplate-dev/core-generators'; + +import { + createTsImportMap, + createTsImportMapSchema, + packageScope, +} from '@baseplate-dev/core-generators'; +import { + userSessionServiceImportsProvider, + userSessionServiceImportsSchema, +} from '@baseplate-dev/fastify-generators'; +import { + createGeneratorTask, + createReadOnlyProviderType, +} from '@baseplate-dev/sync'; + +import { AUTH_CORE_AUTH_MODULE_PATHS } from './template-paths.js'; + +const authModuleImportsSchema = createTsImportMapSchema({ + userSessionPayload: {}, +}); + +export type AuthModuleImportsProvider = TsImportMapProviderFromSchema< + typeof authModuleImportsSchema +>; + +export const authModuleImportsProvider = + createReadOnlyProviderType('auth-module-imports'); + +const authCoreAuthModuleImportsTask = createGeneratorTask({ + dependencies: { + paths: AUTH_CORE_AUTH_MODULE_PATHS.provider, + }, + exports: { + authModuleImports: authModuleImportsProvider.export(packageScope), + userSessionServiceImports: + userSessionServiceImportsProvider.export(packageScope), + }, + run({ paths }) { + return { + providers: { + authModuleImports: createTsImportMap(authModuleImportsSchema, { + userSessionPayload: paths.schemaUserSessionPayloadObjectType, + }), + userSessionServiceImports: createTsImportMap( + userSessionServiceImportsSchema, + { userSessionService: paths.userSessionService }, + ), + }, + }; + }, +}); + +export const AUTH_CORE_AUTH_MODULE_IMPORTS = { + task: authCoreAuthModuleImportsTask, +}; diff --git a/plugins/plugin-auth/src/auth/generators/fastify/auth-module/generated/typed-templates.ts b/plugins/plugin-auth/src/auth/core/generators/auth-module/generated/typed-templates.ts similarity index 63% rename from plugins/plugin-auth/src/auth/generators/fastify/auth-module/generated/typed-templates.ts rename to plugins/plugin-auth/src/auth/core/generators/auth-module/generated/typed-templates.ts index 9e0cbd136..7d1fea0b7 100644 --- a/plugins/plugin-auth/src/auth/generators/fastify/auth-module/generated/typed-templates.ts +++ b/plugins/plugin-auth/src/auth/core/generators/auth-module/generated/typed-templates.ts @@ -4,6 +4,7 @@ import { authRolesImportsProvider, configServiceImportsProvider, errorHandlerServiceImportsProvider, + pothosImportsProvider, requestServiceContextImportsProvider, userSessionTypesImportsProvider, } from '@baseplate-dev/fastify-generators'; @@ -25,6 +26,55 @@ const userSessionConstants = createTsTemplateFile({ export const constantsGroup = { userSessionConstants }; +const schemaUserSessionMutations = createTsTemplateFile({ + fileOptions: { kind: 'singleton' }, + group: 'module', + importMapProviders: { pothosImports: pothosImportsProvider }, + name: 'schema-user-session-mutations', + source: { + path: path.join( + import.meta.dirname, + '../templates/module/schema/user-session.mutations.ts', + ), + }, + variables: {}, +}); + +const schemaUserSessionPayloadObjectType = createTsTemplateFile({ + fileOptions: { kind: 'singleton' }, + group: 'module', + importMapProviders: { pothosImports: pothosImportsProvider }, + name: 'schema-user-session-payload-object-type', + projectExports: { userSessionPayload: {} }, + source: { + path: path.join( + import.meta.dirname, + '../templates/module/schema/user-session-payload.object-type.ts', + ), + }, + variables: { TPL_PRISMA_USER: {}, TPL_USER_OBJECT_TYPE: {} }, +}); + +const schemaUserSessionQueries = createTsTemplateFile({ + fileOptions: { kind: 'singleton' }, + group: 'module', + importMapProviders: { pothosImports: pothosImportsProvider }, + name: 'schema-user-session-queries', + source: { + path: path.join( + import.meta.dirname, + '../templates/module/schema/user-session.queries.ts', + ), + }, + variables: {}, +}); + +export const moduleGroup = { + schemaUserSessionMutations, + schemaUserSessionPayloadObjectType, + schemaUserSessionQueries, +}; + const userSessionService = createTsTemplateFile({ fileOptions: { kind: 'singleton' }, importMapProviders: { @@ -90,8 +140,9 @@ const verifyRequestOrigin = createTsTemplateFile({ export const utilsGroup = { cookieSigner, sessionCookie, verifyRequestOrigin }; -export const FASTIFY_AUTH_MODULE_TEMPLATES = { +export const AUTH_CORE_AUTH_MODULE_TEMPLATES = { constantsGroup, + moduleGroup, utilsGroup, userSessionService, }; diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-module/index.ts b/plugins/plugin-auth/src/auth/core/generators/auth-module/index.ts new file mode 100644 index 000000000..2c46377d4 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-module/index.ts @@ -0,0 +1,3 @@ +export * from './auth-module.generator.js'; +export type { AuthModuleImportsProvider } from './generated/ts-import-providers.js'; +export { authModuleImportsProvider } from './generated/ts-import-providers.js'; diff --git a/plugins/plugin-auth/src/auth/generators/fastify/auth-module/templates/module/constants/user-session.constants.ts b/plugins/plugin-auth/src/auth/core/generators/auth-module/templates/module/constants/user-session.constants.ts similarity index 100% rename from plugins/plugin-auth/src/auth/generators/fastify/auth-module/templates/module/constants/user-session.constants.ts rename to plugins/plugin-auth/src/auth/core/generators/auth-module/templates/module/constants/user-session.constants.ts diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-module/templates/module/schema/user-session-payload.object-type.ts b/plugins/plugin-auth/src/auth/core/generators/auth-module/templates/module/schema/user-session-payload.object-type.ts new file mode 100644 index 000000000..b86c885e3 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-module/templates/module/schema/user-session-payload.object-type.ts @@ -0,0 +1,23 @@ +// @ts-nocheck + +import { builder } from '%pothosImports'; + +export const userSessionPayload = builder.simpleObject( + 'UserSessionPayload', + { + fields: (t) => ({ + expiresAt: t.field({ type: 'DateTime', nullable: true }), + userId: t.field({ type: 'Uuid' }), + }), + }, + (t) => ({ + user: t.prismaField({ + type: TPL_USER_OBJECT_TYPE, + resolve: async (query, root) => + TPL_PRISMA_USER.findUniqueOrThrow({ + where: { id: root.userId }, + ...query, + }), + }), + }), +); diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-module/templates/module/schema/user-session.mutations.ts b/plugins/plugin-auth/src/auth/core/generators/auth-module/templates/module/schema/user-session.mutations.ts new file mode 100644 index 000000000..249e8f53d --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-module/templates/module/schema/user-session.mutations.ts @@ -0,0 +1,23 @@ +// @ts-nocheck + +import { builder } from '%pothosImports'; + +import { userSessionService } from '../services/user-session.service.js'; + +builder.mutationField('logOut', (t) => + t.fieldWithInputPayload({ + authorize: ['public'], + payload: { + success: t.payload.boolean({ + description: 'Whether the logout was successful.', + }), + }, + resolve: async (parent, args, context) => { + if (context.auth.session && context.auth.session.type === 'user') { + await userSessionService.clearSession(context.auth.session, context); + } + + return { success: true }; + }, + }), +); diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-module/templates/module/schema/user-session.queries.ts b/plugins/plugin-auth/src/auth/core/generators/auth-module/templates/module/schema/user-session.queries.ts new file mode 100644 index 000000000..cc31c39c4 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-module/templates/module/schema/user-session.queries.ts @@ -0,0 +1,24 @@ +// @ts-nocheck + +import { builder } from '%pothosImports'; + +import { userSessionPayload } from './user-session-payload.object-type.js'; + +builder.queryField('currentUserSession', (t) => + t.field({ + type: userSessionPayload, + description: 'Get the current user session', + nullable: true, + authorize: ['public'], + resolve: (root, args, { auth }) => { + if (!auth.session || auth.session.type !== 'user') { + return undefined; + } + + return { + expiresAt: auth.session.expiresAt?.toISOString(), + userId: auth.session.userId, + }; + }, + }), +); diff --git a/plugins/plugin-auth/src/auth/generators/fastify/auth-module/templates/module/services/user-session.service.ts b/plugins/plugin-auth/src/auth/core/generators/auth-module/templates/module/services/user-session.service.ts similarity index 100% rename from plugins/plugin-auth/src/auth/generators/fastify/auth-module/templates/module/services/user-session.service.ts rename to plugins/plugin-auth/src/auth/core/generators/auth-module/templates/module/services/user-session.service.ts diff --git a/plugins/plugin-auth/src/auth/generators/fastify/auth-module/templates/module/utils/cookie-signer.ts b/plugins/plugin-auth/src/auth/core/generators/auth-module/templates/module/utils/cookie-signer.ts similarity index 100% rename from plugins/plugin-auth/src/auth/generators/fastify/auth-module/templates/module/utils/cookie-signer.ts rename to plugins/plugin-auth/src/auth/core/generators/auth-module/templates/module/utils/cookie-signer.ts diff --git a/plugins/plugin-auth/src/auth/generators/fastify/auth-module/templates/module/utils/session-cookie.ts b/plugins/plugin-auth/src/auth/core/generators/auth-module/templates/module/utils/session-cookie.ts similarity index 100% rename from plugins/plugin-auth/src/auth/generators/fastify/auth-module/templates/module/utils/session-cookie.ts rename to plugins/plugin-auth/src/auth/core/generators/auth-module/templates/module/utils/session-cookie.ts diff --git a/plugins/plugin-auth/src/auth/generators/fastify/auth-module/templates/module/utils/verify-request-origin.ts b/plugins/plugin-auth/src/auth/core/generators/auth-module/templates/module/utils/verify-request-origin.ts similarity index 100% rename from plugins/plugin-auth/src/auth/generators/fastify/auth-module/templates/module/utils/verify-request-origin.ts rename to plugins/plugin-auth/src/auth/core/generators/auth-module/templates/module/utils/verify-request-origin.ts diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-routes/auth-routes.generator.ts b/plugins/plugin-auth/src/auth/core/generators/auth-routes/auth-routes.generator.ts new file mode 100644 index 000000000..c4496c286 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-routes/auth-routes.generator.ts @@ -0,0 +1,44 @@ +import { renderTextTemplateFileAction } from '@baseplate-dev/core-generators'; +import { createGenerator, createGeneratorTask } from '@baseplate-dev/sync'; +import { z } from 'zod'; + +import { AUTH_CORE_AUTH_ROUTES_GENERATED as GENERATED_TEMPLATES } from './generated'; + +const descriptorSchema = z.object({}); + +/** + * Generator for auth routes for logging in and registering + */ +export const authRoutesGenerator = createGenerator({ + name: 'auth/core/auth-routes', + generatorFileUrl: import.meta.url, + descriptorSchema, + buildTasks: () => ({ + paths: GENERATED_TEMPLATES.paths.task, + renderers: GENERATED_TEMPLATES.renderers.task, + main: createGeneratorTask({ + dependencies: { + renderers: GENERATED_TEMPLATES.renderers.provider, + paths: GENERATED_TEMPLATES.paths.provider, + }, + run({ renderers, paths }) { + return { + build: async (builder) => { + await builder.apply( + renderers.mainGroup.render({ + variables: {}, + }), + ); + await builder.apply( + renderTextTemplateFileAction({ + destination: paths.queriesGql, + template: GENERATED_TEMPLATES.templates.queriesGql, + variables: {}, + }), + ); + }, + }; + }, + }), + }), +}); diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-routes/extractor.json b/plugins/plugin-auth/src/auth/core/generators/auth-routes/extractor.json new file mode 100644 index 000000000..50ae11f1a --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-routes/extractor.json @@ -0,0 +1,84 @@ +{ + "name": "auth/core/auth-routes", + "templates": { + "routes/auth_/login.tsx": { + "name": "login", + "type": "ts", + "fileOptions": { "kind": "singleton" }, + "generator": "@baseplate-dev/plugin-auth#auth/core/auth-routes", + "group": "main", + "importMapProviders": { + "apolloErrorImportsProvider": { + "importName": "apolloErrorImportsProvider", + "packagePathSpecifier": "@baseplate-dev/react-generators:src/generators/apollo/apollo-error/generated/ts-import-providers.ts" + }, + "generatedGraphqlImportsProvider": { + "importName": "generatedGraphqlImportsProvider", + "packagePathSpecifier": "@baseplate-dev/react-generators:src/generators/apollo/react-apollo/providers/generated-graphql.ts" + }, + "reactComponentsImportsProvider": { + "importName": "reactComponentsImportsProvider", + "packagePathSpecifier": "@baseplate-dev/react-generators:src/generators/core/react-components/generated/ts-import-providers.ts" + }, + "reactErrorImportsProvider": { + "importName": "reactErrorImportsProvider", + "packagePathSpecifier": "@baseplate-dev/react-generators:src/generators/core/react-error/generated/ts-import-providers.ts" + }, + "reactSessionImportsProvider": { + "importName": "reactSessionImportsProvider", + "packagePathSpecifier": "@baseplate-dev/plugin-auth:src/auth/core/generators/react-session/generated/ts-import-providers.ts" + } + }, + "pathRootRelativePath": "{routes-root}/auth_/login.tsx", + "variables": {} + }, + "routes/auth_/queries.gql": { + "name": "queries-gql", + "type": "text", + "fileOptions": { "kind": "singleton" }, + "pathRootRelativePath": "{routes-root}/auth_/queries.gql", + "variables": {} + }, + "routes/auth_/register.tsx": { + "name": "register", + "type": "ts", + "fileOptions": { "kind": "singleton" }, + "generator": "@baseplate-dev/plugin-auth#auth/core/auth-routes", + "group": "main", + "importMapProviders": { + "apolloErrorImportsProvider": { + "importName": "apolloErrorImportsProvider", + "packagePathSpecifier": "@baseplate-dev/react-generators:src/generators/apollo/apollo-error/generated/ts-import-providers.ts" + }, + "generatedGraphqlImportsProvider": { + "importName": "generatedGraphqlImportsProvider", + "packagePathSpecifier": "@baseplate-dev/react-generators:src/generators/apollo/react-apollo/providers/generated-graphql.ts" + }, + "reactComponentsImportsProvider": { + "importName": "reactComponentsImportsProvider", + "packagePathSpecifier": "@baseplate-dev/react-generators:src/generators/core/react-components/generated/ts-import-providers.ts" + }, + "reactErrorImportsProvider": { + "importName": "reactErrorImportsProvider", + "packagePathSpecifier": "@baseplate-dev/react-generators:src/generators/core/react-error/generated/ts-import-providers.ts" + }, + "reactSessionImportsProvider": { + "importName": "reactSessionImportsProvider", + "packagePathSpecifier": "@baseplate-dev/plugin-auth:src/auth/core/generators/react-session/generated/ts-import-providers.ts" + } + }, + "pathRootRelativePath": "{routes-root}/auth_/register.tsx", + "variables": {} + }, + "routes/auth_/route.tsx": { + "name": "route", + "type": "ts", + "fileOptions": { "kind": "singleton" }, + "generator": "@baseplate-dev/plugin-auth#auth/core/auth-routes", + "group": "main", + "importMapProviders": {}, + "pathRootRelativePath": "{routes-root}/auth_/route.tsx", + "variables": {} + } + } +} diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-routes/generated/index.ts b/plugins/plugin-auth/src/auth/core/generators/auth-routes/generated/index.ts new file mode 100644 index 000000000..5b2918bcd --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-routes/generated/index.ts @@ -0,0 +1,9 @@ +import { AUTH_CORE_AUTH_ROUTES_PATHS } from './template-paths.js'; +import { AUTH_CORE_AUTH_ROUTES_RENDERERS } from './template-renderers.js'; +import { AUTH_CORE_AUTH_ROUTES_TEMPLATES } from './typed-templates.js'; + +export const AUTH_CORE_AUTH_ROUTES_GENERATED = { + paths: AUTH_CORE_AUTH_ROUTES_PATHS, + renderers: AUTH_CORE_AUTH_ROUTES_RENDERERS, + templates: AUTH_CORE_AUTH_ROUTES_TEMPLATES, +}; diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-routes/generated/template-paths.ts b/plugins/plugin-auth/src/auth/core/generators/auth-routes/generated/template-paths.ts new file mode 100644 index 000000000..4ca54c8f0 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-routes/generated/template-paths.ts @@ -0,0 +1,37 @@ +import { reactRoutesProvider } from '@baseplate-dev/react-generators'; +import { createGeneratorTask, createProviderType } from '@baseplate-dev/sync'; + +export interface AuthCoreAuthRoutesPaths { + queriesGql: string; + login: string; + register: string; + route: string; +} + +const authCoreAuthRoutesPaths = createProviderType( + 'auth-core-auth-routes-paths', +); + +const authCoreAuthRoutesPathsTask = createGeneratorTask({ + dependencies: { reactRoutes: reactRoutesProvider }, + exports: { authCoreAuthRoutesPaths: authCoreAuthRoutesPaths.export() }, + run({ reactRoutes }) { + const routesRoot = reactRoutes.getOutputRelativePath(); + + return { + providers: { + authCoreAuthRoutesPaths: { + login: `${routesRoot}/auth_/login.tsx`, + queriesGql: `${routesRoot}/auth_/queries.gql`, + register: `${routesRoot}/auth_/register.tsx`, + route: `${routesRoot}/auth_/route.tsx`, + }, + }, + }; + }, +}); + +export const AUTH_CORE_AUTH_ROUTES_PATHS = { + provider: authCoreAuthRoutesPaths, + task: authCoreAuthRoutesPathsTask, +}; diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-routes/generated/template-renderers.ts b/plugins/plugin-auth/src/auth/core/generators/auth-routes/generated/template-renderers.ts new file mode 100644 index 000000000..0d0f7a3f0 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-routes/generated/template-renderers.ts @@ -0,0 +1,85 @@ +import type { RenderTsTemplateGroupActionInput } from '@baseplate-dev/core-generators'; +import type { BuilderAction } from '@baseplate-dev/sync'; + +import { typescriptFileProvider } from '@baseplate-dev/core-generators'; +import { + apolloErrorImportsProvider, + generatedGraphqlImportsProvider, + reactComponentsImportsProvider, + reactErrorImportsProvider, +} from '@baseplate-dev/react-generators'; +import { createGeneratorTask, createProviderType } from '@baseplate-dev/sync'; + +import { reactSessionImportsProvider } from '#src/auth/core/generators/react-session/generated/ts-import-providers.js'; + +import { AUTH_CORE_AUTH_ROUTES_PATHS } from './template-paths.js'; +import { AUTH_CORE_AUTH_ROUTES_TEMPLATES } from './typed-templates.js'; + +export interface AuthCoreAuthRoutesRenderers { + mainGroup: { + render: ( + options: Omit< + RenderTsTemplateGroupActionInput< + typeof AUTH_CORE_AUTH_ROUTES_TEMPLATES.mainGroup + >, + 'importMapProviders' | 'group' | 'paths' + >, + ) => BuilderAction; + }; +} + +const authCoreAuthRoutesRenderers = + createProviderType( + 'auth-core-auth-routes-renderers', + ); + +const authCoreAuthRoutesRenderersTask = createGeneratorTask({ + dependencies: { + apolloErrorImports: apolloErrorImportsProvider, + generatedGraphqlImports: generatedGraphqlImportsProvider, + paths: AUTH_CORE_AUTH_ROUTES_PATHS.provider, + reactComponentsImports: reactComponentsImportsProvider, + reactErrorImports: reactErrorImportsProvider, + reactSessionImports: reactSessionImportsProvider, + typescriptFile: typescriptFileProvider, + }, + exports: { + authCoreAuthRoutesRenderers: authCoreAuthRoutesRenderers.export(), + }, + run({ + apolloErrorImports, + generatedGraphqlImports, + paths, + reactComponentsImports, + reactErrorImports, + reactSessionImports, + typescriptFile, + }) { + return { + providers: { + authCoreAuthRoutesRenderers: { + mainGroup: { + render: (options) => + typescriptFile.renderTemplateGroup({ + group: AUTH_CORE_AUTH_ROUTES_TEMPLATES.mainGroup, + paths, + importMapProviders: { + apolloErrorImports, + generatedGraphqlImports, + reactComponentsImports, + reactErrorImports, + reactSessionImports, + }, + ...options, + }), + }, + }, + }, + }; + }, +}); + +export const AUTH_CORE_AUTH_ROUTES_RENDERERS = { + provider: authCoreAuthRoutesRenderers, + task: authCoreAuthRoutesRenderersTask, +}; diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-routes/generated/typed-templates.ts b/plugins/plugin-auth/src/auth/core/generators/auth-routes/generated/typed-templates.ts new file mode 100644 index 000000000..f7dd3e450 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-routes/generated/typed-templates.ts @@ -0,0 +1,77 @@ +import { + createTextTemplateFile, + createTsTemplateFile, +} from '@baseplate-dev/core-generators'; +import { + apolloErrorImportsProvider, + generatedGraphqlImportsProvider, + reactComponentsImportsProvider, + reactErrorImportsProvider, +} from '@baseplate-dev/react-generators'; +import path from 'node:path'; + +import { reactSessionImportsProvider } from '#src/auth/core/generators/react-session/generated/ts-import-providers.js'; + +const login = createTsTemplateFile({ + fileOptions: { kind: 'singleton' }, + group: 'main', + importMapProviders: { + apolloErrorImports: apolloErrorImportsProvider, + generatedGraphqlImports: generatedGraphqlImportsProvider, + reactComponentsImports: reactComponentsImportsProvider, + reactErrorImports: reactErrorImportsProvider, + reactSessionImports: reactSessionImportsProvider, + }, + name: 'login', + source: { + path: path.join(import.meta.dirname, '../templates/routes/auth_/login.tsx'), + }, + variables: {}, +}); + +const register = createTsTemplateFile({ + fileOptions: { kind: 'singleton' }, + group: 'main', + importMapProviders: { + apolloErrorImports: apolloErrorImportsProvider, + generatedGraphqlImports: generatedGraphqlImportsProvider, + reactComponentsImports: reactComponentsImportsProvider, + reactErrorImports: reactErrorImportsProvider, + reactSessionImports: reactSessionImportsProvider, + }, + name: 'register', + source: { + path: path.join( + import.meta.dirname, + '../templates/routes/auth_/register.tsx', + ), + }, + variables: {}, +}); + +const route = createTsTemplateFile({ + fileOptions: { kind: 'singleton' }, + group: 'main', + importMapProviders: {}, + name: 'route', + source: { + path: path.join(import.meta.dirname, '../templates/routes/auth_/route.tsx'), + }, + variables: {}, +}); + +export const mainGroup = { login, register, route }; + +const queriesGql = createTextTemplateFile({ + fileOptions: { kind: 'singleton' }, + name: 'queries-gql', + source: { + path: path.join( + import.meta.dirname, + '../templates/routes/auth_/queries.gql', + ), + }, + variables: {}, +}); + +export const AUTH_CORE_AUTH_ROUTES_TEMPLATES = { queriesGql, mainGroup }; diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-routes/index.ts b/plugins/plugin-auth/src/auth/core/generators/auth-routes/index.ts new file mode 100644 index 000000000..de1270555 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-routes/index.ts @@ -0,0 +1 @@ +export * from './auth-routes.generator.js'; diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-routes/templates/routes/auth_/login.tsx b/plugins/plugin-auth/src/auth/core/generators/auth-routes/templates/routes/auth_/login.tsx new file mode 100644 index 000000000..e950046f0 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-routes/templates/routes/auth_/login.tsx @@ -0,0 +1,160 @@ +// @ts-nocheck + +import { getApolloErrorCode } from '%apolloErrorImports'; +import { LoginWithEmailPasswordDocument } from '%generatedGraphqlImports'; +import { + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + InputFieldController, +} from '%reactComponentsImports'; +import { logAndFormatError, logError } from '%reactErrorImports'; +import { useUserSessionClient } from '%reactSessionImports'; +import { useMutation } from '@apollo/client'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { + createFileRoute, + Link, + redirect, + useNavigate, +} from '@tanstack/react-router'; +import { useForm } from 'react-hook-form'; +import { toast } from 'sonner'; +import { z } from 'zod'; + +export const Route = createFileRoute('/auth_/login')({ + validateSearch: z.object({ + return_to: z + .string() + .regex(/^\/[a-zA-Z0-9\-._~!$&'()*+,;=:@?/]*$/) + .optional(), + }), + component: LoginPage, + beforeLoad: ({ search: { return_to }, context: { userId } }) => { + if (userId) { + throw redirect({ to: return_to ?? '/' }); + } + }, +}); + +const PASSWORD_MIN_LENGTH = 8; + +const formSchema = z.object({ + email: z + .string() + .email() + .transform((value) => value.toLowerCase()), + password: z.string().min(PASSWORD_MIN_LENGTH), +}); + +type FormData = z.infer; + +function LoginPage(): React.JSX.Element { + const { + control, + handleSubmit, + resetField, + setError: setFormError, + } = useForm({ + resolver: zodResolver(formSchema), + }); + const [loginWithEmailPassword, { loading }] = useMutation( + LoginWithEmailPasswordDocument, + ); + const { client } = useUserSessionClient(); + const navigate = useNavigate(); + const { return_to } = Route.useSearch(); + + const onSubmit = (data: FormData): void => { + loginWithEmailPassword({ + variables: { + input: { + email: data.email, + password: data.password, + }, + }, + }) + .then(({ data }) => { + if (!data) { + throw new Error('No data returned from login mutation'); + } + const { userId } = data.loginWithEmailPassword.session; + client.signIn(userId); + + navigate({ to: return_to ?? '/', replace: true }).catch(logError); + }) + .catch((err: unknown) => { + const errorCode = getApolloErrorCode(err, [ + 'invalid-email', + 'invalid-password', + ] as const); + switch (errorCode) { + case 'invalid-email': { + setFormError( + 'email', + { message: 'Email not found' }, + { shouldFocus: true }, + ); + break; + } + case 'invalid-password': { + resetField('password'); + setFormError( + 'password', + { message: 'Password is incorrect' }, + { shouldFocus: true }, + ); + break; + } + default: { + toast.error( + logAndFormatError(err, 'Sorry, we could not log you in.'), + ); + } + } + }); + }; + + return ( + + + Login to your account + + Enter your email below to login to your account + + + +
+
+ + + +
+
+ Don't have an account?{' '} + + Sign up + +
+
+
+
+ ); +} diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-routes/templates/routes/auth_/queries.gql b/plugins/plugin-auth/src/auth/core/generators/auth-routes/templates/routes/auth_/queries.gql new file mode 100644 index 000000000..297957717 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-routes/templates/routes/auth_/queries.gql @@ -0,0 +1,15 @@ +mutation RegisterWithEmailPassword($input: RegisterWithEmailPasswordInput!) { + registerWithEmailPassword(input: $input) { + session { + userId + } + } +} + +mutation LoginWithEmailPassword($input: LoginWithEmailPasswordInput!) { + loginWithEmailPassword(input: $input) { + session { + userId + } + } +} diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-routes/templates/routes/auth_/register.tsx b/plugins/plugin-auth/src/auth/core/generators/auth-routes/templates/routes/auth_/register.tsx new file mode 100644 index 000000000..a10e79cae --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-routes/templates/routes/auth_/register.tsx @@ -0,0 +1,147 @@ +// @ts-nocheck + +import { getApolloErrorCode } from '%apolloErrorImports'; +import { RegisterWithEmailPasswordDocument } from '%generatedGraphqlImports'; +import { + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + InputFieldController, +} from '%reactComponentsImports'; +import { logAndFormatError, logError } from '%reactErrorImports'; +import { useUserSessionClient } from '%reactSessionImports'; +import { useMutation } from '@apollo/client'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { + createFileRoute, + Link, + redirect, + useNavigate, +} from '@tanstack/react-router'; +import { useForm } from 'react-hook-form'; +import { toast } from 'sonner'; +import { z } from 'zod'; + +export const Route = createFileRoute('/auth_/register')({ + validateSearch: z.object({ + return_to: z + .string() + .regex(/^\/[a-zA-Z0-9\-._~!$&'()*+,;=:@?/]*$/) + .optional(), + }), + component: RegisterPage, + beforeLoad: ({ search: { return_to }, context: { userId } }) => { + if (userId) { + throw redirect({ to: return_to ?? '/' }); + } + }, +}); + +const PASSWORD_MIN_LENGTH = 8; + +const formSchema = z.object({ + email: z + .string() + .email() + .transform((value) => value.toLowerCase()), + password: z.string().min(PASSWORD_MIN_LENGTH), +}); + +type FormData = z.infer; + +function RegisterPage(): React.JSX.Element { + const { + control, + handleSubmit, + setError: setFormError, + } = useForm({ + resolver: zodResolver(formSchema), + }); + const [registerWithEmailPassword, { loading }] = useMutation( + RegisterWithEmailPasswordDocument, + ); + const { client } = useUserSessionClient(); + const navigate = useNavigate(); + const { return_to } = Route.useSearch(); + + const onSubmit = (data: FormData): void => { + registerWithEmailPassword({ + variables: { + input: { + email: data.email, + password: data.password, + }, + }, + }) + .then(({ data }) => { + if (!data) { + throw new Error('No data returned from login mutation'); + } + const { userId } = data.registerWithEmailPassword.session; + client.signIn(userId); + + navigate({ to: return_to ?? '/', replace: true }).catch(logError); + }) + .catch((err: unknown) => { + const errorCode = getApolloErrorCode(err, ['email-taken'] as const); + switch (errorCode) { + case 'email-taken': { + setFormError( + 'email', + { message: 'Email already taken' }, + { shouldFocus: true }, + ); + break; + } + default: { + toast.error( + logAndFormatError(err, 'Sorry, we could not register you.'), + ); + } + } + }); + }; + + return ( + + + Register to your account + + Enter your email below to register for an account + + + +
+
+ + + +
+
+ Already have an account?{' '} + + Login + +
+
+
+
+ ); +} diff --git a/plugins/plugin-auth/src/auth/core/generators/auth-routes/templates/routes/auth_/route.tsx b/plugins/plugin-auth/src/auth/core/generators/auth-routes/templates/routes/auth_/route.tsx new file mode 100644 index 000000000..1458a5cdb --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/auth-routes/templates/routes/auth_/route.tsx @@ -0,0 +1,17 @@ +// @ts-nocheck + +import { createFileRoute, Outlet } from '@tanstack/react-router'; + +export const Route = createFileRoute('/auth_')({ + component: RouteComponent, +}); + +function RouteComponent(): React.ReactElement { + return ( +
+
+ +
+
+ ); +} diff --git a/plugins/plugin-auth/src/auth/core/generators/index.ts b/plugins/plugin-auth/src/auth/core/generators/index.ts new file mode 100644 index 000000000..70f12699f --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/index.ts @@ -0,0 +1,6 @@ +export * from './auth-email-password/index.js'; +export * from './auth-hooks/index.js'; +export * from './auth-module/index.js'; +export * from './auth-routes/index.js'; +export * from './react-auth/index.js'; +export * from './react-session/index.js'; diff --git a/plugins/plugin-auth/src/auth/core/generators/react-auth/extractor.json b/plugins/plugin-auth/src/auth/core/generators/react-auth/extractor.json new file mode 100644 index 000000000..98a676921 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/react-auth/extractor.json @@ -0,0 +1,3 @@ +{ + "name": "auth/core/react-auth" +} diff --git a/plugins/plugin-auth/src/auth/core/generators/react-auth/index.ts b/plugins/plugin-auth/src/auth/core/generators/react-auth/index.ts new file mode 100644 index 000000000..2300ffc50 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/react-auth/index.ts @@ -0,0 +1 @@ +export * from './react-auth.generator.js'; diff --git a/plugins/plugin-auth/src/auth/core/generators/react-auth/react-auth.generator.ts b/plugins/plugin-auth/src/auth/core/generators/react-auth/react-auth.generator.ts new file mode 100644 index 000000000..ad5443fc4 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/react-auth/react-auth.generator.ts @@ -0,0 +1,32 @@ +import { packageScope } from '@baseplate-dev/core-generators'; +import { reactAuthRoutesProvider } from '@baseplate-dev/react-generators'; +import { createGenerator, createGeneratorTask } from '@baseplate-dev/sync'; +import { z } from 'zod'; + +const descriptorSchema = z.object({}); + +/** + * Generator for basic React auth integrations + */ +export const reactAuthGenerator = createGenerator({ + name: 'react/react-auth', + generatorFileUrl: import.meta.url, + descriptorSchema, + buildTasks: () => ({ + reactAuth: createGeneratorTask({ + exports: { + reactAuthRoutes: reactAuthRoutesProvider.export(packageScope), + }, + run() { + return { + providers: { + reactAuthRoutes: { + getLoginUrlPath: () => '/auth/login', + getRegisterUrlPath: () => '/auth/register', + }, + }, + }; + }, + }), + }), +}); diff --git a/plugins/plugin-auth/src/auth/core/generators/react-session/extractor.json b/plugins/plugin-auth/src/auth/core/generators/react-session/extractor.json new file mode 100644 index 000000000..5d48bace9 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/react-session/extractor.json @@ -0,0 +1,74 @@ +{ + "name": "auth/core/react-session", + "templates": { + "src/app/user-session-check.gql": { + "name": "user-session-check-gql", + "type": "text", + "fileOptions": { "kind": "singleton" }, + "pathRootRelativePath": "{src-root}/app/user-session-check.gql", + "variables": {} + }, + "src/app/user-session-check.tsx": { + "name": "user-session-check", + "type": "ts", + "fileOptions": { "kind": "singleton" }, + "generator": "@baseplate-dev/plugin-auth#auth/core/react-session", + "group": "main", + "importMapProviders": { + "generatedGraphqlImportsProvider": { + "importName": "generatedGraphqlImportsProvider", + "packagePathSpecifier": "@baseplate-dev/react-generators:src/generators/apollo/react-apollo/providers/generated-graphql.ts" + } + }, + "pathRootRelativePath": "{src-root}/app/user-session-check.tsx", + "variables": {} + }, + "src/app/user-session-provider.tsx": { + "name": "user-session-provider", + "type": "ts", + "fileOptions": { "kind": "singleton" }, + "generator": "@baseplate-dev/plugin-auth#auth/core/react-session", + "group": "main", + "importMapProviders": {}, + "pathRootRelativePath": "{src-root}/app/user-session-provider.tsx", + "variables": {} + }, + "src/hooks/use-user-session-client.ts": { + "name": "use-user-session-client", + "type": "ts", + "fileOptions": { "kind": "singleton" }, + "generator": "@baseplate-dev/plugin-auth#auth/core/react-session", + "group": "main", + "importMapProviders": {}, + "pathRootRelativePath": "{src-root}/hooks/use-user-session-client.ts", + "projectExports": { + "UserSessionClientContext": {}, + "UserSessionClientContextValue": { "isTypeOnly": true }, + "useUserSessionClient": {} + }, + "variables": {} + }, + "src/services/user-session-client.ts": { + "name": "user-session-client", + "type": "ts", + "fileOptions": { "kind": "singleton" }, + "generator": "@baseplate-dev/plugin-auth#auth/core/react-session", + "group": "main", + "importMapProviders": { + "reactUtilsImportsProvider": { + "importName": "reactUtilsImportsProvider", + "packagePathSpecifier": "@baseplate-dev/react-generators:src/generators/core/react-utils/generated/ts-import-providers.ts" + } + }, + "pathRootRelativePath": "{src-root}/services/user-session-client.ts", + "projectExports": { + "createUserSessionClient": {}, + "SessionChangeCallback": { "isTypeOnly": true }, + "UserSessionClient": {}, + "UserSessionClientConfig": { "isTypeOnly": true }, + "UserSessionData": { "isTypeOnly": true } + }, + "variables": {} + } + } +} diff --git a/plugins/plugin-auth/src/auth/core/generators/react-session/generated/index.ts b/plugins/plugin-auth/src/auth/core/generators/react-session/generated/index.ts new file mode 100644 index 000000000..a559c1e7e --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/react-session/generated/index.ts @@ -0,0 +1,11 @@ +import { AUTH_CORE_REACT_SESSION_PATHS } from './template-paths.js'; +import { AUTH_CORE_REACT_SESSION_RENDERERS } from './template-renderers.js'; +import { AUTH_CORE_REACT_SESSION_IMPORTS } from './ts-import-providers.js'; +import { AUTH_CORE_REACT_SESSION_TEMPLATES } from './typed-templates.js'; + +export const AUTH_CORE_REACT_SESSION_GENERATED = { + imports: AUTH_CORE_REACT_SESSION_IMPORTS, + paths: AUTH_CORE_REACT_SESSION_PATHS, + renderers: AUTH_CORE_REACT_SESSION_RENDERERS, + templates: AUTH_CORE_REACT_SESSION_TEMPLATES, +}; diff --git a/plugins/plugin-auth/src/auth/core/generators/react-session/generated/template-paths.ts b/plugins/plugin-auth/src/auth/core/generators/react-session/generated/template-paths.ts new file mode 100644 index 000000000..7e720cb0c --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/react-session/generated/template-paths.ts @@ -0,0 +1,39 @@ +import { packageInfoProvider } from '@baseplate-dev/core-generators'; +import { createGeneratorTask, createProviderType } from '@baseplate-dev/sync'; + +export interface AuthCoreReactSessionPaths { + userSessionCheckGql: string; + userSessionCheck: string; + userSessionProvider: string; + useUserSessionClient: string; + userSessionClient: string; +} + +const authCoreReactSessionPaths = createProviderType( + 'auth-core-react-session-paths', +); + +const authCoreReactSessionPathsTask = createGeneratorTask({ + dependencies: { packageInfo: packageInfoProvider }, + exports: { authCoreReactSessionPaths: authCoreReactSessionPaths.export() }, + run({ packageInfo }) { + const srcRoot = packageInfo.getPackageSrcPath(); + + return { + providers: { + authCoreReactSessionPaths: { + userSessionCheck: `${srcRoot}/app/user-session-check.tsx`, + userSessionCheckGql: `${srcRoot}/app/user-session-check.gql`, + userSessionClient: `${srcRoot}/services/user-session-client.ts`, + userSessionProvider: `${srcRoot}/app/user-session-provider.tsx`, + useUserSessionClient: `${srcRoot}/hooks/use-user-session-client.ts`, + }, + }, + }; + }, +}); + +export const AUTH_CORE_REACT_SESSION_PATHS = { + provider: authCoreReactSessionPaths, + task: authCoreReactSessionPathsTask, +}; diff --git a/plugins/plugin-auth/src/auth/core/generators/react-session/generated/template-renderers.ts b/plugins/plugin-auth/src/auth/core/generators/react-session/generated/template-renderers.ts new file mode 100644 index 000000000..40a3f9f59 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/react-session/generated/template-renderers.ts @@ -0,0 +1,67 @@ +import type { RenderTsTemplateGroupActionInput } from '@baseplate-dev/core-generators'; +import type { BuilderAction } from '@baseplate-dev/sync'; + +import { typescriptFileProvider } from '@baseplate-dev/core-generators'; +import { + generatedGraphqlImportsProvider, + reactUtilsImportsProvider, +} from '@baseplate-dev/react-generators'; +import { createGeneratorTask, createProviderType } from '@baseplate-dev/sync'; + +import { AUTH_CORE_REACT_SESSION_PATHS } from './template-paths.js'; +import { AUTH_CORE_REACT_SESSION_TEMPLATES } from './typed-templates.js'; + +export interface AuthCoreReactSessionRenderers { + mainGroup: { + render: ( + options: Omit< + RenderTsTemplateGroupActionInput< + typeof AUTH_CORE_REACT_SESSION_TEMPLATES.mainGroup + >, + 'importMapProviders' | 'group' | 'paths' + >, + ) => BuilderAction; + }; +} + +const authCoreReactSessionRenderers = + createProviderType( + 'auth-core-react-session-renderers', + ); + +const authCoreReactSessionRenderersTask = createGeneratorTask({ + dependencies: { + generatedGraphqlImports: generatedGraphqlImportsProvider, + paths: AUTH_CORE_REACT_SESSION_PATHS.provider, + reactUtilsImports: reactUtilsImportsProvider, + typescriptFile: typescriptFileProvider, + }, + exports: { + authCoreReactSessionRenderers: authCoreReactSessionRenderers.export(), + }, + run({ generatedGraphqlImports, paths, reactUtilsImports, typescriptFile }) { + return { + providers: { + authCoreReactSessionRenderers: { + mainGroup: { + render: (options) => + typescriptFile.renderTemplateGroup({ + group: AUTH_CORE_REACT_SESSION_TEMPLATES.mainGroup, + paths, + importMapProviders: { + generatedGraphqlImports, + reactUtilsImports, + }, + ...options, + }), + }, + }, + }, + }; + }, +}); + +export const AUTH_CORE_REACT_SESSION_RENDERERS = { + provider: authCoreReactSessionRenderers, + task: authCoreReactSessionRenderersTask, +}; diff --git a/plugins/plugin-auth/src/auth/core/generators/react-session/generated/ts-import-providers.ts b/plugins/plugin-auth/src/auth/core/generators/react-session/generated/ts-import-providers.ts new file mode 100644 index 000000000..3b3aff5ac --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/react-session/generated/ts-import-providers.ts @@ -0,0 +1,62 @@ +import type { TsImportMapProviderFromSchema } from '@baseplate-dev/core-generators'; + +import { + createTsImportMap, + createTsImportMapSchema, + packageScope, +} from '@baseplate-dev/core-generators'; +import { + createGeneratorTask, + createReadOnlyProviderType, +} from '@baseplate-dev/sync'; + +import { AUTH_CORE_REACT_SESSION_PATHS } from './template-paths.js'; + +const reactSessionImportsSchema = createTsImportMapSchema({ + createUserSessionClient: {}, + SessionChangeCallback: { isTypeOnly: true }, + UserSessionClient: {}, + UserSessionClientConfig: { isTypeOnly: true }, + UserSessionClientContext: {}, + UserSessionClientContextValue: { isTypeOnly: true }, + UserSessionData: { isTypeOnly: true }, + useUserSessionClient: {}, +}); + +export type ReactSessionImportsProvider = TsImportMapProviderFromSchema< + typeof reactSessionImportsSchema +>; + +export const reactSessionImportsProvider = + createReadOnlyProviderType( + 'react-session-imports', + ); + +const authCoreReactSessionImportsTask = createGeneratorTask({ + dependencies: { + paths: AUTH_CORE_REACT_SESSION_PATHS.provider, + }, + exports: { + reactSessionImports: reactSessionImportsProvider.export(packageScope), + }, + run({ paths }) { + return { + providers: { + reactSessionImports: createTsImportMap(reactSessionImportsSchema, { + createUserSessionClient: paths.userSessionClient, + SessionChangeCallback: paths.userSessionClient, + UserSessionClient: paths.userSessionClient, + UserSessionClientConfig: paths.userSessionClient, + UserSessionClientContext: paths.useUserSessionClient, + UserSessionClientContextValue: paths.useUserSessionClient, + UserSessionData: paths.userSessionClient, + useUserSessionClient: paths.useUserSessionClient, + }), + }, + }; + }, +}); + +export const AUTH_CORE_REACT_SESSION_IMPORTS = { + task: authCoreReactSessionImportsTask, +}; diff --git a/plugins/plugin-auth/src/auth/core/generators/react-session/generated/typed-templates.ts b/plugins/plugin-auth/src/auth/core/generators/react-session/generated/typed-templates.ts new file mode 100644 index 000000000..9e75f7848 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/react-session/generated/typed-templates.ts @@ -0,0 +1,103 @@ +import { + createTextTemplateFile, + createTsTemplateFile, +} from '@baseplate-dev/core-generators'; +import { + generatedGraphqlImportsProvider, + reactUtilsImportsProvider, +} from '@baseplate-dev/react-generators'; +import path from 'node:path'; + +const userSessionCheck = createTsTemplateFile({ + fileOptions: { kind: 'singleton' }, + group: 'main', + importMapProviders: { + generatedGraphqlImports: generatedGraphqlImportsProvider, + }, + name: 'user-session-check', + source: { + path: path.join( + import.meta.dirname, + '../templates/src/app/user-session-check.tsx', + ), + }, + variables: {}, +}); + +const userSessionClient = createTsTemplateFile({ + fileOptions: { kind: 'singleton' }, + group: 'main', + importMapProviders: { reactUtilsImports: reactUtilsImportsProvider }, + name: 'user-session-client', + projectExports: { + createUserSessionClient: {}, + SessionChangeCallback: { isTypeOnly: true }, + UserSessionClient: {}, + UserSessionClientConfig: { isTypeOnly: true }, + UserSessionData: { isTypeOnly: true }, + }, + source: { + path: path.join( + import.meta.dirname, + '../templates/src/services/user-session-client.ts', + ), + }, + variables: {}, +}); + +const userSessionProvider = createTsTemplateFile({ + fileOptions: { kind: 'singleton' }, + group: 'main', + importMapProviders: {}, + name: 'user-session-provider', + source: { + path: path.join( + import.meta.dirname, + '../templates/src/app/user-session-provider.tsx', + ), + }, + variables: {}, +}); + +const useUserSessionClient = createTsTemplateFile({ + fileOptions: { kind: 'singleton' }, + group: 'main', + importMapProviders: {}, + name: 'use-user-session-client', + projectExports: { + UserSessionClientContext: {}, + UserSessionClientContextValue: { isTypeOnly: true }, + useUserSessionClient: {}, + }, + source: { + path: path.join( + import.meta.dirname, + '../templates/src/hooks/use-user-session-client.ts', + ), + }, + variables: {}, +}); + +export const mainGroup = { + userSessionCheck, + userSessionClient, + userSessionProvider, + useUserSessionClient, +}; + +const userSessionCheckGql = createTextTemplateFile({ + fileOptions: { kind: 'singleton' }, + name: 'user-session-check-gql', + source: { + path: path.join( + import.meta.dirname, + '../templates/src/app/user-session-check.gql', + ), + }, + variables: {}, +}); + +export const AUTH_CORE_REACT_SESSION_TEMPLATES = { + userSessionCheckGql, + mainGroup, +}; diff --git a/plugins/plugin-auth/src/auth/core/generators/react-session/index.ts b/plugins/plugin-auth/src/auth/core/generators/react-session/index.ts new file mode 100644 index 000000000..0b89f3df3 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/react-session/index.ts @@ -0,0 +1,3 @@ +export type { ReactSessionImportsProvider } from './generated/ts-import-providers.js'; +export { reactSessionImportsProvider } from './generated/ts-import-providers.js'; +export * from './react-session.generator.js'; diff --git a/plugins/plugin-auth/src/auth/core/generators/react-session/react-session.generator.ts b/plugins/plugin-auth/src/auth/core/generators/react-session/react-session.generator.ts new file mode 100644 index 000000000..196b6e203 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/react-session/react-session.generator.ts @@ -0,0 +1,45 @@ +import { renderTextTemplateFileAction } from '@baseplate-dev/core-generators'; +import { createGenerator, createGeneratorTask } from '@baseplate-dev/sync'; +import { z } from 'zod'; + +import { AUTH_CORE_REACT_SESSION_GENERATED as GENERATED_TEMPLATES } from './generated'; + +const descriptorSchema = z.object({}); + +/** + * Generator for React session management + */ +export const reactSessionGenerator = createGenerator({ + name: 'auth/core/react-session', + generatorFileUrl: import.meta.url, + descriptorSchema, + buildTasks: () => ({ + paths: GENERATED_TEMPLATES.paths.task, + imports: GENERATED_TEMPLATES.imports.task, + renderers: GENERATED_TEMPLATES.renderers.task, + main: createGeneratorTask({ + dependencies: { + renderers: GENERATED_TEMPLATES.renderers.provider, + paths: GENERATED_TEMPLATES.paths.provider, + }, + run({ renderers, paths }) { + return { + build: async (builder) => { + await builder.apply( + renderers.mainGroup.render({ + variables: {}, + }), + ); + await builder.apply( + renderTextTemplateFileAction({ + destination: paths.userSessionCheckGql, + template: GENERATED_TEMPLATES.templates.userSessionCheckGql, + variables: {}, + }), + ); + }, + }; + }, + }), + }), +}); diff --git a/plugins/plugin-auth/src/auth/core/generators/react-session/templates/src/app/user-session-check.gql b/plugins/plugin-auth/src/auth/core/generators/react-session/templates/src/app/user-session-check.gql new file mode 100644 index 000000000..3d7fe0c11 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/react-session/templates/src/app/user-session-check.gql @@ -0,0 +1,6 @@ +query GetCurrentUserSession { + currentUserSession { + userId + expiresAt + } +} diff --git a/plugins/plugin-auth/src/auth/core/generators/react-session/templates/src/app/user-session-check.tsx b/plugins/plugin-auth/src/auth/core/generators/react-session/templates/src/app/user-session-check.tsx new file mode 100644 index 000000000..e498298d4 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/react-session/templates/src/app/user-session-check.tsx @@ -0,0 +1,29 @@ +// @ts-nocheck + +import { GetCurrentUserSessionDocument } from '%generatedGraphqlImports'; +import { useQuery } from '@apollo/client'; +import { useEffect } from 'react'; + +import { useUserSessionClient } from '../hooks/use-user-session-client.js'; + +/** + * Checks if the user session matches the loaded ID on first page load. + * + * This ensures that we have the correct user ID loaded on first page load. + */ +export function UserSessionCheck(): React.ReactElement | null { + const { data } = useQuery(GetCurrentUserSessionDocument, { + fetchPolicy: 'no-cache', + }); + const { client } = useUserSessionClient(); + + useEffect(() => { + if (!data?.currentUserSession?.userId) return; + + if (data.currentUserSession.userId !== client.getSession()?.userId) { + client.signOut(); + } + }, [data, client]); + + return null; +} diff --git a/plugins/plugin-auth/src/auth/core/generators/react-session/templates/src/app/user-session-provider.tsx b/plugins/plugin-auth/src/auth/core/generators/react-session/templates/src/app/user-session-provider.tsx new file mode 100644 index 000000000..0f6b5b52a --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/react-session/templates/src/app/user-session-provider.tsx @@ -0,0 +1,47 @@ +// @ts-nocheck + +import type React from 'react'; + +import { useEffect, useMemo, useState } from 'react'; + +import type { UserSessionClientContextValue } from '../hooks/use-user-session-client.js'; +import type { UserSessionData } from '../services/user-session-client.js'; + +import { UserSessionClientContext } from '../hooks/use-user-session-client.js'; +import { createUserSessionClient } from '../services/user-session-client.js'; + +interface UserSessionProviderProps { + children: React.ReactNode; +} + +export function UserSessionProvider({ + children, +}: UserSessionProviderProps): React.JSX.Element { + const [userSessionClient] = useState(() => createUserSessionClient()); + const [session, setSession] = useState( + userSessionClient.getSession(), + ); + + // Subscribe to session changes + useEffect( + () => + userSessionClient.onSessionChange((newSession) => { + setSession(newSession); + }), + [userSessionClient], + ); + + const contextValue: UserSessionClientContextValue = useMemo( + () => ({ + client: userSessionClient, + session, + }), + [userSessionClient, session], + ); + + return ( + + {children} + + ); +} diff --git a/plugins/plugin-auth/src/auth/core/generators/react-session/templates/src/hooks/use-user-session-client.ts b/plugins/plugin-auth/src/auth/core/generators/react-session/templates/src/hooks/use-user-session-client.ts new file mode 100644 index 000000000..317213f39 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/react-session/templates/src/hooks/use-user-session-client.ts @@ -0,0 +1,33 @@ +// @ts-nocheck + +import { createContext, useContext } from 'react'; + +import type { + UserSessionClient, + UserSessionData, +} from '../services/user-session-client.js'; + +export interface UserSessionClientContextValue { + client: UserSessionClient; + session: UserSessionData | undefined; +} + +export const UserSessionClientContext = createContext< + UserSessionClientContextValue | undefined +>(undefined); + +/** + * Hook to get the user session client + * @returns The user session client + */ +export function useUserSessionClient(): UserSessionClientContextValue { + const client = useContext(UserSessionClientContext); + + if (!client) { + throw new Error( + 'useUserSessionClient must be used within a UserSessionClientProvider', + ); + } + + return client; +} diff --git a/plugins/plugin-auth/src/auth/core/generators/react-session/templates/src/services/user-session-client.ts b/plugins/plugin-auth/src/auth/core/generators/react-session/templates/src/services/user-session-client.ts new file mode 100644 index 000000000..43b666975 --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/generators/react-session/templates/src/services/user-session-client.ts @@ -0,0 +1,153 @@ +// @ts-nocheck + +import { getSafeLocalStorage } from '%reactUtilsImports'; + +/** + * Session data returned by the user session client + */ +export interface UserSessionData { + userId: string; +} + +/** + * Callback function for session change events + */ +export type SessionChangeCallback = ( + session: UserSessionData | undefined, +) => void; + +/** + * Configuration options for creating the user session client + */ +export interface UserSessionClientConfig { + /** + * Optional initial session data + */ + initialSession?: UserSessionData; +} + +/** + * User session client for managing session persistence using localStorage + * Handles session state persistence and cross-tab synchronization + */ +export class UserSessionClient { + private static readonly USER_ID_STORAGE_KEY = 'APP_USER_ID'; + private readonly storage = getSafeLocalStorage(); + private readonly callbacks = new Set(); + private cleanupListener?: () => void; + + constructor(config?: UserSessionClientConfig) { + // Initialize storage listener for cross-tab synchronization + this.setupStorageListener(); + + // Set initial session if provided + if (config?.initialSession?.userId) { + this.setUserId(config.initialSession.userId); + } + } + + /** + * Get the current session data + * @returns Current session information + */ + getSession(): UserSessionData | undefined { + const userId = this.getUserId(); + return userId ? { userId } : undefined; + } + + /** + * Sign in a user with the given user ID + * @param userId - The user ID to sign in + */ + signIn(userId: string): void { + this.setUserId(userId); + this.notifyCallbacks(); + } + + /** + * Sign out the current user + */ + signOut(): void { + this.setUserId(null); + this.notifyCallbacks(); + } + + /** + * Subscribe to session changes + * @param callback - Function to call when session changes + * @returns Cleanup function to unsubscribe + */ + onSessionChange(callback: SessionChangeCallback): () => void { + this.callbacks.add(callback); + return () => { + this.callbacks.delete(callback); + }; + } + + /** + * Clean up resources when the client is no longer needed + */ + destroy(): void { + this.cleanupListener?.(); + this.callbacks.clear(); + } + + /** + * Get the current user ID from storage + * @returns User ID or null if not authenticated + */ + private getUserId(): string | null { + return this.storage.getItem(UserSessionClient.USER_ID_STORAGE_KEY); + } + + /** + * Set the user ID in storage + * @param userId - User ID to store, or null to clear + */ + private setUserId(userId: string | null): void { + const currentUserId = this.getUserId(); + const userIdChanged = userId !== currentUserId; + + if (!userIdChanged) { + return; + } + + if (userId) { + this.storage.setItem(UserSessionClient.USER_ID_STORAGE_KEY, userId); + } else { + this.storage.removeItem(UserSessionClient.USER_ID_STORAGE_KEY); + } + } + + /** + * Set up storage listener for cross-tab synchronization + */ + private setupStorageListener(): void { + this.cleanupListener = this.storage.addEventListener((key) => { + if (key === UserSessionClient.USER_ID_STORAGE_KEY) { + this.notifyCallbacks(); + } + }); + } + + /** + * Notify all registered callbacks of session changes + */ + private notifyCallbacks(): void { + const session = this.getSession(); + for (const callback of this.callbacks) { + callback(session); + } + } +} + +/** + * Factory function to create a user session client + * @param config - Optional configuration for the client + * @returns New UserSessionClient instance + */ +export function createUserSessionClient( + config?: UserSessionClientConfig, +): UserSessionClient { + return new UserSessionClient(config); +} diff --git a/plugins/plugin-auth/src/auth/core/index.ts b/plugins/plugin-auth/src/auth/core/index.ts new file mode 100644 index 000000000..8e0bbc5af --- /dev/null +++ b/plugins/plugin-auth/src/auth/core/index.ts @@ -0,0 +1 @@ +export * from './generators/index.js'; diff --git a/plugins/plugin-auth/src/auth/core/node.ts b/plugins/plugin-auth/src/auth/core/node.ts index 1d2ca9c89..fe8ee2ee5 100644 --- a/plugins/plugin-auth/src/auth/core/node.ts +++ b/plugins/plugin-auth/src/auth/core/node.ts @@ -1,9 +1,6 @@ import { - authContextGenerator, - authPluginGenerator, - authRolesGenerator, - pothosAuthGenerator, - userSessionTypesGenerator, + appModuleGenerator, + passwordHasherServiceGenerator, } from '@baseplate-dev/fastify-generators'; import { adminAppEntryType, @@ -13,14 +10,20 @@ import { PluginUtils, webAppEntryType, } from '@baseplate-dev/project-builder-lib'; + import { - authIdentifyGenerator, - placeholderAuthHooksGenerator, -} from '@baseplate-dev/react-generators'; + createCommonBackendAuthModuleGenerators, + createCommonBackendAuthRootGenerators, + createCommonWebAuthGenerators, +} from '#src/common/index.js'; import type { AuthPluginDefinition } from './schema/plugin-definition.js'; -import { authModuleGenerator } from '../generators/index.js'; +import { authEmailPasswordGenerator } from './generators/auth-email-password/auth-email-password.generator.js'; +import { authHooksGenerator } from './generators/auth-hooks/auth-hooks.generator.js'; +import { authRoutesGenerator } from './generators/auth-routes/auth-routes.generator.js'; +import { authModuleGenerator, reactAuthGenerator } from './generators/index.js'; +import { reactSessionGenerator } from './generators/react-session/react-session.generator.js'; export default createPlatformPluginExport({ dependencies: { @@ -39,48 +42,48 @@ export default createPlatformPluginExport({ ) as AuthPluginDefinition; appCompiler.addChildrenToFeature(auth.authFeatureRef, { - authContext: authContextGenerator({}), - authPlugin: authPluginGenerator({}), - authRoles: authRolesGenerator({ - roles: auth.roles.map((r) => ({ - name: r.name, - comment: r.comment, - builtIn: r.builtIn, - })), - }), - userSessionTypes: userSessionTypesGenerator({}), + ...createCommonBackendAuthModuleGenerators({ roles: auth.roles }), authModule: authModuleGenerator({ userSessionModelName: definitionContainer.nameFromId( auth.modelRefs.userSession, ), + userModelName: definitionContainer.nameFromId(auth.modelRefs.user), + }), + emailPassword: appModuleGenerator({ + id: 'email-password', + name: 'password', + children: { + module: authEmailPasswordGenerator({}), + hasher: passwordHasherServiceGenerator({}), + }, }), }); - appCompiler.addRootChildren({ - pothosAuth: pothosAuthGenerator({}), - }); + appCompiler.addRootChildren(createCommonBackendAuthRootGenerators()); }, }); + const sharedWebGenerators = { + ...createCommonWebAuthGenerators(), + reactAuth: reactAuthGenerator({}), + authHooks: authHooksGenerator({}), + reactSession: reactSessionGenerator({}), + authRoutes: authRoutesGenerator({}), + }; + // register web compiler appCompiler.registerAppCompiler({ pluginId, appType: webAppEntryType, compile: ({ appCompiler }) => { - appCompiler.addRootChildren({ - authIdentify: authIdentifyGenerator({}), - authHooks: placeholderAuthHooksGenerator({}), - }); + appCompiler.addRootChildren(sharedWebGenerators); }, }); appCompiler.registerAppCompiler({ pluginId, appType: adminAppEntryType, compile: ({ appCompiler }) => { - appCompiler.addRootChildren({ - authIdentify: authIdentifyGenerator({}), - authHooks: placeholderAuthHooksGenerator({}), - }); + appCompiler.addRootChildren(sharedWebGenerators); }, }); diff --git a/plugins/plugin-auth/src/auth/core/schema/models.ts b/plugins/plugin-auth/src/auth/core/schema/models.ts index b3557c676..9c2ae4aa8 100644 --- a/plugins/plugin-auth/src/auth/core/schema/models.ts +++ b/plugins/plugin-auth/src/auth/core/schema/models.ts @@ -23,14 +23,18 @@ export function createAuthModels({ options: { genUuid: true }, }, { - name: 'email', + name: 'name', type: 'string', isOptional: true, }, { - name: 'phone', + name: 'email', type: 'string', - isOptional: true, + }, + { + name: 'emailVerified', + type: 'boolean', + options: { default: 'false' }, }, { name: 'updatedAt', @@ -72,7 +76,7 @@ export function createAuthModels({ type: 'uuid', }, { - name: 'providerType', + name: 'accountId', type: 'string', }, { @@ -80,8 +84,9 @@ export function createAuthModels({ type: 'string', }, { - name: 'providerSecret', + name: 'password', type: 'string', + isOptional: true, }, { name: 'createdAt', @@ -97,7 +102,7 @@ export function createAuthModels({ primaryKeyFieldRefs: ['id'], uniqueConstraints: [ { - fields: [{ fieldRef: 'providerType' }, { fieldRef: 'providerId' }], + fields: [{ fieldRef: 'accountId' }, { fieldRef: 'providerId' }], }, ], relations: [ @@ -159,14 +164,14 @@ export function createAuthModels({ type: 'uuid', options: { genUuid: true }, }, - { - name: 'token', - type: 'string', - }, { name: 'userId', type: 'uuid', }, + { + name: 'token', + type: 'string', + }, { name: 'expiresAt', type: 'dateTime', diff --git a/plugins/plugin-auth/src/auth/core/schema/plugin-definition.ts b/plugins/plugin-auth/src/auth/core/schema/plugin-definition.ts index 7d4fd307f..edef5daa1 100644 --- a/plugins/plugin-auth/src/auth/core/schema/plugin-definition.ts +++ b/plugins/plugin-auth/src/auth/core/schema/plugin-definition.ts @@ -7,7 +7,7 @@ import { } from '@baseplate-dev/project-builder-lib'; import { z } from 'zod'; -import { createAuthRolesSchema } from '#src/roles/index.js'; +import { createAuthRolesSchema } from '#src/common/roles/index.js'; export const createAuthPluginDefinitionSchema = definitionSchema((ctx) => z.object({ diff --git a/plugins/plugin-auth/src/auth/generators/fastify/auth-module/auth-module.generator.ts b/plugins/plugin-auth/src/auth/generators/fastify/auth-module/auth-module.generator.ts deleted file mode 100644 index 00e1e7944..000000000 --- a/plugins/plugin-auth/src/auth/generators/fastify/auth-module/auth-module.generator.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { - tsCodeFragment, - typescriptFileProvider, -} from '@baseplate-dev/core-generators'; -import { - authContextImportsProvider, - authRolesImportsProvider, - configServiceImportsProvider, - configServiceProvider, - errorHandlerServiceImportsProvider, - prismaOutputProvider, - requestServiceContextImportsProvider, - userSessionTypesImportsProvider, -} from '@baseplate-dev/fastify-generators'; -import { - createGenerator, - createGeneratorTask, - createProviderTask, -} from '@baseplate-dev/sync'; -import { z } from 'zod'; - -import { FASTIFY_AUTH_MODULE_GENERATED } from './generated'; - -const descriptorSchema = z.object({ - userSessionModelName: z.string().min(1), -}); - -export const authModuleGenerator = createGenerator({ - name: 'fastify/auth-module', - generatorFileUrl: import.meta.url, - descriptorSchema, - buildTasks: ({ userSessionModelName }) => ({ - paths: FASTIFY_AUTH_MODULE_GENERATED.paths.task, - imports: FASTIFY_AUTH_MODULE_GENERATED.imports.task, - config: createProviderTask(configServiceProvider, (configService) => { - configService.configFields.set('AUTH_SECRET', { - validator: tsCodeFragment( - 'z.string().regex(/^[a-zA-Z0-9-_+=/]{20,}$/)', - ), - comment: - 'Secret key for signing auth cookie (at least 20 alphanumeric characters)', - seedValue: 'a-secret-key-1234567890', - exampleValue: '', - }); - }), - main: createGeneratorTask({ - dependencies: { - typescriptFile: typescriptFileProvider, - authRolesImports: authRolesImportsProvider, - configServiceImports: configServiceImportsProvider, - prismaOutput: prismaOutputProvider, - userSessionTypesImports: userSessionTypesImportsProvider, - authContextImports: authContextImportsProvider, - errorHandlerServiceImports: errorHandlerServiceImportsProvider, - requestServiceContextImports: requestServiceContextImportsProvider, - paths: FASTIFY_AUTH_MODULE_GENERATED.paths.provider, - }, - run({ - typescriptFile, - authRolesImports, - prismaOutput, - configServiceImports, - userSessionTypesImports, - authContextImports, - errorHandlerServiceImports, - requestServiceContextImports, - paths, - }) { - return { - providers: { - authModule: {}, - }, - build: async (builder) => { - await builder.apply( - typescriptFile.renderTemplateFile({ - template: - FASTIFY_AUTH_MODULE_GENERATED.templates.userSessionService, - destination: paths.userSessionService, - variables: { - TPL_PRISMA_USER_SESSION: - prismaOutput.getPrismaModelFragment(userSessionModelName), - }, - importMapProviders: { - configServiceImports, - authContextImports, - authRolesImports, - userSessionTypesImports, - errorHandlerServiceImports, - requestServiceContextImports, - }, - }), - ); - await builder.apply( - typescriptFile.renderTemplateGroup({ - group: FASTIFY_AUTH_MODULE_GENERATED.templates.constantsGroup, - paths, - }), - ); - await builder.apply( - typescriptFile.renderTemplateGroup({ - group: FASTIFY_AUTH_MODULE_GENERATED.templates.utilsGroup, - paths, - importMapProviders: { - configServiceImports, - }, - }), - ); - }, - }; - }, - }), - }), -}); diff --git a/plugins/plugin-auth/src/auth/generators/fastify/auth-module/generated/index.ts b/plugins/plugin-auth/src/auth/generators/fastify/auth-module/generated/index.ts deleted file mode 100644 index b4ab7bd5d..000000000 --- a/plugins/plugin-auth/src/auth/generators/fastify/auth-module/generated/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { FASTIFY_AUTH_MODULE_PATHS } from './template-paths.js'; -import { FASTIFY_AUTH_MODULE_IMPORTS } from './ts-import-providers.js'; -import { FASTIFY_AUTH_MODULE_TEMPLATES } from './typed-templates.js'; - -export const FASTIFY_AUTH_MODULE_GENERATED = { - imports: FASTIFY_AUTH_MODULE_IMPORTS, - paths: FASTIFY_AUTH_MODULE_PATHS, - templates: FASTIFY_AUTH_MODULE_TEMPLATES, -}; diff --git a/plugins/plugin-auth/src/auth/generators/fastify/auth-module/generated/ts-import-providers.ts b/plugins/plugin-auth/src/auth/generators/fastify/auth-module/generated/ts-import-providers.ts deleted file mode 100644 index e5900c818..000000000 --- a/plugins/plugin-auth/src/auth/generators/fastify/auth-module/generated/ts-import-providers.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { - createTsImportMap, - packageScope, -} from '@baseplate-dev/core-generators'; -import { - userSessionServiceImportsProvider, - userSessionServiceImportsSchema, -} from '@baseplate-dev/fastify-generators'; -import { createGeneratorTask } from '@baseplate-dev/sync'; - -import { FASTIFY_AUTH_MODULE_PATHS } from './template-paths.js'; - -const fastifyAuthModuleImportsTask = createGeneratorTask({ - dependencies: { - paths: FASTIFY_AUTH_MODULE_PATHS.provider, - }, - exports: { - userSessionServiceImports: - userSessionServiceImportsProvider.export(packageScope), - }, - run({ paths }) { - return { - providers: { - userSessionServiceImports: createTsImportMap( - userSessionServiceImportsSchema, - { userSessionService: paths.userSessionService }, - ), - }, - }; - }, -}); - -export const FASTIFY_AUTH_MODULE_IMPORTS = { - task: fastifyAuthModuleImportsTask, -}; diff --git a/plugins/plugin-auth/src/auth/generators/fastify/auth-module/ts-extractor.json b/plugins/plugin-auth/src/auth/generators/fastify/auth-module/ts-extractor.json deleted file mode 100644 index aecd896e9..000000000 --- a/plugins/plugin-auth/src/auth/generators/fastify/auth-module/ts-extractor.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "exportGroups": { - "user-session-service": { - "existingImportsProvider": { - "moduleSpecifier": "@baseplate-dev/fastify-generators", - "importSchemaName": "userSessionServiceImportsSchema", - "providerName": "userSessionServiceImportsProvider", - "providerTypeName": "UserSessionServiceImportsProvider" - } - } - } -} diff --git a/plugins/plugin-auth/src/auth/generators/fastify/index.ts b/plugins/plugin-auth/src/auth/generators/fastify/index.ts deleted file mode 100644 index a66693c0f..000000000 --- a/plugins/plugin-auth/src/auth/generators/fastify/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './auth-module/index.js'; diff --git a/plugins/plugin-auth/src/auth/generators/index.ts b/plugins/plugin-auth/src/auth/generators/index.ts deleted file mode 100644 index ca8d191d4..000000000 --- a/plugins/plugin-auth/src/auth/generators/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './fastify/index.js'; diff --git a/plugins/plugin-auth/src/auth/index.ts b/plugins/plugin-auth/src/auth/index.ts index 8e0bbc5af..17f45946d 100644 --- a/plugins/plugin-auth/src/auth/index.ts +++ b/plugins/plugin-auth/src/auth/index.ts @@ -1 +1 @@ -export * from './generators/index.js'; +export * from './core/index.js'; diff --git a/plugins/plugin-auth/src/auth0/core/components/auth-definition-editor.tsx b/plugins/plugin-auth/src/auth0/core/components/auth-definition-editor.tsx index 2d0223d43..93006e261 100644 --- a/plugins/plugin-auth/src/auth0/core/components/auth-definition-editor.tsx +++ b/plugins/plugin-auth/src/auth0/core/components/auth-definition-editor.tsx @@ -5,6 +5,7 @@ import { authRoleEntityType, createAndApplyModelMergerResults, createModelMergerResults, + doesModelMergerResultsHaveChanges, FeatureUtils, ModelUtils, PluginUtils, @@ -27,16 +28,17 @@ import { SectionListSectionHeader, SectionListSectionTitle, } from '@baseplate-dev/ui-components'; +import { useLens } from '@hookform/lenses'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMemo } from 'react'; -import { AUTH_DEFAULT_ROLES } from '#src/roles/index.js'; +import { RoleEditorForm } from '#src/common/roles/components/index.js'; +import { AUTH_DEFAULT_ROLES } from '#src/common/roles/index.js'; import type { Auth0PluginDefinitionInput } from '../schema/plugin-definition.js'; import { createAuth0Models } from '../schema/models.js'; import { createAuth0PluginDefinitionSchema } from '../schema/plugin-definition.js'; -import RoleEditorForm from './role-editor-form.js'; import '#src/styles.css'; @@ -126,10 +128,12 @@ export function AuthDefinitionEditor({ useBlockUnsavedChangesNavigate({ control, reset, onSubmit }); + const lens = useLens({ control }); + return (
@@ -148,7 +152,7 @@ export function AuthDefinitionEditor({ pendingModelChanges={pendingModelChanges} /> -
+
- +
- + ); } diff --git a/plugins/plugin-auth/src/auth0/core/node.ts b/plugins/plugin-auth/src/auth0/core/node.ts index 77cb2ca55..5cad7968f 100644 --- a/plugins/plugin-auth/src/auth0/core/node.ts +++ b/plugins/plugin-auth/src/auth0/core/node.ts @@ -1,10 +1,3 @@ -import { - authContextGenerator, - authPluginGenerator, - authRolesGenerator, - pothosAuthGenerator, - userSessionTypesGenerator, -} from '@baseplate-dev/fastify-generators'; import { adminAppEntryType, appCompilerSpec, @@ -13,16 +6,18 @@ import { PluginUtils, webAppEntryType, } from '@baseplate-dev/project-builder-lib'; +import { reactRoutesGenerator } from '@baseplate-dev/react-generators'; + import { - authIdentifyGenerator, - reactRoutesGenerator, -} from '@baseplate-dev/react-generators'; + createCommonBackendAuthModuleGenerators, + createCommonBackendAuthRootGenerators, + createCommonWebAuthGenerators, +} from '#src/common/index.js'; import type { Auth0PluginDefinition } from './schema/plugin-definition.js'; import { auth0ApolloGenerator, - auth0ComponentsGenerator, auth0HooksGenerator, auth0ModuleGenerator, auth0PagesGenerator, @@ -46,65 +41,43 @@ export default createPlatformPluginExport({ ) as Auth0PluginDefinition; appCompiler.addChildrenToFeature(auth.authFeatureRef, { - authContext: authContextGenerator({}), - authPlugin: authPluginGenerator({}), - authRoles: authRolesGenerator({ - roles: auth.roles.map((r) => ({ - name: r.name, - comment: r.comment, - builtIn: r.builtIn, - })), - }), + ...createCommonBackendAuthModuleGenerators({ roles: auth.roles }), auth0Module: auth0ModuleGenerator({ userModelName: definitionContainer.nameFromId(auth.modelRefs.user), includeManagement: true, }), - userSessionTypes: userSessionTypesGenerator({}), }); - appCompiler.addRootChildren({ - pothosAuth: pothosAuthGenerator({}), - }); + appCompiler.addRootChildren(createCommonBackendAuthRootGenerators()); }, }); + const sharedWebGenerators = { + ...createCommonWebAuthGenerators(), + auth: reactAuth0Generator({}), + authHooks: auth0HooksGenerator({}), + auth0Apollo: auth0ApolloGenerator({}), + auth0Callback: reactRoutesGenerator({ + name: 'auth', + children: { + auth: auth0PagesGenerator({}), + }, + }), + }; + // register web compiler appCompiler.registerAppCompiler({ pluginId, appType: webAppEntryType, compile: ({ appCompiler }) => { - appCompiler.addRootChildren({ - auth: reactAuth0Generator({}), - authHooks: auth0HooksGenerator({}), - authIdentify: authIdentifyGenerator({}), - auth0Apollo: auth0ApolloGenerator({}), - auth0Components: auth0ComponentsGenerator({}), - auth0Callback: reactRoutesGenerator({ - name: 'auth', - children: { - auth: auth0PagesGenerator({}), - }, - }), - }); + appCompiler.addRootChildren(sharedWebGenerators); }, }); appCompiler.registerAppCompiler({ pluginId, appType: adminAppEntryType, compile: ({ appCompiler }) => { - appCompiler.addRootChildren({ - auth: reactAuth0Generator({}), - authHooks: auth0HooksGenerator({}), - authIdentify: authIdentifyGenerator({}), - auth0Apollo: auth0ApolloGenerator({}), - auth0Components: auth0ComponentsGenerator({}), - auth0Callback: reactRoutesGenerator({ - name: 'auth', - children: { - auth: auth0PagesGenerator({}), - }, - }), - }); + appCompiler.addRootChildren(sharedWebGenerators); }, }); diff --git a/plugins/plugin-auth/src/auth0/core/schema/plugin-definition.ts b/plugins/plugin-auth/src/auth0/core/schema/plugin-definition.ts index 51a56a55a..de120b2e6 100644 --- a/plugins/plugin-auth/src/auth0/core/schema/plugin-definition.ts +++ b/plugins/plugin-auth/src/auth0/core/schema/plugin-definition.ts @@ -7,7 +7,7 @@ import { } from '@baseplate-dev/project-builder-lib'; import { z } from 'zod'; -import { createAuthRolesSchema } from '#src/roles/index.js'; +import { createAuthRolesSchema } from '#src/common/roles/index.js'; export const createAuth0PluginDefinitionSchema = definitionSchema((ctx) => z.object({ diff --git a/plugins/plugin-auth/src/auth0/generators/react/auth0-components/auth0-components.generator.ts b/plugins/plugin-auth/src/auth0/generators/react/auth0-components/auth0-components.generator.ts deleted file mode 100644 index e90d4157d..000000000 --- a/plugins/plugin-auth/src/auth0/generators/react/auth0-components/auth0-components.generator.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { typescriptFileProvider } from '@baseplate-dev/core-generators'; -import { - reactComponentsImportsProvider, - reactComponentsProvider, -} from '@baseplate-dev/react-generators'; -import { createGenerator, createGeneratorTask } from '@baseplate-dev/sync'; -import { z } from 'zod'; - -import { AUTH0_AUTH0_COMPONENTS_GENERATED } from './generated/index.js'; - -const descriptorSchema = z.object({}); - -export const auth0ComponentsGenerator = createGenerator({ - name: 'auth0/auth0-components', - generatorFileUrl: import.meta.url, - descriptorSchema, - buildTasks: () => ({ - paths: AUTH0_AUTH0_COMPONENTS_GENERATED.paths.task, - imports: AUTH0_AUTH0_COMPONENTS_GENERATED.imports.task, - main: createGeneratorTask({ - dependencies: { - reactComponents: reactComponentsProvider, - reactComponentsImports: reactComponentsImportsProvider, - typescriptFile: typescriptFileProvider, - paths: AUTH0_AUTH0_COMPONENTS_GENERATED.paths.provider, - }, - run({ reactComponents, typescriptFile, reactComponentsImports, paths }) { - reactComponents.registerComponent({ - name: 'require-auth', - }); - - return { - build: async (builder) => { - await builder.apply( - typescriptFile.renderTemplateFile({ - template: - AUTH0_AUTH0_COMPONENTS_GENERATED.templates.requireAuth, - destination: paths.requireAuth, - importMapProviders: { - reactComponentsImports, - }, - }), - ); - }, - }; - }, - }), - }), -}); diff --git a/plugins/plugin-auth/src/auth0/generators/react/auth0-components/extractor.json b/plugins/plugin-auth/src/auth0/generators/react/auth0-components/extractor.json deleted file mode 100644 index d147bb86a..000000000 --- a/plugins/plugin-auth/src/auth0/generators/react/auth0-components/extractor.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "auth0/auth0-components", - "extractors": { - "ts": { - "importProviders": [ - "@baseplate-dev/react-generators:authComponentsImportsProvider" - ], - "skipDefaultImportMap": true - } - }, - "templates": { - "src/components/require-auth/require-auth.tsx": { - "name": "require-auth", - "type": "ts", - "fileOptions": { "kind": "singleton" }, - "generator": "@baseplate-dev/plugin-auth#auth0/auth0-components", - "importMapProviders": { - "reactComponentsImportsProvider": { - "importName": "reactComponentsImportsProvider", - "packagePathSpecifier": "@baseplate-dev/react-generators:src/generators/core/react-components/generated/ts-import-providers.ts" - } - }, - "pathRootRelativePath": "{src-root}/components/require-auth/require-auth.tsx", - "projectExports": { "RequireAuth": {} }, - "variables": {} - } - } -} diff --git a/plugins/plugin-auth/src/auth0/generators/react/auth0-components/generated/index.ts b/plugins/plugin-auth/src/auth0/generators/react/auth0-components/generated/index.ts deleted file mode 100644 index 278756f2d..000000000 --- a/plugins/plugin-auth/src/auth0/generators/react/auth0-components/generated/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { AUTH0_AUTH0_COMPONENTS_PATHS } from './template-paths.js'; -import { AUTH0_AUTH0_COMPONENTS_RENDERERS } from './template-renderers.js'; -import { AUTH0_AUTH0_COMPONENTS_IMPORTS } from './ts-import-providers.js'; -import { AUTH0_AUTH0_COMPONENTS_TEMPLATES } from './typed-templates.js'; - -export const AUTH0_AUTH0_COMPONENTS_GENERATED = { - imports: AUTH0_AUTH0_COMPONENTS_IMPORTS, - paths: AUTH0_AUTH0_COMPONENTS_PATHS, - renderers: AUTH0_AUTH0_COMPONENTS_RENDERERS, - templates: AUTH0_AUTH0_COMPONENTS_TEMPLATES, -}; diff --git a/plugins/plugin-auth/src/auth0/generators/react/auth0-components/generated/template-paths.ts b/plugins/plugin-auth/src/auth0/generators/react/auth0-components/generated/template-paths.ts deleted file mode 100644 index 4ee879893..000000000 --- a/plugins/plugin-auth/src/auth0/generators/react/auth0-components/generated/template-paths.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { packageInfoProvider } from '@baseplate-dev/core-generators'; -import { createGeneratorTask, createProviderType } from '@baseplate-dev/sync'; - -export interface Auth0Auth0ComponentsPaths { - requireAuth: string; -} - -const auth0Auth0ComponentsPaths = createProviderType( - 'auth0-auth0-components-paths', -); - -const auth0Auth0ComponentsPathsTask = createGeneratorTask({ - dependencies: { packageInfo: packageInfoProvider }, - exports: { auth0Auth0ComponentsPaths: auth0Auth0ComponentsPaths.export() }, - run({ packageInfo }) { - const srcRoot = packageInfo.getPackageSrcPath(); - - return { - providers: { - auth0Auth0ComponentsPaths: { - requireAuth: `${srcRoot}/components/require-auth/require-auth.tsx`, - }, - }, - }; - }, -}); - -export const AUTH0_AUTH0_COMPONENTS_PATHS = { - provider: auth0Auth0ComponentsPaths, - task: auth0Auth0ComponentsPathsTask, -}; diff --git a/plugins/plugin-auth/src/auth0/generators/react/auth0-components/generated/template-renderers.ts b/plugins/plugin-auth/src/auth0/generators/react/auth0-components/generated/template-renderers.ts deleted file mode 100644 index 61e831b2e..000000000 --- a/plugins/plugin-auth/src/auth0/generators/react/auth0-components/generated/template-renderers.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { RenderTsTemplateFileActionInput } from '@baseplate-dev/core-generators'; -import type { BuilderAction } from '@baseplate-dev/sync'; - -import { typescriptFileProvider } from '@baseplate-dev/core-generators'; -import { reactComponentsImportsProvider } from '@baseplate-dev/react-generators'; -import { createGeneratorTask, createProviderType } from '@baseplate-dev/sync'; - -import { AUTH0_AUTH0_COMPONENTS_PATHS } from './template-paths.js'; -import { AUTH0_AUTH0_COMPONENTS_TEMPLATES } from './typed-templates.js'; - -export interface Auth0Auth0ComponentsRenderers { - requireAuth: { - render: ( - options: Omit< - RenderTsTemplateFileActionInput< - typeof AUTH0_AUTH0_COMPONENTS_TEMPLATES.requireAuth - >, - 'destination' | 'importMapProviders' | 'template' - >, - ) => BuilderAction; - }; -} - -const auth0Auth0ComponentsRenderers = - createProviderType( - 'auth0-auth0-components-renderers', - ); - -const auth0Auth0ComponentsRenderersTask = createGeneratorTask({ - dependencies: { - paths: AUTH0_AUTH0_COMPONENTS_PATHS.provider, - reactComponentsImports: reactComponentsImportsProvider, - typescriptFile: typescriptFileProvider, - }, - exports: { - auth0Auth0ComponentsRenderers: auth0Auth0ComponentsRenderers.export(), - }, - run({ paths, reactComponentsImports, typescriptFile }) { - return { - providers: { - auth0Auth0ComponentsRenderers: { - requireAuth: { - render: (options) => - typescriptFile.renderTemplateFile({ - template: AUTH0_AUTH0_COMPONENTS_TEMPLATES.requireAuth, - destination: paths.requireAuth, - importMapProviders: { - reactComponentsImports, - }, - ...options, - }), - }, - }, - }, - }; - }, -}); - -export const AUTH0_AUTH0_COMPONENTS_RENDERERS = { - provider: auth0Auth0ComponentsRenderers, - task: auth0Auth0ComponentsRenderersTask, -}; diff --git a/plugins/plugin-auth/src/auth0/generators/react/auth0-components/generated/ts-import-providers.ts b/plugins/plugin-auth/src/auth0/generators/react/auth0-components/generated/ts-import-providers.ts deleted file mode 100644 index 1c28b4acb..000000000 --- a/plugins/plugin-auth/src/auth0/generators/react/auth0-components/generated/ts-import-providers.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { - createTsImportMap, - packageScope, -} from '@baseplate-dev/core-generators'; -import { - authComponentsImportsProvider, - authComponentsImportsSchema, -} from '@baseplate-dev/react-generators'; -import { createGeneratorTask } from '@baseplate-dev/sync'; - -import { AUTH0_AUTH0_COMPONENTS_PATHS } from './template-paths.js'; - -const auth0Auth0ComponentsImportsTask = createGeneratorTask({ - dependencies: { - paths: AUTH0_AUTH0_COMPONENTS_PATHS.provider, - }, - exports: { - authComponentsImports: authComponentsImportsProvider.export(packageScope), - }, - run({ paths }) { - return { - providers: { - authComponentsImports: createTsImportMap(authComponentsImportsSchema, { - RequireAuth: paths.requireAuth, - }), - }, - }; - }, -}); - -export const AUTH0_AUTH0_COMPONENTS_IMPORTS = { - task: auth0Auth0ComponentsImportsTask, -}; diff --git a/plugins/plugin-auth/src/auth0/generators/react/auth0-components/generated/typed-templates.ts b/plugins/plugin-auth/src/auth0/generators/react/auth0-components/generated/typed-templates.ts deleted file mode 100644 index a1a5d3311..000000000 --- a/plugins/plugin-auth/src/auth0/generators/react/auth0-components/generated/typed-templates.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { createTsTemplateFile } from '@baseplate-dev/core-generators'; -import { reactComponentsImportsProvider } from '@baseplate-dev/react-generators'; -import path from 'node:path'; - -const requireAuth = createTsTemplateFile({ - fileOptions: { kind: 'singleton' }, - importMapProviders: { - reactComponentsImports: reactComponentsImportsProvider, - }, - name: 'require-auth', - projectExports: { RequireAuth: {} }, - source: { - path: path.join( - import.meta.dirname, - '../templates/src/components/require-auth/require-auth.tsx', - ), - }, - variables: {}, -}); - -export const AUTH0_AUTH0_COMPONENTS_TEMPLATES = { requireAuth }; diff --git a/plugins/plugin-auth/src/auth0/generators/react/auth0-components/index.ts b/plugins/plugin-auth/src/auth0/generators/react/auth0-components/index.ts deleted file mode 100644 index babb82ff7..000000000 --- a/plugins/plugin-auth/src/auth0/generators/react/auth0-components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './auth0-components.generator.js'; diff --git a/plugins/plugin-auth/src/auth0/generators/react/auth0-components/templates/src/components/require-auth/require-auth.tsx b/plugins/plugin-auth/src/auth0/generators/react/auth0-components/templates/src/components/require-auth/require-auth.tsx deleted file mode 100644 index bb12988e2..000000000 --- a/plugins/plugin-auth/src/auth0/generators/react/auth0-components/templates/src/components/require-auth/require-auth.tsx +++ /dev/null @@ -1,22 +0,0 @@ -// @ts-nocheck - -import type { ReactElement } from 'react'; - -import { Loader } from '%reactComponentsImports'; -import { withAuthenticationRequired } from '@auth0/auth0-react'; - -interface Props { - children: ReactElement; -} - -function RequireAuthRoot({ children }: Props): ReactElement { - return children; -} - -export const RequireAuth = withAuthenticationRequired(RequireAuthRoot, { - onRedirecting: () => ( -
- -
- ), -}); diff --git a/plugins/plugin-auth/src/auth0/generators/react/auth0-components/ts-extractor.json b/plugins/plugin-auth/src/auth0/generators/react/auth0-components/ts-extractor.json deleted file mode 100644 index 42c4db798..000000000 --- a/plugins/plugin-auth/src/auth0/generators/react/auth0-components/ts-extractor.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "exportGroups": { - "": { - "existingImportsProvider": { - "moduleSpecifier": "@/src/generators/auth/_providers/auth-components.js", - "importSchemaName": "authComponentsImportsSchema", - "providerName": "authComponentsImportsProvider", - "providerTypeName": "AuthComponentImportsProvider" - } - } - } -} diff --git a/plugins/plugin-auth/src/auth0/generators/react/auth0-hooks/auth0-hooks.generator.ts b/plugins/plugin-auth/src/auth0/generators/react/auth0-hooks/auth0-hooks.generator.ts index 08f00542f..aab9c3a4e 100644 --- a/plugins/plugin-auth/src/auth0/generators/react/auth0-hooks/auth0-hooks.generator.ts +++ b/plugins/plugin-auth/src/auth0/generators/react/auth0-hooks/auth0-hooks.generator.ts @@ -4,7 +4,6 @@ import { } from '@baseplate-dev/core-generators'; import { generatedGraphqlImportsProvider, - reactApolloProvider, reactErrorImportsProvider, } from '@baseplate-dev/react-generators'; import { createGenerator, createGeneratorTask } from '@baseplate-dev/sync'; @@ -26,7 +25,6 @@ export const auth0HooksGenerator = createGenerator({ main: createGeneratorTask({ dependencies: { typescriptFile: typescriptFileProvider, - reactApollo: reactApolloProvider, reactErrorImports: reactErrorImportsProvider, generatedGraphqlImports: generatedGraphqlImportsProvider, paths: AUTH0_AUTH0_HOOKS_GENERATED.paths.provider, @@ -35,7 +33,6 @@ export const auth0HooksGenerator = createGenerator({ typescriptFile, reactErrorImports, generatedGraphqlImports, - reactApollo, paths, }) { return { @@ -66,8 +63,6 @@ export const auth0HooksGenerator = createGenerator({ }, }), ); - - reactApollo.registerGqlFile(paths.useCurrentUserGql); }, }; }, diff --git a/plugins/plugin-auth/src/auth0/generators/react/index.ts b/plugins/plugin-auth/src/auth0/generators/react/index.ts index 6487943bd..7bbb720ad 100644 --- a/plugins/plugin-auth/src/auth0/generators/react/index.ts +++ b/plugins/plugin-auth/src/auth0/generators/react/index.ts @@ -1,5 +1,4 @@ export * from './auth0-apollo/index.js'; -export * from './auth0-components/index.js'; export * from './auth0-hooks/index.js'; export * from './auth0-pages/index.js'; export * from './react-auth0/index.js'; diff --git a/plugins/plugin-auth/src/auth0/generators/react/react-auth0/react-auth0.generator.ts b/plugins/plugin-auth/src/auth0/generators/react/react-auth0/react-auth0.generator.ts index 7e4f1ae85..16b99ca07 100644 --- a/plugins/plugin-auth/src/auth0/generators/react/react-auth0/react-auth0.generator.ts +++ b/plugins/plugin-auth/src/auth0/generators/react/react-auth0/react-auth0.generator.ts @@ -8,9 +8,8 @@ import { tsTemplateWithImports, } from '@baseplate-dev/core-generators'; import { - authContextTask, reactAppConfigProvider, - reactAuthProvider, + reactAuthRoutesProvider, reactConfigImportsProvider, reactConfigProvider, reactRouterConfigProvider, @@ -57,7 +56,6 @@ export const reactAuth0Generator = createGenerator({ }, }); }), - authContext: authContextTask, reactRouterContext: createGeneratorTask({ dependencies: { reactRouterConfig: reactRouterConfigProvider, @@ -97,7 +95,7 @@ export const reactAuth0Generator = createGenerator({ }), reactAuth: createGeneratorTask({ exports: { - reactAuth: reactAuthProvider.export(packageScope), + reactAuth: reactAuthRoutesProvider.export(packageScope), }, run() { return { diff --git a/plugins/plugin-auth/src/common/compiler/generator-creators.ts b/plugins/plugin-auth/src/common/compiler/generator-creators.ts new file mode 100644 index 000000000..97f480864 --- /dev/null +++ b/plugins/plugin-auth/src/common/compiler/generator-creators.ts @@ -0,0 +1,64 @@ +import type { GeneratorBundle } from '@baseplate-dev/sync'; + +import { + authContextGenerator, + authPluginGenerator, + authRolesGenerator, + pothosAuthGenerator, + userSessionTypesGenerator, +} from '@baseplate-dev/fastify-generators'; +import { authIdentifyGenerator } from '@baseplate-dev/react-generators'; + +import type { AuthRoleDefinition } from '../roles'; + +type BackendAuthGenerators = + | 'authContext' + | 'authPlugin' + | 'authRoles' + | 'userSessionTypes'; + +/** + * Creates the common backend auth generators + * + * @param params - The parameters for the generators + * @param params.roles - The roles of the auth plugin + * @returns The common backend auth generators + */ +export function createCommonBackendAuthModuleGenerators({ + roles, +}: { + roles: AuthRoleDefinition[]; +}): Record { + return { + authContext: authContextGenerator({}), + authPlugin: authPluginGenerator({}), + authRoles: authRolesGenerator({ + roles: roles.map((r) => ({ + name: r.name, + comment: r.comment, + builtIn: r.builtIn, + })), + }), + userSessionTypes: userSessionTypesGenerator({}), + }; +} + +export function createCommonBackendAuthRootGenerators(): Record< + 'pothosAuth', + GeneratorBundle +> { + return { + pothosAuth: pothosAuthGenerator({}), + }; +} + +type WebAuthGenerators = 'authIdentify'; + +export function createCommonWebAuthGenerators(): Record< + WebAuthGenerators, + GeneratorBundle +> { + return { + authIdentify: authIdentifyGenerator({}), + }; +} diff --git a/plugins/plugin-auth/src/common/compiler/index.ts b/plugins/plugin-auth/src/common/compiler/index.ts new file mode 100644 index 000000000..57467651b --- /dev/null +++ b/plugins/plugin-auth/src/common/compiler/index.ts @@ -0,0 +1 @@ +export * from './generator-creators.js'; diff --git a/plugins/plugin-auth/src/common/index.ts b/plugins/plugin-auth/src/common/index.ts new file mode 100644 index 000000000..f6fe67dc7 --- /dev/null +++ b/plugins/plugin-auth/src/common/index.ts @@ -0,0 +1,2 @@ +export * from './compiler/index.js'; +export * from './roles/index.js'; diff --git a/plugins/plugin-auth/src/common/roles/components/index.ts b/plugins/plugin-auth/src/common/roles/components/index.ts new file mode 100644 index 000000000..62594d33b --- /dev/null +++ b/plugins/plugin-auth/src/common/roles/components/index.ts @@ -0,0 +1 @@ +export * from './role-editor-form/role-editor-form.js'; diff --git a/plugins/plugin-auth/src/auth0/core/components/role-dialog.tsx b/plugins/plugin-auth/src/common/roles/components/role-editor-form/role-dialog.tsx similarity index 95% rename from plugins/plugin-auth/src/auth0/core/components/role-dialog.tsx rename to plugins/plugin-auth/src/common/roles/components/role-editor-form/role-dialog.tsx index fd5f65f7b..bd8335cbf 100644 --- a/plugins/plugin-auth/src/auth0/core/components/role-dialog.tsx +++ b/plugins/plugin-auth/src/common/roles/components/role-editor-form/role-dialog.tsx @@ -16,9 +16,9 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useId } from 'react'; import { useForm } from 'react-hook-form'; -import type { AuthRoleInput } from '#src/roles/schema.js'; +import type { AuthRoleInput } from '#src/common/roles/schema.js'; -import { createAuthRoleSchema } from '#src/roles/schema.js'; +import { createAuthRoleSchema } from '#src/common/roles/schema.js'; interface RoleDialogProps { open?: boolean; diff --git a/plugins/plugin-auth/src/auth0/core/components/role-editor-form.tsx b/plugins/plugin-auth/src/common/roles/components/role-editor-form/role-editor-form.tsx similarity index 88% rename from plugins/plugin-auth/src/auth0/core/components/role-editor-form.tsx rename to plugins/plugin-auth/src/common/roles/components/role-editor-form/role-editor-form.tsx index 0a82b2445..7eea6b222 100644 --- a/plugins/plugin-auth/src/auth0/core/components/role-editor-form.tsx +++ b/plugins/plugin-auth/src/common/roles/components/role-editor-form/role-editor-form.tsx @@ -1,5 +1,5 @@ +import type { Lens } from '@hookform/lenses'; import type React from 'react'; -import type { Control } from 'react-hook-form'; import { authRoleEntityType } from '@baseplate-dev/project-builder-lib'; import { @@ -16,31 +16,27 @@ import { SectionListSectionTitle, useConfirmDialog, } from '@baseplate-dev/ui-components'; +import { useFieldArray } from '@hookform/lenses/rhf'; import { useState } from 'react'; -import { useFieldArray, useWatch } from 'react-hook-form'; +import { useWatch } from 'react-hook-form'; import { MdAdd, MdDeleteOutline, MdEdit } from 'react-icons/md'; -import type { AuthRoleInput } from '#src/roles/index.js'; - -import type { Auth0PluginDefinitionInput } from '../schema/plugin-definition.js'; +import type { AuthRoleInput } from '#src/common/roles/index.js'; import { RoleDialog } from './role-dialog.js'; interface Props { className?: string; - control: Control; + lens: Lens; } -function RoleEditorForm({ className, control }: Props): React.JSX.Element { +export function RoleEditorForm({ className, lens }: Props): React.JSX.Element { const { requestConfirm } = useConfirmDialog(); - const { append, update, remove } = useFieldArray({ - control, - name: 'roles', - }); + const { append, update, remove } = useFieldArray(lens.interop()); const [roleToEdit, setRoleToEdit] = useState(); const [isEditing, setIsEditing] = useState(false); - const roles = useWatch({ control, name: 'roles' }); + const roles = useWatch(lens.interop()); function handleSaveRole(newRole: AuthRoleInput): void { const existingIndex = roles.findIndex((role) => role.id === newRole.id); @@ -147,5 +143,3 @@ function RoleEditorForm({ className, control }: Props): React.JSX.Element { ); } - -export default RoleEditorForm; diff --git a/plugins/plugin-auth/src/roles/constants.ts b/plugins/plugin-auth/src/common/roles/constants.ts similarity index 100% rename from plugins/plugin-auth/src/roles/constants.ts rename to plugins/plugin-auth/src/common/roles/constants.ts diff --git a/plugins/plugin-auth/src/roles/index.ts b/plugins/plugin-auth/src/common/roles/index.ts similarity index 61% rename from plugins/plugin-auth/src/roles/index.ts rename to plugins/plugin-auth/src/common/roles/index.ts index 5c0ba8383..1579ced02 100644 --- a/plugins/plugin-auth/src/roles/index.ts +++ b/plugins/plugin-auth/src/common/roles/index.ts @@ -1,2 +1,3 @@ +export * from './components/index.js'; export * from './constants.js'; export * from './schema.js'; diff --git a/plugins/plugin-auth/src/roles/schema.ts b/plugins/plugin-auth/src/common/roles/schema.ts similarity index 100% rename from plugins/plugin-auth/src/roles/schema.ts rename to plugins/plugin-auth/src/common/roles/schema.ts diff --git a/plugins/plugin-auth/src/index.ts b/plugins/plugin-auth/src/index.ts index fd4a05a8e..8dfc313b3 100644 --- a/plugins/plugin-auth/src/index.ts +++ b/plugins/plugin-auth/src/index.ts @@ -1 +1,2 @@ export * from './auth/index.js'; +export * from './placeholder-auth/index.js'; diff --git a/plugins/plugin-auth/src/placeholder-auth/core/common.ts b/plugins/plugin-auth/src/placeholder-auth/core/common.ts new file mode 100644 index 000000000..3de13573d --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/core/common.ts @@ -0,0 +1,46 @@ +import { + authConfigSpec, + createPlatformPluginExport, + pluginConfigSpec, + PluginUtils, +} from '@baseplate-dev/project-builder-lib'; + +import type { PlaceholderAuthPluginDefinition } from './schema/plugin-definition.js'; + +import { createPlaceholderAuthPluginDefinitionSchema } from './schema/plugin-definition.js'; + +// necessary for Typescript to infer the return type of the initialize function +export type { PluginPlatformModule } from '@baseplate-dev/project-builder-lib'; + +export default createPlatformPluginExport({ + dependencies: { + config: pluginConfigSpec, + }, + exports: { + authConfig: authConfigSpec, + }, + initialize: ({ config }, { pluginId }) => { + config.registerSchemaCreator( + pluginId, + createPlaceholderAuthPluginDefinitionSchema, + ); + return { + authConfig: { + getUserModel: (definition) => { + const pluginConfig = PluginUtils.configByIdOrThrow( + definition, + pluginId, + ) as PlaceholderAuthPluginDefinition; + return pluginConfig.modelRefs.user; + }, + getAuthRoles: (definition) => { + const pluginConfig = PluginUtils.configByIdOrThrow( + definition, + pluginId, + ) as PlaceholderAuthPluginDefinition; + return pluginConfig.roles; + }, + }, + }; + }, +}); diff --git a/plugins/plugin-auth/src/placeholder-auth/core/components/placeholder-auth-definition-editor.tsx b/plugins/plugin-auth/src/placeholder-auth/core/components/placeholder-auth-definition-editor.tsx new file mode 100644 index 000000000..5946fc477 --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/core/components/placeholder-auth-definition-editor.tsx @@ -0,0 +1,183 @@ +import type { WebConfigProps } from '@baseplate-dev/project-builder-lib'; +import type React from 'react'; + +import { + createAndApplyModelMergerResults, + createModelMergerResults, + doesModelMergerResultsHaveChanges, + FeatureUtils, + ModelUtils, + PluginUtils, +} from '@baseplate-dev/project-builder-lib'; +import { + FeatureComboboxFieldController, + ModelComboboxFieldController, + ModelMergerResultAlert, + useBlockUnsavedChangesNavigate, + useDefinitionSchema, + useProjectDefinition, + useResettableForm, +} from '@baseplate-dev/project-builder-lib/web'; +import { + FormActionBar, + SectionList, + SectionListSection, + SectionListSectionContent, + SectionListSectionDescription, + SectionListSectionHeader, + SectionListSectionTitle, +} from '@baseplate-dev/ui-components'; +import { useLens } from '@hookform/lenses'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useMemo } from 'react'; + +import { RoleEditorForm } from '#src/common/roles/components/index.js'; +import { createDefaultAuthRoles } from '#src/common/roles/index.js'; + +import type { PlaceholderAuthPluginDefinition } from '../schema/plugin-definition.js'; + +import { createAuthModels } from '../schema/models.js'; +import { createPlaceholderAuthPluginDefinitionSchema } from '../schema/plugin-definition.js'; + +import '#src/styles.css'; + +export function PlaceholderAuthDefinitionEditor({ + definition: pluginMetadata, + metadata, + onSave, +}: WebConfigProps): React.JSX.Element { + const { definition, definitionContainer, saveDefinitionWithFeedback } = + useProjectDefinition(); + + const authPluginDefinitionSchema = useDefinitionSchema( + createPlaceholderAuthPluginDefinitionSchema, + ); + + const defaultValues = useMemo(() => { + if (pluginMetadata?.config) { + return pluginMetadata.config as PlaceholderAuthPluginDefinition; + } + + return { + modelRefs: { + user: ModelUtils.getModelIdByNameOrDefault(definition, 'User'), + }, + authFeatureRef: FeatureUtils.getFeatureIdByNameOrDefault( + definition, + 'auth', + ), + roles: createDefaultAuthRoles(), + } satisfies PlaceholderAuthPluginDefinition; + }, [definition, pluginMetadata?.config]); + + const form = useResettableForm({ + resolver: zodResolver(authPluginDefinitionSchema), + defaultValues, + }); + const { control, reset, handleSubmit, watch } = form; + + const modelRefs = watch('modelRefs'); + const authFeatureRef = watch('authFeatureRef'); + + const pendingModelChanges = useMemo(() => { + const desiredModels = createAuthModels({ modelRefs, authFeatureRef }); + + return createModelMergerResults( + modelRefs, + desiredModels, + definitionContainer, + ); + }, [definitionContainer, authFeatureRef, modelRefs]); + + const onSubmit = handleSubmit((data) => + saveDefinitionWithFeedback( + (draftConfig) => { + const featureRef = FeatureUtils.ensureFeatureByNameRecursively( + draftConfig, + data.authFeatureRef, + ); + const updatedData = { + ...data, + authFeatureRef: featureRef, + }; + updatedData.modelRefs = createAndApplyModelMergerResults( + draftConfig, + updatedData.modelRefs, + createAuthModels(updatedData), + definitionContainer, + ); + PluginUtils.setPluginConfig( + draftConfig, + metadata, + updatedData, + definitionContainer.pluginStore, + ); + }, + { + successMessage: 'Successfully saved auth plugin!', + onSuccess: () => { + onSave(); + }, + }, + ), + ); + + useBlockUnsavedChangesNavigate({ control, reset, onSubmit }); + + const lens = useLens({ control }); + + return ( +
+
+ + + + + Placeholder Auth Configuration + + + Configure your placeholder auth settings, user model, and role + definitions. + + + + + +
+ + +
+
+
+ + +
+
+ + + + ); +} diff --git a/plugins/plugin-auth/src/placeholder-auth/core/generators/index.ts b/plugins/plugin-auth/src/placeholder-auth/core/generators/index.ts new file mode 100644 index 000000000..de1f17283 --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/core/generators/index.ts @@ -0,0 +1,3 @@ +export * from './placeholder-auth-hooks/index.js'; +export * from './placeholder-auth-module/index.js'; +export * from './placeholder-react-auth/index.js'; diff --git a/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/extractor.json b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/extractor.json new file mode 100644 index 000000000..6ef4705ef --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/extractor.json @@ -0,0 +1,72 @@ +{ + "name": "placeholder-auth/core/placeholder-auth-hooks", + "extractors": { + "ts": { + "importProviders": [ + "@baseplate-dev/react-generators:authHooksImportsProvider" + ], + "skipDefaultImportMap": true + } + }, + "templates": { + "src/hooks/use-current-user.gql": { + "name": "use-current-user-gql", + "type": "text", + "fileOptions": { "kind": "singleton" }, + "pathRootRelativePath": "{src-root}/hooks/use-current-user.gql", + "variables": {} + }, + "src/hooks/use-current-user.ts": { + "name": "use-current-user", + "type": "ts", + "fileOptions": { "kind": "singleton" }, + "generator": "@baseplate-dev/plugin-auth#placeholder-auth/core/placeholder-auth-hooks", + "group": "hooks", + "importMapProviders": { + "generatedGraphqlImportsProvider": { + "importName": "generatedGraphqlImportsProvider", + "packagePathSpecifier": "@baseplate-dev/react-generators:src/generators/apollo/react-apollo/providers/generated-graphql.ts" + } + }, + "pathRootRelativePath": "{src-root}/hooks/use-current-user.ts", + "projectExports": { "useCurrentUser": {} }, + "variables": {} + }, + "src/hooks/use-log-out.ts": { + "name": "use-log-out", + "type": "ts", + "fileOptions": { "kind": "singleton" }, + "generator": "@baseplate-dev/plugin-auth#placeholder-auth/core/placeholder-auth-hooks", + "group": "hooks", + "importMapProviders": {}, + "pathRootRelativePath": "{src-root}/hooks/use-log-out.ts", + "projectExports": { "useLogOut": {} }, + "variables": {} + }, + "src/hooks/use-session.ts": { + "name": "use-session", + "type": "ts", + "fileOptions": { "kind": "singleton" }, + "generator": "@baseplate-dev/plugin-auth#placeholder-auth/core/placeholder-auth-hooks", + "group": "hooks", + "importMapProviders": {}, + "pathRootRelativePath": "{src-root}/hooks/use-session.ts", + "projectExports": { + "SessionData": { "isTypeOnly": true }, + "useSession": {} + }, + "variables": {} + }, + "src/hooks/use-user-id-or-throw.ts": { + "name": "use-required-user-id", + "type": "ts", + "fileOptions": { "kind": "singleton" }, + "generator": "@baseplate-dev/plugin-auth#placeholder-auth/core/placeholder-auth-hooks", + "group": "hooks", + "importMapProviders": {}, + "pathRootRelativePath": "{src-root}/hooks/use-user-id-or-throw.ts", + "projectExports": { "useRequiredUserId": {} }, + "variables": {} + } + } +} diff --git a/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/generated/index.ts b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/generated/index.ts new file mode 100644 index 000000000..8ce270212 --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/generated/index.ts @@ -0,0 +1,11 @@ +import { PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_HOOKS_PATHS } from './template-paths.js'; +import { PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_HOOKS_RENDERERS } from './template-renderers.js'; +import { PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_HOOKS_IMPORTS } from './ts-import-providers.js'; +import { PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_HOOKS_TEMPLATES } from './typed-templates.js'; + +export const PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_HOOKS_GENERATED = { + imports: PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_HOOKS_IMPORTS, + paths: PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_HOOKS_PATHS, + renderers: PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_HOOKS_RENDERERS, + templates: PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_HOOKS_TEMPLATES, +}; diff --git a/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/generated/template-paths.ts b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/generated/template-paths.ts new file mode 100644 index 000000000..22a623e14 --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/generated/template-paths.ts @@ -0,0 +1,43 @@ +import { packageInfoProvider } from '@baseplate-dev/core-generators'; +import { createGeneratorTask, createProviderType } from '@baseplate-dev/sync'; + +export interface PlaceholderAuthCorePlaceholderAuthHooksPaths { + useCurrentUserGql: string; + useCurrentUser: string; + useLogOut: string; + useSession: string; + useRequiredUserId: string; +} + +const placeholderAuthCorePlaceholderAuthHooksPaths = + createProviderType( + 'placeholder-auth-core-placeholder-auth-hooks-paths', + ); + +const placeholderAuthCorePlaceholderAuthHooksPathsTask = createGeneratorTask({ + dependencies: { packageInfo: packageInfoProvider }, + exports: { + placeholderAuthCorePlaceholderAuthHooksPaths: + placeholderAuthCorePlaceholderAuthHooksPaths.export(), + }, + run({ packageInfo }) { + const srcRoot = packageInfo.getPackageSrcPath(); + + return { + providers: { + placeholderAuthCorePlaceholderAuthHooksPaths: { + useCurrentUser: `${srcRoot}/hooks/use-current-user.ts`, + useCurrentUserGql: `${srcRoot}/hooks/use-current-user.gql`, + useLogOut: `${srcRoot}/hooks/use-log-out.ts`, + useRequiredUserId: `${srcRoot}/hooks/use-user-id-or-throw.ts`, + useSession: `${srcRoot}/hooks/use-session.ts`, + }, + }, + }; + }, +}); + +export const PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_HOOKS_PATHS = { + provider: placeholderAuthCorePlaceholderAuthHooksPaths, + task: placeholderAuthCorePlaceholderAuthHooksPathsTask, +}; diff --git a/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/generated/template-renderers.ts b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/generated/template-renderers.ts new file mode 100644 index 000000000..b3874f478 --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/generated/template-renderers.ts @@ -0,0 +1,65 @@ +import type { RenderTsTemplateGroupActionInput } from '@baseplate-dev/core-generators'; +import type { BuilderAction } from '@baseplate-dev/sync'; + +import { typescriptFileProvider } from '@baseplate-dev/core-generators'; +import { generatedGraphqlImportsProvider } from '@baseplate-dev/react-generators'; +import { createGeneratorTask, createProviderType } from '@baseplate-dev/sync'; + +import { PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_HOOKS_PATHS } from './template-paths.js'; +import { PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_HOOKS_TEMPLATES } from './typed-templates.js'; + +export interface PlaceholderAuthCorePlaceholderAuthHooksRenderers { + hooksGroup: { + render: ( + options: Omit< + RenderTsTemplateGroupActionInput< + typeof PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_HOOKS_TEMPLATES.hooksGroup + >, + 'importMapProviders' | 'group' | 'paths' + >, + ) => BuilderAction; + }; +} + +const placeholderAuthCorePlaceholderAuthHooksRenderers = + createProviderType( + 'placeholder-auth-core-placeholder-auth-hooks-renderers', + ); + +const placeholderAuthCorePlaceholderAuthHooksRenderersTask = + createGeneratorTask({ + dependencies: { + generatedGraphqlImports: generatedGraphqlImportsProvider, + paths: PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_HOOKS_PATHS.provider, + typescriptFile: typescriptFileProvider, + }, + exports: { + placeholderAuthCorePlaceholderAuthHooksRenderers: + placeholderAuthCorePlaceholderAuthHooksRenderers.export(), + }, + run({ generatedGraphqlImports, paths, typescriptFile }) { + return { + providers: { + placeholderAuthCorePlaceholderAuthHooksRenderers: { + hooksGroup: { + render: (options) => + typescriptFile.renderTemplateGroup({ + group: + PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_HOOKS_TEMPLATES.hooksGroup, + paths, + importMapProviders: { + generatedGraphqlImports, + }, + ...options, + }), + }, + }, + }, + }; + }, + }); + +export const PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_HOOKS_RENDERERS = { + provider: placeholderAuthCorePlaceholderAuthHooksRenderers, + task: placeholderAuthCorePlaceholderAuthHooksRenderersTask, +}; diff --git a/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/generated/ts-import-providers.ts b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/generated/ts-import-providers.ts new file mode 100644 index 000000000..d58795727 --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/generated/ts-import-providers.ts @@ -0,0 +1,35 @@ +import { + createTsImportMap, + packageScope, +} from '@baseplate-dev/core-generators'; +import { + authHooksImportsProvider, + authHooksImportsSchema, +} from '@baseplate-dev/react-generators'; +import { createGeneratorTask } from '@baseplate-dev/sync'; + +import { PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_HOOKS_PATHS } from './template-paths.js'; + +const placeholderAuthCorePlaceholderAuthHooksImportsTask = createGeneratorTask({ + dependencies: { + paths: PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_HOOKS_PATHS.provider, + }, + exports: { authHooksImports: authHooksImportsProvider.export(packageScope) }, + run({ paths }) { + return { + providers: { + authHooksImports: createTsImportMap(authHooksImportsSchema, { + SessionData: paths.useSession, + useCurrentUser: paths.useCurrentUser, + useLogOut: paths.useLogOut, + useRequiredUserId: paths.useRequiredUserId, + useSession: paths.useSession, + }), + }, + }; + }, +}); + +export const PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_HOOKS_IMPORTS = { + task: placeholderAuthCorePlaceholderAuthHooksImportsTask, +}; diff --git a/packages/react-generators/src/generators/auth/placeholder-auth-hooks/generated/typed-templates.ts b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/generated/typed-templates.ts similarity index 57% rename from packages/react-generators/src/generators/auth/placeholder-auth-hooks/generated/typed-templates.ts rename to plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/generated/typed-templates.ts index a951f6c65..3b0032ee4 100644 --- a/packages/react-generators/src/generators/auth/placeholder-auth-hooks/generated/typed-templates.ts +++ b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/generated/typed-templates.ts @@ -1,16 +1,22 @@ -import { createTsTemplateFile } from '@baseplate-dev/core-generators'; +import { + createTextTemplateFile, + createTsTemplateFile, +} from '@baseplate-dev/core-generators'; +import { generatedGraphqlImportsProvider } from '@baseplate-dev/react-generators'; import path from 'node:path'; const useCurrentUser = createTsTemplateFile({ fileOptions: { kind: 'singleton' }, group: 'hooks', - importMapProviders: {}, + importMapProviders: { + generatedGraphqlImports: generatedGraphqlImportsProvider, + }, name: 'use-current-user', projectExports: { useCurrentUser: {} }, source: { path: path.join( import.meta.dirname, - '../templates/src/hooks/useCurrentUser.ts', + '../templates/src/hooks/use-current-user.ts', ), }, variables: {}, @@ -23,7 +29,10 @@ const useLogOut = createTsTemplateFile({ name: 'use-log-out', projectExports: { useLogOut: {} }, source: { - path: path.join(import.meta.dirname, '../templates/src/hooks/useLogOut.ts'), + path: path.join( + import.meta.dirname, + '../templates/src/hooks/use-log-out.ts', + ), }, variables: {}, }); @@ -37,7 +46,7 @@ const useRequiredUserId = createTsTemplateFile({ source: { path: path.join( import.meta.dirname, - '../templates/src/hooks/useRequiredUserId.ts', + '../templates/src/hooks/use-user-id-or-throw.ts', ), }, variables: {}, @@ -52,7 +61,7 @@ const useSession = createTsTemplateFile({ source: { path: path.join( import.meta.dirname, - '../templates/src/hooks/useSession.ts', + '../templates/src/hooks/use-session.ts', ), }, variables: {}, @@ -65,4 +74,19 @@ export const hooksGroup = { useSession, }; -export const AUTH_PLACEHOLDER_AUTH_HOOKS_TEMPLATES = { hooksGroup }; +const useCurrentUserGql = createTextTemplateFile({ + fileOptions: { kind: 'singleton' }, + name: 'use-current-user-gql', + source: { + path: path.join( + import.meta.dirname, + '../templates/src/hooks/use-current-user.gql', + ), + }, + variables: {}, +}); + +export const PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_HOOKS_TEMPLATES = { + useCurrentUserGql, + hooksGroup, +}; diff --git a/packages/react-generators/src/generators/auth/placeholder-auth-hooks/index.ts b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/index.ts similarity index 100% rename from packages/react-generators/src/generators/auth/placeholder-auth-hooks/index.ts rename to plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/index.ts diff --git a/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/placeholder-auth-hooks.generator.ts b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/placeholder-auth-hooks.generator.ts new file mode 100644 index 000000000..4bcb66b9f --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/placeholder-auth-hooks.generator.ts @@ -0,0 +1,42 @@ +import { renderTextTemplateFileAction } from '@baseplate-dev/core-generators'; +import { createGenerator, createGeneratorTask } from '@baseplate-dev/sync'; +import { z } from 'zod'; + +import { PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_HOOKS_GENERATED as GENERATED_TEMPLATES } from './generated/index.js'; + +const descriptorSchema = z.object({}); + +/** + * Placeholder generator for auth hooks. + * + * Useful for creating a test auth implementation. + */ +export const placeholderAuthHooksGenerator = createGenerator({ + name: 'placeholder-auth/core/placeholder-auth-hooks', + generatorFileUrl: import.meta.url, + descriptorSchema, + buildTasks: () => ({ + paths: GENERATED_TEMPLATES.paths.task, + imports: GENERATED_TEMPLATES.imports.task, + renderers: GENERATED_TEMPLATES.renderers.task, + main: createGeneratorTask({ + dependencies: { + paths: GENERATED_TEMPLATES.paths.provider, + renderers: GENERATED_TEMPLATES.renderers.provider, + }, + run({ paths, renderers }) { + return { + build: async (builder) => { + await builder.apply(renderers.hooksGroup.render({})); + await builder.apply( + renderTextTemplateFileAction({ + template: GENERATED_TEMPLATES.templates.useCurrentUserGql, + destination: paths.useCurrentUserGql, + }), + ); + }, + }; + }, + }), + }), +}); diff --git a/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/templates/src/hooks/use-current-user.gql b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/templates/src/hooks/use-current-user.gql new file mode 100644 index 000000000..903173c05 --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/templates/src/hooks/use-current-user.gql @@ -0,0 +1,10 @@ +fragment CurrentUser on User { + id + email +} + +query getUserById($id: Uuid!) { + user(id: $id) { + ...CurrentUser + } +} diff --git a/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/templates/src/hooks/use-current-user.ts b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/templates/src/hooks/use-current-user.ts new file mode 100644 index 000000000..d883aa129 --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/templates/src/hooks/use-current-user.ts @@ -0,0 +1,35 @@ +// @ts-nocheck + +import type { CurrentUserFragment } from '%generatedGraphqlImports'; + +import { GetUserByIdDocument } from '%generatedGraphqlImports'; +import { useQuery } from '@apollo/client'; + +import { useSession } from './use-session.js'; + +interface UseCurrentUserResult { + user?: CurrentUserFragment; + loading: boolean; + error?: Error | undefined; +} + +/** + * Fetches information about the current user + * + * @returns A result containing the current user or an error if the user is not authenticated + */ +export function useCurrentUser(): UseCurrentUserResult { + const { userId } = useSession(); + const { data, loading, error } = useQuery(GetUserByIdDocument, { + variables: { id: userId ?? '' }, + skip: !userId, + }); + + const noUserError = !userId ? new Error('No user logged in') : undefined; + + return { + user: data?.user, + loading, + error: error ?? noUserError, + }; +} diff --git a/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/templates/src/hooks/use-log-out.ts b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/templates/src/hooks/use-log-out.ts new file mode 100644 index 000000000..ad7c6e3b0 --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/templates/src/hooks/use-log-out.ts @@ -0,0 +1,8 @@ +// @ts-nocheck + +/** + * Provides a function to log out the user + */ +export function useLogOut(): () => void { + throw new Error('Not implemented'); +} diff --git a/packages/react-generators/src/generators/auth/placeholder-auth-hooks/templates/src/hooks/useSession.ts b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/templates/src/hooks/use-session.ts similarity index 65% rename from packages/react-generators/src/generators/auth/placeholder-auth-hooks/templates/src/hooks/useSession.ts rename to plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/templates/src/hooks/use-session.ts index f71f37871..ee81e469e 100644 --- a/packages/react-generators/src/generators/auth/placeholder-auth-hooks/templates/src/hooks/useSession.ts +++ b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/templates/src/hooks/use-session.ts @@ -5,6 +5,9 @@ export interface SessionData { isAuthenticated: boolean; } +/** + * Provides the current session data such as the user id and whether the user is authenticated + */ export function useSession(): SessionData { throw new Error('Not implemented'); } diff --git a/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/templates/src/hooks/use-user-id-or-throw.ts b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/templates/src/hooks/use-user-id-or-throw.ts new file mode 100644 index 000000000..a7b6932b8 --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-hooks/templates/src/hooks/use-user-id-or-throw.ts @@ -0,0 +1,14 @@ +// @ts-nocheck + +import { useSession } from './use-session.js'; + +/** + * Provides the current user id or throws an error if the user is not authenticated + */ +export function useUserIdOrThrow(): string { + const { userId } = useSession(); + if (!userId) { + throw new Error('User not authenticated'); + } + return userId; +} diff --git a/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-module/auth-module.generator.ts b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-module/auth-module.generator.ts new file mode 100644 index 000000000..092b4931e --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-module/auth-module.generator.ts @@ -0,0 +1,29 @@ +import { createGenerator, createGeneratorTask } from '@baseplate-dev/sync'; +import { z } from 'zod'; + +import { PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_MODULE_GENERATED as GENERATED_TEMPLATES } from './generated'; + +const descriptorSchema = z.object({}); + +export const placeholderAuthModuleGenerator = createGenerator({ + name: 'placeholder-auth/core/placeholder-auth-module', + generatorFileUrl: import.meta.url, + descriptorSchema, + buildTasks: () => ({ + paths: GENERATED_TEMPLATES.paths.task, + imports: GENERATED_TEMPLATES.imports.task, + renderers: GENERATED_TEMPLATES.renderers.task, + authService: createGeneratorTask({ + dependencies: { + renderers: GENERATED_TEMPLATES.renderers.provider, + }, + run({ renderers }) { + return { + async build(builder) { + await builder.apply(renderers.userSessionService.render({})); + }, + }; + }, + }), + }), +}); diff --git a/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-module/extractor.json b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-module/extractor.json new file mode 100644 index 000000000..271fde091 --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-module/extractor.json @@ -0,0 +1,32 @@ +{ + "name": "placeholder-auth/core/placeholder-auth-module", + "extractors": { + "ts": { + "importProviders": [ + "@baseplate-dev/fastify-generators:userSessionServiceImportsProvider" + ], + "skipDefaultImportMap": true + } + }, + "templates": { + "module/services/user-session.service.ts": { + "name": "user-session-service", + "type": "ts", + "fileOptions": { "kind": "singleton" }, + "generator": "@baseplate-dev/plugin-auth#placeholder-auth/core/placeholder-auth-module", + "importMapProviders": { + "authContextImportsProvider": { + "importName": "authContextImportsProvider", + "packagePathSpecifier": "@baseplate-dev/fastify-generators:src/generators/auth/auth-context/generated/ts-import-providers.ts" + }, + "userSessionTypesImportsProvider": { + "importName": "userSessionTypesImportsProvider", + "packagePathSpecifier": "@baseplate-dev/fastify-generators:src/generators/auth/user-session-types/generated/ts-import-providers.ts" + } + }, + "pathRootRelativePath": "{module-root}/services/user-session.service.ts", + "projectExports": { "userSessionService": {} }, + "variables": {} + } + } +} diff --git a/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-module/generated/index.ts b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-module/generated/index.ts new file mode 100644 index 000000000..8bb8ae1f0 --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-module/generated/index.ts @@ -0,0 +1,11 @@ +import { PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_MODULE_PATHS } from './template-paths.js'; +import { PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_MODULE_RENDERERS } from './template-renderers.js'; +import { PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_MODULE_IMPORTS } from './ts-import-providers.js'; +import { PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_MODULE_TEMPLATES } from './typed-templates.js'; + +export const PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_MODULE_GENERATED = { + imports: PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_MODULE_IMPORTS, + paths: PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_MODULE_PATHS, + renderers: PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_MODULE_RENDERERS, + templates: PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_MODULE_TEMPLATES, +}; diff --git a/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-module/generated/template-paths.ts b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-module/generated/template-paths.ts new file mode 100644 index 000000000..cea3c9ddc --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-module/generated/template-paths.ts @@ -0,0 +1,35 @@ +import { appModuleProvider } from '@baseplate-dev/fastify-generators'; +import { createGeneratorTask, createProviderType } from '@baseplate-dev/sync'; + +export interface PlaceholderAuthCorePlaceholderAuthModulePaths { + userSessionService: string; +} + +const placeholderAuthCorePlaceholderAuthModulePaths = + createProviderType( + 'placeholder-auth-core-placeholder-auth-module-paths', + ); + +const placeholderAuthCorePlaceholderAuthModulePathsTask = createGeneratorTask({ + dependencies: { appModule: appModuleProvider }, + exports: { + placeholderAuthCorePlaceholderAuthModulePaths: + placeholderAuthCorePlaceholderAuthModulePaths.export(), + }, + run({ appModule }) { + const moduleRoot = appModule.getModuleFolder(); + + return { + providers: { + placeholderAuthCorePlaceholderAuthModulePaths: { + userSessionService: `${moduleRoot}/services/user-session.service.ts`, + }, + }, + }; + }, +}); + +export const PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_MODULE_PATHS = { + provider: placeholderAuthCorePlaceholderAuthModulePaths, + task: placeholderAuthCorePlaceholderAuthModulePathsTask, +}; diff --git a/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-module/generated/template-renderers.ts b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-module/generated/template-renderers.ts new file mode 100644 index 000000000..d532bd90d --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-module/generated/template-renderers.ts @@ -0,0 +1,75 @@ +import type { RenderTsTemplateFileActionInput } from '@baseplate-dev/core-generators'; +import type { BuilderAction } from '@baseplate-dev/sync'; + +import { typescriptFileProvider } from '@baseplate-dev/core-generators'; +import { + authContextImportsProvider, + userSessionTypesImportsProvider, +} from '@baseplate-dev/fastify-generators'; +import { createGeneratorTask, createProviderType } from '@baseplate-dev/sync'; + +import { PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_MODULE_PATHS } from './template-paths.js'; +import { PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_MODULE_TEMPLATES } from './typed-templates.js'; + +export interface PlaceholderAuthCorePlaceholderAuthModuleRenderers { + userSessionService: { + render: ( + options: Omit< + RenderTsTemplateFileActionInput< + typeof PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_MODULE_TEMPLATES.userSessionService + >, + 'destination' | 'importMapProviders' | 'template' + >, + ) => BuilderAction; + }; +} + +const placeholderAuthCorePlaceholderAuthModuleRenderers = + createProviderType( + 'placeholder-auth-core-placeholder-auth-module-renderers', + ); + +const placeholderAuthCorePlaceholderAuthModuleRenderersTask = + createGeneratorTask({ + dependencies: { + authContextImports: authContextImportsProvider, + paths: PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_MODULE_PATHS.provider, + typescriptFile: typescriptFileProvider, + userSessionTypesImports: userSessionTypesImportsProvider, + }, + exports: { + placeholderAuthCorePlaceholderAuthModuleRenderers: + placeholderAuthCorePlaceholderAuthModuleRenderers.export(), + }, + run({ + authContextImports, + paths, + typescriptFile, + userSessionTypesImports, + }) { + return { + providers: { + placeholderAuthCorePlaceholderAuthModuleRenderers: { + userSessionService: { + render: (options) => + typescriptFile.renderTemplateFile({ + template: + PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_MODULE_TEMPLATES.userSessionService, + destination: paths.userSessionService, + importMapProviders: { + authContextImports, + userSessionTypesImports, + }, + ...options, + }), + }, + }, + }, + }; + }, + }); + +export const PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_MODULE_RENDERERS = { + provider: placeholderAuthCorePlaceholderAuthModuleRenderers, + task: placeholderAuthCorePlaceholderAuthModuleRenderersTask, +}; diff --git a/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-module/generated/ts-import-providers.ts b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-module/generated/ts-import-providers.ts new file mode 100644 index 000000000..fb14ed3a6 --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-module/generated/ts-import-providers.ts @@ -0,0 +1,37 @@ +import { + createTsImportMap, + packageScope, +} from '@baseplate-dev/core-generators'; +import { + userSessionServiceImportsProvider, + userSessionServiceImportsSchema, +} from '@baseplate-dev/fastify-generators'; +import { createGeneratorTask } from '@baseplate-dev/sync'; + +import { PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_MODULE_PATHS } from './template-paths.js'; + +const placeholderAuthCorePlaceholderAuthModuleImportsTask = createGeneratorTask( + { + dependencies: { + paths: PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_MODULE_PATHS.provider, + }, + exports: { + userSessionServiceImports: + userSessionServiceImportsProvider.export(packageScope), + }, + run({ paths }) { + return { + providers: { + userSessionServiceImports: createTsImportMap( + userSessionServiceImportsSchema, + { userSessionService: paths.userSessionService }, + ), + }, + }; + }, + }, +); + +export const PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_MODULE_IMPORTS = { + task: placeholderAuthCorePlaceholderAuthModuleImportsTask, +}; diff --git a/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-module/generated/typed-templates.ts b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-module/generated/typed-templates.ts new file mode 100644 index 000000000..111dd996f --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-module/generated/typed-templates.ts @@ -0,0 +1,27 @@ +import { createTsTemplateFile } from '@baseplate-dev/core-generators'; +import { + authContextImportsProvider, + userSessionTypesImportsProvider, +} from '@baseplate-dev/fastify-generators'; +import path from 'node:path'; + +const userSessionService = createTsTemplateFile({ + fileOptions: { kind: 'singleton' }, + importMapProviders: { + authContextImports: authContextImportsProvider, + userSessionTypesImports: userSessionTypesImportsProvider, + }, + name: 'user-session-service', + projectExports: { userSessionService: {} }, + source: { + path: path.join( + import.meta.dirname, + '../templates/module/services/user-session.service.ts', + ), + }, + variables: {}, +}); + +export const PLACEHOLDER_AUTH_CORE_PLACEHOLDER_AUTH_MODULE_TEMPLATES = { + userSessionService, +}; diff --git a/plugins/plugin-auth/src/auth/generators/fastify/auth-module/index.ts b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-module/index.ts similarity index 100% rename from plugins/plugin-auth/src/auth/generators/fastify/auth-module/index.ts rename to plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-module/index.ts diff --git a/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-module/templates/module/services/user-session.service.ts b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-module/templates/module/services/user-session.service.ts new file mode 100644 index 000000000..31ee6ce29 --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-auth-module/templates/module/services/user-session.service.ts @@ -0,0 +1,41 @@ +// @ts-nocheck + +import type { AuthUserSessionInfo } from '%authContextImports'; +import type { UserSessionService } from '%userSessionTypesImports'; +import type { FastifyRequest } from 'fastify'; + +export class PlaceholderUserSessionService implements UserSessionService { + /** + * Retrieves the user session information from the request. + * + * @param req - The Fastify request object containing the cookies. + * @returns A promise that resolves to the authenticated user session information or null if the session is invalid. + */ + async getSessionInfoFromRequest( + req: FastifyRequest, + ): Promise { + console.info(req.cookies); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + throw new Error('Not implemented'); + } + + /** + * Retrieves the user session information from the authentication token + * + * Note: Since we use cookies, we ignore the authToken parameter + * + * @param req The request object + * @returns The session info or undefined if no session is found + */ + async getSessionInfoFromToken( + req: FastifyRequest, + ): Promise { + console.info(req.cookies); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + throw new Error('Not implemented'); + } +} + +export const userSessionService = new PlaceholderUserSessionService(); diff --git a/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-react-auth/index.ts b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-react-auth/index.ts new file mode 100644 index 000000000..2300ffc50 --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-react-auth/index.ts @@ -0,0 +1 @@ +export * from './react-auth.generator.js'; diff --git a/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-react-auth/react-auth.generator.ts b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-react-auth/react-auth.generator.ts new file mode 100644 index 000000000..3a26342ac --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/core/generators/placeholder-react-auth/react-auth.generator.ts @@ -0,0 +1,32 @@ +import { packageScope } from '@baseplate-dev/core-generators'; +import { reactAuthRoutesProvider } from '@baseplate-dev/react-generators'; +import { createGenerator, createGeneratorTask } from '@baseplate-dev/sync'; +import { z } from 'zod'; + +const descriptorSchema = z.object({}); + +/** + * Generator for placeholder React auth + */ +export const placeholderReactAuthGenerator = createGenerator({ + name: 'placeholder-auth/core/placeholder-react-auth', + generatorFileUrl: import.meta.url, + descriptorSchema, + buildTasks: () => ({ + reactAuth: createGeneratorTask({ + exports: { + reactAuth: reactAuthRoutesProvider.export(packageScope), + }, + run() { + return { + providers: { + reactAuth: { + getLoginUrlPath: () => '/auth/login', + getRegisterUrlPath: () => '/auth/register', + }, + }, + }; + }, + }), + }), +}); diff --git a/plugins/plugin-auth/src/placeholder-auth/core/index.ts b/plugins/plugin-auth/src/placeholder-auth/core/index.ts new file mode 100644 index 000000000..8e0bbc5af --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/core/index.ts @@ -0,0 +1 @@ +export * from './generators/index.js'; diff --git a/plugins/plugin-auth/src/placeholder-auth/core/node.ts b/plugins/plugin-auth/src/placeholder-auth/core/node.ts new file mode 100644 index 000000000..f88adb1c9 --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/core/node.ts @@ -0,0 +1,73 @@ +import { + adminAppEntryType, + appCompilerSpec, + backendAppEntryType, + createPlatformPluginExport, + PluginUtils, + webAppEntryType, +} from '@baseplate-dev/project-builder-lib'; + +import { + createCommonBackendAuthModuleGenerators, + createCommonBackendAuthRootGenerators, + createCommonWebAuthGenerators, +} from '#src/common/index.js'; + +import type { PlaceholderAuthPluginDefinition } from './schema/plugin-definition.js'; + +import { + placeholderAuthHooksGenerator, + placeholderAuthModuleGenerator, + placeholderReactAuthGenerator, +} from './generators/index.js'; + +export default createPlatformPluginExport({ + dependencies: { + appCompiler: appCompilerSpec, + }, + exports: {}, + initialize: ({ appCompiler }, { pluginId }) => { + // register backend compiler + appCompiler.registerAppCompiler({ + pluginId, + appType: backendAppEntryType, + compile: ({ projectDefinition, appCompiler }) => { + const auth = PluginUtils.configByIdOrThrow( + projectDefinition, + pluginId, + ) as PlaceholderAuthPluginDefinition; + + appCompiler.addChildrenToFeature(auth.authFeatureRef, { + ...createCommonBackendAuthModuleGenerators({ roles: auth.roles }), + authModule: placeholderAuthModuleGenerator({}), + }); + + appCompiler.addRootChildren(createCommonBackendAuthRootGenerators()); + }, + }); + + const sharedWebGenerators = { + ...createCommonWebAuthGenerators(), + reactAuth: placeholderReactAuthGenerator({}), + authHooks: placeholderAuthHooksGenerator({}), + }; + + // register web compiler + appCompiler.registerAppCompiler({ + pluginId, + appType: webAppEntryType, + compile: ({ appCompiler }) => { + appCompiler.addRootChildren(sharedWebGenerators); + }, + }); + appCompiler.registerAppCompiler({ + pluginId, + appType: adminAppEntryType, + compile: ({ appCompiler }) => { + appCompiler.addRootChildren(sharedWebGenerators); + }, + }); + + return {}; + }, +}); diff --git a/plugins/plugin-auth/src/placeholder-auth/core/schema/models.ts b/plugins/plugin-auth/src/placeholder-auth/core/schema/models.ts new file mode 100644 index 000000000..91cd643d0 --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/core/schema/models.ts @@ -0,0 +1,43 @@ +import type { ModelMergerModelInput } from '@baseplate-dev/project-builder-lib'; + +import type { PlaceholderAuthPluginDefinition } from './plugin-definition.js'; + +export function createAuthModels({ + authFeatureRef, + modelRefs, +}: Pick): { + user: ModelMergerModelInput; +} { + return { + user: { + name: modelRefs.user, + featureRef: authFeatureRef, + model: { + fields: [ + { + name: 'id', + type: 'uuid', + options: { genUuid: true }, + }, + { + name: 'email', + type: 'string', + isOptional: true, + }, + ], + primaryKeyFieldRefs: ['id'], + uniqueConstraints: [ + { + fields: [{ fieldRef: 'email' }], + }, + ], + }, + graphql: { + objectType: { + enabled: true, + fields: ['id', 'email'], + }, + }, + }, + }; +} diff --git a/plugins/plugin-auth/src/placeholder-auth/core/schema/plugin-definition.ts b/plugins/plugin-auth/src/placeholder-auth/core/schema/plugin-definition.ts new file mode 100644 index 000000000..586532a81 --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/core/schema/plugin-definition.ts @@ -0,0 +1,31 @@ +import type { def } from '@baseplate-dev/project-builder-lib'; + +import { + definitionSchema, + featureEntityType, + modelEntityType, +} from '@baseplate-dev/project-builder-lib'; +import { z } from 'zod'; + +import { createAuthRolesSchema } from '#src/common/roles/index.js'; + +export const createPlaceholderAuthPluginDefinitionSchema = definitionSchema( + (ctx) => + z.object({ + modelRefs: z.object({ + user: ctx.withRef({ + type: modelEntityType, + onDelete: 'RESTRICT', + }), + }), + authFeatureRef: ctx.withRef({ + type: featureEntityType, + onDelete: 'RESTRICT', + }), + roles: createAuthRolesSchema(ctx), + }), +); + +export type PlaceholderAuthPluginDefinition = def.InferOutput< + typeof createPlaceholderAuthPluginDefinitionSchema +>; diff --git a/plugins/plugin-auth/src/placeholder-auth/core/web.ts b/plugins/plugin-auth/src/placeholder-auth/core/web.ts new file mode 100644 index 000000000..7d57bb9ca --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/core/web.ts @@ -0,0 +1,22 @@ +import { + createPlatformPluginExport, + webConfigSpec, +} from '@baseplate-dev/project-builder-lib'; + +import { PlaceholderAuthDefinitionEditor } from './components/placeholder-auth-definition-editor.js'; + +import '../../styles.css'; + +export default createPlatformPluginExport({ + dependencies: { + webConfig: webConfigSpec, + }, + exports: {}, + initialize: ({ webConfig }, { pluginId }) => { + webConfig.registerWebConfigComponent( + pluginId, + PlaceholderAuthDefinitionEditor, + ); + return {}; + }, +}); diff --git a/plugins/plugin-auth/src/placeholder-auth/index.ts b/plugins/plugin-auth/src/placeholder-auth/index.ts new file mode 100644 index 000000000..17f45946d --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/index.ts @@ -0,0 +1 @@ +export * from './core/index.js'; diff --git a/plugins/plugin-auth/src/placeholder-auth/metadata.json b/plugins/plugin-auth/src/placeholder-auth/metadata.json new file mode 100644 index 000000000..5ec9fe80b --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/metadata.json @@ -0,0 +1,9 @@ +{ + "name": "placeholder-auth", + "displayName": "Placeholder Auth", + "icon": "icon.svg", + "description": "This plugin sets up a placeholder auth service that acts as a template for other auth plugins.", + "version": "0.1.0", + "hidden": true, + "moduleDirectories": ["core"] +} diff --git a/plugins/plugin-auth/src/placeholder-auth/static/icon.svg b/plugins/plugin-auth/src/placeholder-auth/static/icon.svg new file mode 100644 index 000000000..f21439901 --- /dev/null +++ b/plugins/plugin-auth/src/placeholder-auth/static/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/plugin-auth/src/utils/cn.ts b/plugins/plugin-auth/src/utils/cn.ts deleted file mode 100644 index 0f0ba75b9..000000000 --- a/plugins/plugin-auth/src/utils/cn.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const cn = (...classes: (string | undefined | false)[]): string => - classes.filter((x): x is string => !!x).join(' '); diff --git a/plugins/plugin-storage/package.json b/plugins/plugin-storage/package.json index 75edc2e90..c772f4c7a 100644 --- a/plugins/plugin-storage/package.json +++ b/plugins/plugin-storage/package.json @@ -56,7 +56,7 @@ "inflection": "3.0.0", "react": "catalog:", "react-dom": "catalog:", - "react-hook-form": "7.56.3", + "react-hook-form": "7.60.0", "zod": "catalog:" }, "devDependencies": { diff --git a/plugins/plugin-storage/src/generators/react/upload-components/upload-components.generator.ts b/plugins/plugin-storage/src/generators/react/upload-components/upload-components.generator.ts index e07f3a560..bc8bd9571 100644 --- a/plugins/plugin-storage/src/generators/react/upload-components/upload-components.generator.ts +++ b/plugins/plugin-storage/src/generators/react/upload-components/upload-components.generator.ts @@ -6,7 +6,6 @@ import { } from '@baseplate-dev/core-generators'; import { generatedGraphqlImportsProvider, - reactApolloProvider, reactComponentsImportsProvider, reactComponentsProvider, reactErrorImportsProvider, @@ -45,7 +44,6 @@ export const uploadComponentsGenerator = createGenerator({ reactComponents: reactComponentsProvider, reactComponentsImports: reactComponentsImportsProvider, generatedGraphqlImports: generatedGraphqlImportsProvider, - reactApollo: reactApolloProvider, paths: REACT_UPLOAD_COMPONENTS_GENERATED.paths.provider, }, run({ @@ -53,7 +51,6 @@ export const uploadComponentsGenerator = createGenerator({ typescriptFile, reactComponentsImports, generatedGraphqlImports, - reactApollo, reactComponents, paths, }) { @@ -61,8 +58,6 @@ export const uploadComponentsGenerator = createGenerator({ name: 'file-input', }); - reactApollo.registerGqlFile(paths.fileInputUploadGql); - return { build: async (builder) => { await builder.apply( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5467d2db..52eb87ec1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -454,8 +454,8 @@ importers: specifier: 'catalog:' version: 19.1.0 react-hook-form: - specifier: 7.56.3 - version: 7.56.3(react@19.1.0) + specifier: 7.60.0 + version: 7.60.0(react@19.1.0) zod: specifier: 'catalog:' version: 3.24.1 @@ -683,7 +683,7 @@ importers: version: 3.2.2(react@19.1.0) '@hookform/resolvers': specifier: 5.0.1 - version: 5.0.1(react-hook-form@7.56.3(react@19.1.0)) + version: 5.0.1(react-hook-form@7.60.0(react@19.1.0)) '@tanstack/react-router': specifier: 1.124.0 version: 1.124.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -742,8 +742,8 @@ importers: specifier: 6.0.0 version: 6.0.0(react@19.1.0) react-hook-form: - specifier: 7.56.3 - version: 7.56.3(react@19.1.0) + specifier: 7.60.0 + version: 7.60.0(react@19.1.0) react-icons: specifier: 5.5.0 version: 5.5.0(react@19.1.0) @@ -881,6 +881,9 @@ importers: globby: specifier: ^14.0.2 version: 14.0.2 + micromatch: + specifier: 4.0.8 + version: 4.0.8 ms: specifier: 2.1.3 version: 2.1.3 @@ -900,6 +903,9 @@ importers: '@baseplate-dev/tools': specifier: workspace:* version: link:../tools + '@types/micromatch': + specifier: 4.0.9 + version: 4.0.9 '@types/ms': specifier: 0.7.34 version: 0.7.34 @@ -1008,7 +1014,7 @@ importers: version: 5.2.5 '@hookform/resolvers': specifier: 5.0.1 - version: 5.0.1(react-hook-form@7.56.3(react@19.1.0)) + version: 5.0.1(react-hook-form@7.60.0(react@19.1.0)) '@types/react': specifier: 'catalog:' version: 19.1.3 @@ -1043,8 +1049,8 @@ importers: specifier: 'catalog:' version: 19.1.0(react@19.1.0) react-hook-form: - specifier: 7.56.3 - version: 7.56.3(react@19.1.0) + specifier: 7.60.0 + version: 7.60.0(react@19.1.0) react-icons: specifier: 5.5.0 version: 5.5.0(react@19.1.0) @@ -1203,9 +1209,12 @@ importers: '@baseplate-dev/ui-components': specifier: workspace:* version: link:../../packages/ui-components + '@hookform/lenses': + specifier: 0.7.1 + version: 0.7.1(react-hook-form@7.60.0(react@19.1.0))(react@19.1.0) '@hookform/resolvers': specifier: 5.0.1 - version: 5.0.1(react-hook-form@7.56.3(react@19.1.0)) + version: 5.0.1(react-hook-form@7.60.0(react@19.1.0)) react: specifier: 'catalog:' version: 19.1.0 @@ -1213,8 +1222,8 @@ importers: specifier: 'catalog:' version: 19.1.0(react@19.1.0) react-hook-form: - specifier: 7.56.3 - version: 7.56.3(react@19.1.0) + specifier: 7.60.0 + version: 7.60.0(react@19.1.0) react-icons: specifier: 5.5.0 version: 5.5.0(react@19.1.0) @@ -1293,7 +1302,7 @@ importers: version: link:../../packages/utils '@hookform/resolvers': specifier: 5.0.1 - version: 5.0.1(react-hook-form@7.56.3(react@19.1.0)) + version: 5.0.1(react-hook-form@7.60.0(react@19.1.0)) inflection: specifier: 3.0.0 version: 3.0.0 @@ -1304,8 +1313,8 @@ importers: specifier: 'catalog:' version: 19.1.0(react@19.1.0) react-hook-form: - specifier: 7.56.3 - version: 7.56.3(react@19.1.0) + specifier: 7.60.0 + version: 7.60.0(react@19.1.0) zod: specifier: 'catalog:' version: 3.24.1 @@ -1910,6 +1919,13 @@ packages: engines: {node: '>=6'} hasBin: true + '@hookform/lenses@0.7.1': + resolution: {integrity: sha512-lURXz2/mzQFGW6taJi+klKxVqMkwSqh/eZGOzA98svLhYC/63rKyJQTX7mK17NCwDo02j9wYxrG1qD85MBTWYg==} + engines: {bun: '>=1.2'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react-hook-form: ^7.0.0 + '@hookform/resolvers@5.0.1': resolution: {integrity: sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA==} peerDependencies: @@ -3643,6 +3659,9 @@ packages: '@types/babel__traverse@7.20.6': resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + '@types/braces@3.0.5': + resolution: {integrity: sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==} + '@types/culori@2.1.1': resolution: {integrity: sha512-NzLYD0vNHLxTdPp8+RlvGbR2NfOZkwxcYGFwxNtm+WH2NuUNV8785zv1h0sulFQ5aFQ9n/jNDUuJeo3Bh7+oFA==} @@ -3673,6 +3692,9 @@ packages: '@types/mdx@2.0.13': resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} + '@types/micromatch@4.0.9': + resolution: {integrity: sha512-7V+8ncr22h4UoYRLnLXSpTxjQrNUXtWHGeMPRJt1nULXI57G9bIcpyrHlmrQ7QK24EyyuXvYcSSWAM8GA9nqCg==} + '@types/ms@0.7.34': resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} @@ -6513,8 +6535,8 @@ packages: peerDependencies: react: '>=16.13.1' - react-hook-form@7.56.3: - resolution: {integrity: sha512-IK18V6GVbab4TAo1/cz3kqajxbDPGofdF0w7VHdCo0Nt8PrPlOZcuuDq9YYIV1BtjcX78x0XsldbQRQnQXWXmw==} + react-hook-form@7.60.0: + resolution: {integrity: sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 @@ -8315,10 +8337,15 @@ snapshots: protobufjs: 7.4.0 yargs: 17.7.2 - '@hookform/resolvers@5.0.1(react-hook-form@7.56.3(react@19.1.0))': + '@hookform/lenses@0.7.1(react-hook-form@7.60.0(react@19.1.0))(react@19.1.0)': + dependencies: + react: 19.1.0 + react-hook-form: 7.60.0(react@19.1.0) + + '@hookform/resolvers@5.0.1(react-hook-form@7.60.0(react@19.1.0))': dependencies: '@standard-schema/utils': 0.3.0 - react-hook-form: 7.56.3(react@19.1.0) + react-hook-form: 7.60.0(react@19.1.0) '@humanfs/core@0.19.1': {} @@ -10119,6 +10146,8 @@ snapshots: dependencies: '@babel/types': 7.28.0 + '@types/braces@3.0.5': {} + '@types/culori@2.1.1': {} '@types/docker-modem@3.0.6': @@ -10151,6 +10180,10 @@ snapshots: '@types/mdx@2.0.13': {} + '@types/micromatch@4.0.9': + dependencies: + '@types/braces': 3.0.5 + '@types/ms@0.7.34': {} '@types/node@12.20.55': {} @@ -13241,7 +13274,7 @@ snapshots: '@babel/runtime': 7.26.10 react: 19.1.0 - react-hook-form@7.56.3(react@19.1.0): + react-hook-form@7.60.0(react@19.1.0): dependencies: react: 19.1.0