diff --git a/apps/api/Controllers/AccountController.cs b/apps/api/Controllers/AccountController.cs new file mode 100644 index 0000000..b426bfa --- /dev/null +++ b/apps/api/Controllers/AccountController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Api.Controllers; + +public record CreateAccountRequest(string Email, string Password); + +public record CreateAccountResponse(int Id, string Email); + + +[ApiController] +[Route("api/[controller]")] +public class AccountController : ControllerBase +{ + [HttpPost] + public ActionResult Register(CreateAccountRequest request) + { + return this.Ok(new CreateAccountResponse(1, request.Email)); + } +} diff --git a/apps/api/Program.cs b/apps/api/Program.cs index 8d4058d..4824690 100644 --- a/apps/api/Program.cs +++ b/apps/api/Program.cs @@ -10,6 +10,15 @@ builder.WebHost.UseUrls($"http://0.0.0.0:{port}"); builder.Services.AddControllers(); + +if (builder.Environment.IsDevelopment()) +{ + builder.Services.AddCors(options => + { + options.AddDefaultPolicy(policy => + policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()); + }); +} builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options => { @@ -61,6 +70,11 @@ [new OpenApiSecuritySchemeReference("Bearer", doc)] = [], c.RoutePrefix = "api/docs"; }); +if (app.Environment.IsDevelopment()) +{ + app.UseCors(); +} + app.MapControllers(); Console.WriteLine($"Starting ExpressThat Auth API on port {port}..., http://localhost:{port}/api/docs"); diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 259053a..2c41c3f 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,7 +1,5 @@ -"use client"; - import { Button } from "@expressthat-auth/internal-components/button"; -import { ExLoginBox, ExTestButton } from "@expressthat-auth/ui-react"; +import { ExLoginBox, ExRegisterBox, ExTestButton } from "@expressthat-auth/ui-react/next"; export default function Home() { return ( @@ -9,17 +7,15 @@ export default function Home() {

ExpressThat Auth

Welcome to the authentication portal.

- { - alert("test"); - }} - /> +

test

+ + +

test

+
); } diff --git a/packages/ui/package.json b/packages/ui/package.json index ec59b31..64e8af8 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -4,6 +4,7 @@ "private": true, "description": "StencilJS web component library for ExpressThat Auth", "type": "module", + "main": "dist/index.cjs.js", "module": "dist/components/index.js", "collection": "dist/collection/collection-manifest.json", "types": "dist/types/index.d.ts", @@ -50,11 +51,13 @@ "check-types": "tsc --noEmit" }, "devDependencies": { + "@expressthat-auth/api-client": "workspace:*", "@expressthat-auth/typescript-config": "workspace:*", "@stencil/angular-output-target": "^0.10.0", "@stencil/core": "^4.30.0", "@stencil/react-output-target": "^1.2.0", "@stencil/vue-output-target": "^0.13.1", + "esbuild": "^0.28.0", "typescript": "6.0.3" } } diff --git a/packages/ui/src/components.d.ts b/packages/ui/src/components.d.ts index 286e58d..e8900d5 100644 --- a/packages/ui/src/components.d.ts +++ b/packages/ui/src/components.d.ts @@ -6,7 +6,9 @@ */ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; import { EXLoginBoxSubmitDetail } from "./components/ex-login-box/ex-login-box"; +import { EXRegisterBoxSubmitDetail } from "./components/ex-register-box/ex-register-box"; export { EXLoginBoxSubmitDetail } from "./components/ex-login-box/ex-login-box"; +export { EXRegisterBoxSubmitDetail } from "./components/ex-register-box/ex-register-box"; export namespace Components { interface ExButton { "colorBackground"?: string; @@ -152,6 +154,34 @@ export namespace Components { "radiusSm"?: string; "radiusXl"?: string; } + interface ExRegisterBox { + "colorBackground"?: string; + "colorDivider"?: string; + "colorError"?: string; + "colorInputBackground"?: string; + "colorInputBorder"?: string; + "colorInputBorderFocus"?: string; + "colorLink"?: string; + "colorPrimary"?: string; + "colorPrimaryForeground"?: string; + "colorPrimaryHover"?: string; + "colorSurface"?: string; + "colorText"?: string; + "colorTextMuted"?: string; + "fontFamily"?: string; + "fontSizeLg"?: string; + "fontSizeMd"?: string; + "fontSizeSm"?: string; + "fontSizeXl"?: string; + "fontSizeXs"?: string; + "fontWeightMedium"?: string; + "fontWeightNormal"?: string; + "fontWeightSemibold"?: string; + "radiusLg"?: string; + "radiusMd"?: string; + "radiusSm"?: string; + "radiusXl"?: string; + } interface ExTestButton { /** * Whether the button is disabled. @@ -182,6 +212,10 @@ export interface ExLoginBoxCustomEvent extends CustomEvent { detail: T; target: HTMLExLoginBoxElement; } +export interface ExRegisterBoxCustomEvent extends CustomEvent { + detail: T; + target: HTMLExRegisterBoxElement; +} export interface ExTestButtonCustomEvent extends CustomEvent { detail: T; target: HTMLExTestButtonElement; @@ -240,6 +274,24 @@ declare global { prototype: HTMLExLoginBoxElement; new (): HTMLExLoginBoxElement; }; + interface HTMLExRegisterBoxElementEventMap { + "exSubmit": EXRegisterBoxSubmitDetail; + "exSignIn": void; + } + interface HTMLExRegisterBoxElement extends Components.ExRegisterBox, HTMLStencilElement { + addEventListener(type: K, listener: (this: HTMLExRegisterBoxElement, ev: ExRegisterBoxCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLExRegisterBoxElement, ev: ExRegisterBoxCustomEvent) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; + } + var HTMLExRegisterBoxElement: { + prototype: HTMLExRegisterBoxElement; + new (): HTMLExRegisterBoxElement; + }; interface HTMLExTestButtonElementEventMap { "exTestClick": void; } @@ -261,6 +313,7 @@ declare global { "ex-button": HTMLExButtonElement; "ex-input": HTMLExInputElement; "ex-login-box": HTMLExLoginBoxElement; + "ex-register-box": HTMLExRegisterBoxElement; "ex-test-button": HTMLExTestButtonElement; } } @@ -429,6 +482,42 @@ declare namespace LocalJSX { "radiusSm"?: string; "radiusXl"?: string; } + interface ExRegisterBox { + "colorBackground"?: string; + "colorDivider"?: string; + "colorError"?: string; + "colorInputBackground"?: string; + "colorInputBorder"?: string; + "colorInputBorderFocus"?: string; + "colorLink"?: string; + "colorPrimary"?: string; + "colorPrimaryForeground"?: string; + "colorPrimaryHover"?: string; + "colorSurface"?: string; + "colorText"?: string; + "colorTextMuted"?: string; + "fontFamily"?: string; + "fontSizeLg"?: string; + "fontSizeMd"?: string; + "fontSizeSm"?: string; + "fontSizeXl"?: string; + "fontSizeXs"?: string; + "fontWeightMedium"?: string; + "fontWeightNormal"?: string; + "fontWeightSemibold"?: string; + /** + * Fired when the "Sign in" link is clicked. + */ + "onExSignIn"?: (event: ExRegisterBoxCustomEvent) => void; + /** + * Fired when the form is submitted; detail contains email + password. + */ + "onExSubmit"?: (event: ExRegisterBoxCustomEvent) => void; + "radiusLg"?: string; + "radiusMd"?: string; + "radiusSm"?: string; + "radiusXl"?: string; + } interface ExTestButton { /** * Whether the button is disabled. @@ -548,6 +637,34 @@ declare namespace LocalJSX { "radiusLg": string; "radiusXl": string; } + interface ExRegisterBoxAttributes { + "colorBackground": string; + "colorSurface": string; + "colorPrimary": string; + "colorPrimaryHover": string; + "colorPrimaryForeground": string; + "colorText": string; + "colorTextMuted": string; + "colorInputBackground": string; + "colorInputBorder": string; + "colorInputBorderFocus": string; + "colorError": string; + "colorDivider": string; + "colorLink": string; + "fontFamily": string; + "fontSizeXs": string; + "fontSizeSm": string; + "fontSizeMd": string; + "fontSizeLg": string; + "fontSizeXl": string; + "fontWeightNormal": string; + "fontWeightMedium": string; + "fontWeightSemibold": string; + "radiusSm": string; + "radiusMd": string; + "radiusLg": string; + "radiusXl": string; + } interface ExTestButtonAttributes { "label": string; "variant": "primary" | "secondary" | "outline"; @@ -558,6 +675,7 @@ declare namespace LocalJSX { "ex-button": Omit & { [K in keyof ExButton & keyof ExButtonAttributes]?: ExButton[K] } & { [K in keyof ExButton & keyof ExButtonAttributes as `attr:${K}`]?: ExButtonAttributes[K] } & { [K in keyof ExButton & keyof ExButtonAttributes as `prop:${K}`]?: ExButton[K] }; "ex-input": Omit & { [K in keyof ExInput & keyof ExInputAttributes]?: ExInput[K] } & { [K in keyof ExInput & keyof ExInputAttributes as `attr:${K}`]?: ExInputAttributes[K] } & { [K in keyof ExInput & keyof ExInputAttributes as `prop:${K}`]?: ExInput[K] }; "ex-login-box": Omit & { [K in keyof ExLoginBox & keyof ExLoginBoxAttributes]?: ExLoginBox[K] } & { [K in keyof ExLoginBox & keyof ExLoginBoxAttributes as `attr:${K}`]?: ExLoginBoxAttributes[K] } & { [K in keyof ExLoginBox & keyof ExLoginBoxAttributes as `prop:${K}`]?: ExLoginBox[K] }; + "ex-register-box": Omit & { [K in keyof ExRegisterBox & keyof ExRegisterBoxAttributes]?: ExRegisterBox[K] } & { [K in keyof ExRegisterBox & keyof ExRegisterBoxAttributes as `attr:${K}`]?: ExRegisterBoxAttributes[K] } & { [K in keyof ExRegisterBox & keyof ExRegisterBoxAttributes as `prop:${K}`]?: ExRegisterBox[K] }; "ex-test-button": Omit & { [K in keyof ExTestButton & keyof ExTestButtonAttributes]?: ExTestButton[K] } & { [K in keyof ExTestButton & keyof ExTestButtonAttributes as `attr:${K}`]?: ExTestButtonAttributes[K] } & { [K in keyof ExTestButton & keyof ExTestButtonAttributes as `prop:${K}`]?: ExTestButton[K] }; } } @@ -568,6 +686,7 @@ declare module "@stencil/core" { "ex-button": LocalJSX.IntrinsicElements["ex-button"] & JSXBase.HTMLAttributes; "ex-input": LocalJSX.IntrinsicElements["ex-input"] & JSXBase.HTMLAttributes; "ex-login-box": LocalJSX.IntrinsicElements["ex-login-box"] & JSXBase.HTMLAttributes; + "ex-register-box": LocalJSX.IntrinsicElements["ex-register-box"] & JSXBase.HTMLAttributes; "ex-test-button": LocalJSX.IntrinsicElements["ex-test-button"] & JSXBase.HTMLAttributes; } } diff --git a/packages/ui/src/components/ex-button/readme.md b/packages/ui/src/components/ex-button/readme.md index eb925ba..72b6f41 100644 --- a/packages/ui/src/components/ex-button/readme.md +++ b/packages/ui/src/components/ex-button/readme.md @@ -61,11 +61,13 @@ ### Used by - [ex-login-box](../ex-login-box) + - [ex-register-box](../ex-register-box) ### Graph ```mermaid graph TD; ex-login-box --> ex-button + ex-register-box --> ex-button style ex-button fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/packages/ui/src/components/ex-input/readme.md b/packages/ui/src/components/ex-input/readme.md index aa978c5..61373ca 100644 --- a/packages/ui/src/components/ex-input/readme.md +++ b/packages/ui/src/components/ex-input/readme.md @@ -66,11 +66,13 @@ ### Used by - [ex-login-box](../ex-login-box) + - [ex-register-box](../ex-register-box) ### Graph ```mermaid graph TD; ex-login-box --> ex-input + ex-register-box --> ex-input style ex-input fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/packages/ui/src/components/ex-register-box/ex-register-box.css b/packages/ui/src/components/ex-register-box/ex-register-box.css new file mode 100644 index 0000000..5d4e87f --- /dev/null +++ b/packages/ui/src/components/ex-register-box/ex-register-box.css @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/packages/ui/src/components/ex-register-box/ex-register-box.tsx b/packages/ui/src/components/ex-register-box/ex-register-box.tsx new file mode 100644 index 0000000..b55915a --- /dev/null +++ b/packages/ui/src/components/ex-register-box/ex-register-box.tsx @@ -0,0 +1,221 @@ +// biome-ignore lint/correctness/noUnusedImports: Stencil requires explicit h factory and decorator imports + +import { createExpressThatAuthClient } from "@expressthat-auth/api-client"; +import { Component, Event, type EventEmitter, h, State } from "@stencil/core"; +import { ThemeBase } from "../../theme-base"; + +export interface EXRegisterBoxSubmitDetail { + email: string; + password: string; +} + +@Component({ + tag: "ex-register-box", + styleUrl: "ex-register-box.css", + shadow: true, +}) +export class EXRegisterBox extends ThemeBase { + /** Fired when the form is submitted; detail contains email + password. */ + @Event() exSubmit!: EventEmitter; + + /** Fired when the "Sign in" link is clicked. */ + @Event() exSignIn!: EventEmitter; + + @State() private email: string = ""; + @State() private password: string = ""; + @State() private confirmPassword: string = ""; + @State() private showPassword: boolean = false; + @State() private showConfirmPassword: boolean = false; + @State() private confirmError: string = ""; + + private client = createExpressThatAuthClient("http://localhost:3001"); + + private handleSubmit = (e?: Event) => { + console.log("Submitting registration form with email:", this.email); + e?.preventDefault(); + if (this.password !== this.confirmPassword) { + this.confirmError = "Passwords do not match"; + return; + } + this.confirmError = ""; + + this.client.account + .register({ + email: this.email, + password: this.password, + }) + .then((response) => { + console.log(`Registration successful: Email: ${response.email}, User ID: ${response.id}`); + }) + .catch((error) => { + console.error("Registration failed:", error); + }); + + this.exSubmit.emit({ email: this.email, password: this.password }); + }; + + render() { + const theme = this.resolvedTheme; + const passwordToggleLabel = this.showPassword ? "Hide" : "Show"; + const confirmToggleLabel = this.showConfirmPassword ? "Hide" : "Show"; + + return ( +
+
+ {/* Header */} +
+

+ Create an account +

+

+ Sign up to get started +

+
+ + {/* Form */} +
+ {/* Email field */} +
+ ) => (this.email = e.detail)} + /> +
+ + {/* Password field */} +
+ ) => (this.password = e.detail)} + onExRightButtonClick={() => (this.showPassword = !this.showPassword)} + /> +
+ + {/* Confirm password field */} +
+ ) => (this.confirmPassword = e.detail)} + onExRightButtonClick={() => (this.showConfirmPassword = !this.showConfirmPassword)} + /> +
+ +
+ +
+
+ + {/* Social logins slot */} +
+
+
+ + or continue with + +
+
+ +
+ + {/* Sign in link */} +

