init, auth-ui, auth-api, auth-core

This commit is contained in:
echo committed 2025-03-24 00:47:33 +01:00
1 parent 094cdb12f8
commit c527aa7bb7
92 files changed
+21484

No files matched your search

+36
View File
@@ -0,0 +1,36 @@
{
"name": "@mono/auth-api",
"version": "0.1.0",
"private": true,
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts --clean --tsconfig tsconfig.json",
"dev": "tsup src/index.ts --format cjs,esm --dts --watch --clean --tsconfig tsconfig.json",
"clean": "rm -rf dist",
"lint": "eslint src/",
"check-types": "tsc --noEmit"
},
"dependencies": {
"@mono/auth-core": "0.1.0",
"@nestjs/common": "^10.3.3",
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
"bcryptjs": "^2.4.3",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38",
"tsup": "^8.0.2",
"typescript": "^5.3.3"
},
"peerDependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0"
}
}
+37
View File
@@ -0,0 +1,37 @@
import { DynamicModule, Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { JwtStrategy } from './strategies/jwt.strategy';
import { LocalStrategy } from './strategies/local.strategy';
export interface AuthModuleOptions {
jwtSecret: string;
jwtExpiresIn?: string;
}
@Module({})
export class AuthModule {
static register(options: AuthModuleOptions): DynamicModule {
return {
module: AuthModule,
imports: [
PassportModule,
JwtModule.register({
secret: options.jwtSecret,
signOptions: { expiresIn: options.jwtExpiresIn || '1h' },
}),
],
providers: [
AuthService,
LocalStrategy,
JwtStrategy,
{
provide: 'JWT_SECRET',
useValue: options.jwtSecret,
},
],
exports: [AuthService],
};
}
}
+30
View File
@@ -0,0 +1,30 @@
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { User, AuthTokens } from '@mono/auth-core';
import * as bcrypt from 'bcryptjs';
@Injectable()
export class AuthService {
constructor(private jwtService: JwtService) {}
async validatePassword(password: string, hashedPassword: string): Promise<boolean> {
return bcrypt.compare(password, hashedPassword);
}
async hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, 10);
}
async generateTokens(user: User): Promise<AuthTokens> {
const payload = { sub: user.id, email: user.email };
return {
accessToken: this.jwtService.sign(payload),
refreshToken: this.jwtService.sign(payload, { expiresIn: '7d' }),
};
}
async verifyToken(token: string): Promise<any> {
return this.jwtService.verify(token);
}
}
+4
View File
@@ -0,0 +1,4 @@
export * from './auth.module';
export * from './auth.service';
export * from './strategies/jwt.strategy';
export * from './strategies/local.strategy';
@@ -0,0 +1,18 @@
import { Injectable, Inject } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(@Inject('JWT_SECRET') jwtSecret: string) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: jwtSecret,
});
}
async validate(payload: any) {
return { id: payload.sub, email: payload.email };
}
}
@@ -0,0 +1,15 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor() {
super({ usernameField: 'email' });
}
async validate(email: string, password: string): Promise<any> {
// This method should be overridden in the implementing application
throw new UnauthorizedException('Strategy not implemented');
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"composite": true,
"incremental": true,
"baseUrl": "../..",
"paths": {
"@mono/auth-core": ["packages/auth-core/src"],
"@mono/auth-core/*": ["packages/auth-core/src/*"]
}
},
"include": ["src"],
"references": [
{ "path": "../auth-core" }
]
}
+23
View File
@@ -0,0 +1,23 @@
{
"name": "@mono/auth-core",
"version": "0.1.0",
"private": true,
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts --clean --tsconfig tsconfig.json",
"dev": "tsup src/index.ts --format cjs,esm --dts --watch --clean --tsconfig tsconfig.json",
"clean": "rm -rf dist",
"lint": "eslint src/",
"check-types": "tsc --noEmit"
},
"dependencies": {
"zod": "^3.22.4"
},
"devDependencies": {
"tsup": "^8.0.2",
"typescript": "^5.3.3",
"@types/node": "^20.11.24"
}
}
+1
View File
@@ -0,0 +1 @@
export * from './types';
+27
View File
@@ -0,0 +1,27 @@
import { z } from 'zod';
export const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
name: z.string().optional(),
});
export type User = z.infer<typeof UserSchema>;
export const LoginInputSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
export type LoginInput = z.infer<typeof LoginInputSchema>;
export const RegisterInputSchema = LoginInputSchema.extend({
name: z.string().min(2).optional(),
});
export type RegisterInput = z.infer<typeof RegisterInputSchema>;
export interface AuthTokens {
accessToken: string;
refreshToken: string;
}
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"composite": true,
"declaration": true,
"declarationMap": true,
"incremental": true
},
"include": ["src"]
}
+33
View File
@@ -0,0 +1,33 @@
{
"name": "@mono/auth-ui",
"version": "0.1.0",
"private": true,
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsup src/index.tsx --format cjs,esm --dts",
"dev": "tsup src/index.tsx --format cjs,esm --dts --watch",
"clean": "rm -rf dist",
"lint": "eslint src/",
"check-types": "tsc --noEmit"
},
"dependencies": {
"@hookform/resolvers": "^3.10.0",
"@mono/auth-core": "0.1.0",
"react-hook-form": "^7.50.0",
"zod": "^3.24.2",
"zustand": "^4.5.0"
},
"devDependencies": {
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"tsup": "^8.0.2",
"typescript": "^5.3.3"
},
"peerDependencies": {
"next": "^14.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}
@@ -0,0 +1,63 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { LoginInput, LoginInputSchema } from '@mono/auth-core';
interface LoginFormProps {
onSubmit: (data: LoginInput) => Promise<void>;
error?: string;
}
export const LoginForm: React.FC<LoginFormProps> = ({ onSubmit, error }) => {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<LoginInput>({
resolver: zodResolver(LoginInputSchema),
});
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium">
Email
</label>
<input
{...register('email')}
type="email"
id="email"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">{errors.email.message}</p>
)}
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium">
Password
</label>
<input
{...register('password')}
type="password"
id="password"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
/>
{errors.password && (
<p className="mt-1 text-sm text-red-600">{errors.password.message}</p>
)}
</div>
{error && <p className="text-sm text-red-600">{error}</p>}
<button
type="submit"
disabled={isSubmitting}
className="w-full rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50"
>
{isSubmitting ? 'Signing in...' : 'Sign in'}
</button>
</form>
);
};
@@ -0,0 +1,78 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { RegisterInput, RegisterInputSchema } from '@mono/auth-core';
interface RegisterFormProps {
onSubmit: (data: RegisterInput) => Promise<void>;
error?: string;
}
export const RegisterForm: React.FC<RegisterFormProps> = ({ onSubmit, error }) => {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<RegisterInput>({
resolver: zodResolver(RegisterInputSchema),
});
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium">
Email
</label>
<input
{...register('email')}
type="email"
id="email"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">{errors.email.message}</p>
)}
</div>
<div>
<label htmlFor="name" className="block text-sm font-medium">
Name
</label>
<input
{...register('name')}
type="text"
id="name"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
/>
{errors.name && (
<p className="mt-1 text-sm text-red-600">{errors.name.message}</p>
)}
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium">
Password
</label>
<input
{...register('password')}
type="password"
id="password"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
/>
{errors.password && (
<p className="mt-1 text-sm text-red-600">{errors.password.message}</p>
)}
</div>
{error && <p className="text-sm text-red-600">{error}</p>}
<button
type="submit"
disabled={isSubmitting}
className="w-full rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50"
>
{isSubmitting ? 'Creating account...' : 'Create account'}
</button>
</form>
);
};
+54
View File
@@ -0,0 +1,54 @@
import { useAuthStore } from '../store/auth.store';
import { LoginInput, RegisterInput, AuthTokens, User } from '@mono/auth-core';
export interface AuthConfig {
apiUrl: string;
}
export const createAuthHook = (config: AuthConfig) => {
const useAuth = () => {
const { user, accessToken, setUser, setTokens, logout } = useAuthStore();
const login = async (data: LoginInput): Promise<void> => {
const response = await fetch(`${config.apiUrl}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error('Login failed');
}
const { user, ...tokens }: { user: User } & AuthTokens = await response.json();
setUser(user);
setTokens(tokens.accessToken, tokens.refreshToken);
};
const register = async (data: RegisterInput): Promise<void> => {
const response = await fetch(`${config.apiUrl}/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error('Registration failed');
}
const { user, ...tokens }: { user: User } & AuthTokens = await response.json();
setUser(user);
setTokens(tokens.accessToken, tokens.refreshToken);
};
return {
user,
isAuthenticated: !!user && !!accessToken,
login,
register,
logout,
};
};
return useAuth;
};
+4
View File
@@ -0,0 +1,4 @@
export * from './components/LoginForm';
export * from './components/RegisterForm';
export * from './hooks/useAuth';
export * from './store/auth.store';
+20
View File
@@ -0,0 +1,20 @@
import { create } from 'zustand';
import { User } from '@mono/auth-core';
interface AuthState {
user: User | null;
accessToken: string | null;
refreshToken: string | null;
setTokens: (accessToken: string, refreshToken: string) => void;
setUser: (user: User) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>((set) => ({
user: null,
accessToken: null,
refreshToken: null,
setTokens: (accessToken, refreshToken) => set({ accessToken, refreshToken }),
setUser: (user) => set({ user }),
logout: () => set({ user: null, accessToken: null, refreshToken: null }),
}));
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"jsx": "react-jsx"
},
"include": ["src"]
}
+3
View File
@@ -0,0 +1,3 @@
# `@turbo/eslint-config`
Collection of internal eslint configurations.
+32
View File
@@ -0,0 +1,32 @@
import js from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier";
import turboPlugin from "eslint-plugin-turbo";
import tseslint from "typescript-eslint";
import onlyWarn from "eslint-plugin-only-warn";
/**
* A shared ESLint configuration for the repository.
*
* @type {import("eslint").Linter.Config[]}
* */
export const config = [
js.configs.recommended,
eslintConfigPrettier,
...tseslint.configs.recommended,
{
plugins: {
turbo: turboPlugin,
},
rules: {
"turbo/no-undeclared-env-vars": "warn",
},
},
{
plugins: {
onlyWarn,
},
},
{
ignores: ["dist/**"],
},
];
+49
View File
@@ -0,0 +1,49 @@
import js from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier";
import tseslint from "typescript-eslint";
import pluginReactHooks from "eslint-plugin-react-hooks";
import pluginReact from "eslint-plugin-react";
import globals from "globals";
import pluginNext from "@next/eslint-plugin-next";
import { config as baseConfig } from "./base.js";
/**
* A custom ESLint configuration for libraries that use Next.js.
*
* @type {import("eslint").Linter.Config[]}
* */
export const nextJsConfig = [
...baseConfig,
js.configs.recommended,
eslintConfigPrettier,
...tseslint.configs.recommended,
{
...pluginReact.configs.flat.recommended,
languageOptions: {
...pluginReact.configs.flat.recommended.languageOptions,
globals: {
...globals.serviceworker,
},
},
},
{
plugins: {
"@next/next": pluginNext,
},
rules: {
...pluginNext.configs.recommended.rules,
...pluginNext.configs["core-web-vitals"].rules,
},
},
{
plugins: {
"react-hooks": pluginReactHooks,
},
settings: { react: { version: "detect" } },
rules: {
...pluginReactHooks.configs.recommended.rules,
// React scope no longer necessary with new JSX transform.
"react/react-in-jsx-scope": "off",
},
},
];
+24
View File
@@ -0,0 +1,24 @@
{
"name": "@repo/eslint-config",
"version": "0.0.0",
"type": "module",
"private": true,
"exports": {
"./base": "./base.js",
"./next-js": "./next.js",
"./react-internal": "./react-internal.js"
},
"devDependencies": {
"@eslint/js": "^9.22.0",
"@next/eslint-plugin-next": "^15.2.1",
"eslint": "^9.22.0",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-only-warn": "^1.1.0",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-turbo": "^2.4.4",
"globals": "^16.0.0",
"typescript": "^5.8.2",
"typescript-eslint": "^8.26.0"
}
}
+39
View File
@@ -0,0 +1,39 @@
import js from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier";
import tseslint from "typescript-eslint";
import pluginReactHooks from "eslint-plugin-react-hooks";
import pluginReact from "eslint-plugin-react";
import globals from "globals";
import { config as baseConfig } from "./base.js";
/**
* A custom ESLint configuration for libraries that use React.
*
* @type {import("eslint").Linter.Config[]} */
export const config = [
...baseConfig,
js.configs.recommended,
eslintConfigPrettier,
...tseslint.configs.recommended,
pluginReact.configs.flat.recommended,
{
languageOptions: {
...pluginReact.configs.flat.recommended.languageOptions,
globals: {
...globals.serviceworker,
...globals.browser,
},
},
},
{
plugins: {
"react-hooks": pluginReactHooks,
},
settings: { react: { version: "detect" } },
rules: {
...pluginReactHooks.configs.recommended.rules,
// React scope no longer necessary with new JSX transform.
"react/react-in-jsx-scope": "off",
},
},
];
+19
View File
@@ -0,0 +1,19 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"incremental": false,
"isolatedModules": true,
"lib": ["es2022", "DOM", "DOM.Iterable"],
"module": "NodeNext",
"moduleDetection": "force",
"moduleResolution": "NodeNext",
"noUncheckedIndexedAccess": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "ES2022"
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"plugins": [{ "name": "next" }],
"module": "ESNext",
"moduleResolution": "Bundler",
"allowJs": true,
"jsx": "preserve",
"noEmit": true
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"name": "@repo/typescript-config",
"version": "0.0.0",
"private": true,
"license": "MIT",
"publishConfig": {
"access": "public"
}
}
@@ -0,0 +1,7 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"jsx": "react-jsx"
}
}
+4
View File
@@ -0,0 +1,4 @@
import { config } from "@repo/eslint-config/react-internal";
/** @type {import("eslint").Linter.Config} */
export default config;
+27
View File
@@ -0,0 +1,27 @@
{
"name": "@repo/ui",
"version": "0.0.0",
"private": true,
"exports": {
"./*": "./src/*.tsx"
},
"scripts": {
"lint": "eslint . --max-warnings 0",
"generate:component": "turbo gen react-component",
"check-types": "tsc --noEmit"
},
"devDependencies": {
"@repo/eslint-config": "*",
"@repo/typescript-config": "*",
"@turbo/gen": "^2.4.4",
"@types/node": "^22.13.10",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"eslint": "^9.22.0",
"typescript": "5.8.2"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}
+20
View File
@@ -0,0 +1,20 @@
"use client";
import { ReactNode } from "react";
interface ButtonProps {
children: ReactNode;
className?: string;
appName: string;
}
export const Button = ({ children, className, appName }: ButtonProps) => {
return (
<button
className={className}
onClick={() => alert(`Hello from your ${appName} app!`)}
>
{children}
</button>
);
};
+27
View File
@@ -0,0 +1,27 @@
import { type JSX } from "react";
export function Card({
className,
title,
children,
href,
}: {
className?: string;
title: string;
children: React.ReactNode;
href: string;
}): JSX.Element {
return (
<a
className={className}
href={`${href}?utm_source=create-turbo&utm_medium=basic&utm_campaign=create-turbo"`}
rel="noopener noreferrer"
target="_blank"
>
<h2>
{title} <span>-&gt;</span>
</h2>
<p>{children}</p>
</a>
);
}
+11
View File
@@ -0,0 +1,11 @@
import { type JSX } from "react";
export function Code({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}): JSX.Element {
return <code className={className}>{children}</code>;
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "@repo/typescript-config/react-library.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
+30
View File
@@ -0,0 +1,30 @@
import type { PlopTypes } from "@turbo/gen";
// Learn more about Turborepo Generators at https://turbo.build/repo/docs/core-concepts/monorepos/code-generation
export default function generator(plop: PlopTypes.NodePlopAPI): void {
// A simple generator to add a new React component to the internal UI library
plop.setGenerator("react-component", {
description: "Adds a new react component",
prompts: [
{
type: "input",
name: "name",
message: "What is the name of the component?",
},
],
actions: [
{
type: "add",
path: "src/{{kebabCase name}}.tsx",
templateFile: "templates/component.hbs",
},
{
type: "append",
path: "package.json",
pattern: /"exports": {(?<insertion>)/g,
template: ' "./{{kebabCase name}}": "./src/{{kebabCase name}}.tsx",',
},
],
});
}
@@ -0,0 +1,8 @@
export const {{ pascalCase name }} = ({ children }: { children: React.ReactNode }) => {
return (
<div>
<h1>{{ pascalCase name }} Component</h1>
{children}
</div>
);
};