+ Already have an account?{" "} + +

+
+
+ ); + } +} diff --git a/packages/ui/src/components/ex-register-box/readme.md b/packages/ui/src/components/ex-register-box/readme.md new file mode 100644 index 0000000..1af26fc --- /dev/null +++ b/packages/ui/src/components/ex-register-box/readme.md @@ -0,0 +1,72 @@ +# ex-register-box + + + + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| ------------------------ | -------------------------- | ----------- | --------------------- | ----------- | +| `colorBackground` | `color-background` | | `string \| undefined` | `undefined` | +| `colorDivider` | `color-divider` | | `string \| undefined` | `undefined` | +| `colorError` | `color-error` | | `string \| undefined` | `undefined` | +| `colorInputBackground` | `color-input-background` | | `string \| undefined` | `undefined` | +| `colorInputBorder` | `color-input-border` | | `string \| undefined` | `undefined` | +| `colorInputBorderFocus` | `color-input-border-focus` | | `string \| undefined` | `undefined` | +| `colorLink` | `color-link` | | `string \| undefined` | `undefined` | +| `colorPrimary` | `color-primary` | | `string \| undefined` | `undefined` | +| `colorPrimaryForeground` | `color-primary-foreground` | | `string \| undefined` | `undefined` | +| `colorPrimaryHover` | `color-primary-hover` | | `string \| undefined` | `undefined` | +| `colorSurface` | `color-surface` | | `string \| undefined` | `undefined` | +| `colorText` | `color-text` | | `string \| undefined` | `undefined` | +| `colorTextMuted` | `color-text-muted` | | `string \| undefined` | `undefined` | +| `fontFamily` | `font-family` | | `string \| undefined` | `undefined` | +| `fontSizeLg` | `font-size-lg` | | `string \| undefined` | `undefined` | +| `fontSizeMd` | `font-size-md` | | `string \| undefined` | `undefined` | +| `fontSizeSm` | `font-size-sm` | | `string \| undefined` | `undefined` | +| `fontSizeXl` | `font-size-xl` | | `string \| undefined` | `undefined` | +| `fontSizeXs` | `font-size-xs` | | `string \| undefined` | `undefined` | +| `fontWeightMedium` | `font-weight-medium` | | `string \| undefined` | `undefined` | +| `fontWeightNormal` | `font-weight-normal` | | `string \| undefined` | `undefined` | +| `fontWeightSemibold` | `font-weight-semibold` | | `string \| undefined` | `undefined` | +| `radiusLg` | `radius-lg` | | `string \| undefined` | `undefined` | +| `radiusMd` | `radius-md` | | `string \| undefined` | `undefined` | +| `radiusSm` | `radius-sm` | | `string \| undefined` | `undefined` | +| `radiusXl` | `radius-xl` | | `string \| undefined` | `undefined` | + + +## Events + +| Event | Description | Type | +| ---------- | ------------------------------------------------------------------- | ---------------------------------------- | +| `exSignIn` | Fired when the "Sign in" link is clicked. | `CustomEvent` | +| `exSubmit` | Fired when the form is submitted; detail contains email + password. | `CustomEvent` | + + +## Shadow Parts + +| Part | Description | +| ----------- | ----------- | +| `"sign-in"` | | + + +## Dependencies + +### Depends on + +- [ex-input](../ex-input) +- [ex-button](../ex-button) + +### Graph +```mermaid +graph TD; + ex-register-box --> ex-input + ex-register-box --> ex-button + style ex-register-box fill:#f9f,stroke:#333,stroke-width:4px +``` + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 19283ca..db73755 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -1,6 +1,7 @@ export { EXButton } from "./components/ex-button/ex-button"; export { EXInput } from "./components/ex-input/ex-input"; export { EXLoginBox } from "./components/ex-login-box/ex-login-box"; +export { EXRegisterBox } from "./components/ex-register-box/ex-register-box"; export { EXTestButton } from "./components/ex-test-button/ex-test-button"; export type { EXTheme } from "./theme"; export { DEFAULT_THEME } from "./theme"; diff --git a/packages/ui/stencil.config.ts b/packages/ui/stencil.config.ts index 8284043..417a5d7 100644 --- a/packages/ui/stencil.config.ts +++ b/packages/ui/stencil.config.ts @@ -2,10 +2,28 @@ import { angularOutputTarget } from "@stencil/angular-output-target"; import type { Config } from "@stencil/core"; import { reactOutputTarget } from "@stencil/react-output-target"; import { vueOutputTarget } from "@stencil/vue-output-target"; +import { transform } from "esbuild"; +import type { Plugin } from "rollup"; + +// Rollup plugin to transpile TypeScript files from workspace packages +// (e.g. @expressthat-auth/api-client uses JIT source exports pointing to .ts files) +const externalTsPlugin: Plugin = { + name: "external-ts", + async transform(code, id) { + if (id.endsWith(".ts") && !id.includes("/ui/src/")) { + const result = await transform(code, { loader: "ts", sourcemap: false }); + return { code: result.code }; + } + return null; + }, +}; export const config: Config = { namespace: "expressthat-auth-ui", minifyJs: false, + rollupPlugins: { + before: [externalTsPlugin], + }, outputTargets: [ { type: "dist", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d37f89..35eaa3d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -700,6 +700,9 @@ importers: packages/ui: devDependencies: + '@expressthat-auth/api-client': + specifier: workspace:* + version: link:../api-client '@expressthat-auth/typescript-config': specifier: workspace:* version: link:../typescript-config @@ -715,6 +718,9 @@ importers: '@stencil/vue-output-target': specifier: ^0.13.1 version: 0.13.1(@stencil/core@4.43.4)(vue-router@5.0.6(@vue/compiler-sfc@3.5.33)(vue@3.5.33(typescript@6.0.3)))(vue@3.5.33(typescript@6.0.3)) + esbuild: + specifier: ^0.28.0 + version: 0.28.0 typescript: specifier: 6.0.3 version: 6.0.3