Skip to content

Commit

Permalink
feat(next): init and sign in route
Browse files Browse the repository at this point in the history
  • Loading branch information
wangsijie committed Jul 18, 2022
1 parent b235f68 commit 04abe94
Show file tree
Hide file tree
Showing 19 changed files with 1,047 additions and 4 deletions.
7 changes: 7 additions & 0 deletions packages/next-sample/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "@silverhand/react",
"rules": {
"consistent-default-export-name/default-export-match-filename": "off",
"react/react-in-jsx-scope": "off"
}
}
1 change: 1 addition & 0 deletions packages/next-sample/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.next
9 changes: 9 additions & 0 deletions packages/next-sample/libraries/logto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import LogtoClient from '@logto/next';

export const logtoClient = new LogtoClient({
appId: 'foo',
endpoint: 'https://logto.dev',
baseUrl: 'http://localhost:3000',
cookieSecret: 'complex_password_at_least_32_characters_long',
cookieSecure: process.env.NODE_ENV === 'production',
});
5 changes: 5 additions & 0 deletions packages/next-sample/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
37 changes: 37 additions & 0 deletions packages/next-sample/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@logto/next-sample",
"version": "1.0.0-alpha.2",
"license": "MIT",
"private": true,
"scripts": {
"build": "next build",
"dev": "next dev",
"start": "next start"
},
"dependencies": {
"@logto/next": "^1.0.0-alpha.2",
"next": "^12.2.2",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@silverhand/eslint-config": "^0.14.0",
"@silverhand/eslint-config-react": "^0.14.0",
"@silverhand/ts-config": "^0.14.0",
"@silverhand/ts-config-react": "^0.14.0",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11",
"@types/react-router-dom": "^5.3.3",
"eslint": "^8.9.0",
"lint-staged": "^13.0.0",
"postcss": "^8.4.6",
"postcss-modules": "^4.3.0",
"prettier": "^2.5.1",
"stylelint": "^14.8.2",
"typescript": "^4.5.5"
},
"stylelint": {
"extends": "@silverhand/eslint-config-react/.stylelintrc"
},
"prettier": "@silverhand/eslint-config/.prettierrc"
}
3 changes: 3 additions & 0 deletions packages/next-sample/pages/api/sign-in.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { logtoClient } from '../../libraries/logto';

export default logtoClient.handleSignIn();
14 changes: 14 additions & 0 deletions packages/next-sample/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Link from 'next/link';

const Home = () => {
return (
<div>
Hello Logto.{' '}
<Link href="/api/sign-in">
<a>Sign In</a>
</Link>
</div>
);
};

export default Home;
15 changes: 15 additions & 0 deletions packages/next-sample/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"extends": "@silverhand/ts-config-react/tsconfig.base",
"compilerOptions": {
"jsx": "preserve",
"incremental": true
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
}
10 changes: 10 additions & 0 deletions packages/next/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { Config } from '@jest/types';

const config: Config.InitialOptions = {
preset: 'ts-jest',
collectCoverageFrom: ['src/**/*.ts'],
coverageReporters: ['lcov', 'text-summary'],
setupFilesAfterEnv: ['jest-matcher-specific-error'],
};

export default config;
71 changes: 71 additions & 0 deletions packages/next/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{
"name": "@logto/next",
"version": "1.0.0-alpha.2",
"source": "./src/index.ts",
"main": "./lib/index.js",
"exports": {
"require": "./lib/index.js",
"import": "./lib/module.js"
},
"module": "./lib/module.js",
"types": "./lib/index.d.ts",
"files": [
"lib"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/logto-io/js.git",
"directory": "packages/next"
},
"scripts": {
"dev:tsc": "tsc -p tsconfig.build.json -w --preserveWatchOutput",
"precommit": "lint-staged",
"check": "tsc --noEmit",
"build": "rm -rf lib/ && pnpm check && parcel build",
"lint": "eslint --ext .ts src",
"test": "jest",
"test:coverage": "jest --silent --coverage",
"prepack": "pnpm test"
},
"dependencies": {
"@logto/node": "^1.0.0-alpha.2",
"iron-session": "^6.1.3"
},
"devDependencies": {
"@jest/types": "^27.5.1",
"@parcel/core": "^2.6.2",
"@parcel/packager-ts": "^2.6.2",
"@parcel/transformer-typescript-types": "^2.6.2",
"@silverhand/eslint-config": "^0.14.0",
"@silverhand/ts-config": "^0.14.0",
"@silverhand/ts-config-react": "^0.14.0",
"@types/jest": "^27.4.0",
"eslint": "^8.9.0",
"jest": "^27.5.1",
"jest-location-mock": "^1.0.9",
"jest-matcher-specific-error": "^1.0.0",
"lint-staged": "^13.0.0",
"next": "^12.2.2",
"next-test-api-route-handler": "^3.1.6",
"parcel": "^2.6.2",
"prettier": "^2.3.2",
"ts-jest": "^27.0.4",
"typescript": "^4.5.5"
},
"eslintConfig": {
"extends": "@silverhand"
},
"prettier": "@silverhand/eslint-config/.prettierrc",
"publishConfig": {
"access": "public"
},
"targets": {
"main": {
"context": "node",
"engines": {
"node": "^16"
}
}
}
}
72 changes: 72 additions & 0 deletions packages/next/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { testApiHandler } from 'next-test-api-route-handler';

import LogtoClient from '.';
import { LogtoNextConfig } from './types';

const signInUrl = 'http://mock-logto-server.com/sign-in';

const configs: LogtoNextConfig = {
appId: 'app_id_value',
endpoint: 'https://logto.dev',
baseUrl: 'http://localhost:3000',
cookieSecret: 'complex_password_at_least_32_characters_long',
cookieSecure: process.env.NODE_ENV === 'production',
};

const setItem = jest.fn((key, value) => {
console.log(key, value);
});
const getItem = jest.fn();
const save = jest.fn();
const signIn = jest.fn();

jest.mock('./storage', () =>
jest.fn(() => ({
setItem,
getItem,
removeItem: jest.fn(),
save: () => {
save();
},
}))
);

type Adapter = {
navigate: (url: string) => void;
};

jest.mock('@logto/node', () =>
jest.fn((_: unknown, { navigate }: Adapter) => ({
signIn: () => {
navigate(signInUrl);
signIn();
},
}))
);

describe('Next', () => {
afterEach(() => {
jest.clearAllMocks();
});

it('creates an instance without crash', () => {
expect(() => new LogtoClient(configs)).not.toThrow();
});

describe('handleSignIn', () => {
it('should redirect to Logto sign in url and save session', async () => {
const client = new LogtoClient(configs);
await testApiHandler({
handler: client.handleSignIn(),
url: '/api/sign-in',
test: async ({ fetch }) => {
const response = await fetch({ method: 'GET', redirect: 'manual' });
const headers = response.headers as Map<string, string>;
expect(headers.get('location')).toEqual(signInUrl);
},
});
expect(save).toHaveBeenCalled();
expect(signIn).toHaveBeenCalled();
});
});
});
44 changes: 44 additions & 0 deletions packages/next/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import NodeClient from '@logto/node';
import { withIronSessionApiRoute } from 'iron-session/next';
import { NextApiHandler, NextApiRequest } from 'next';

import NextStorage from './storage';
import { LogtoNextConfig } from './types';

export default class LogtoClient {
private navigateUrl?: string;
private storage?: NextStorage;
constructor(private readonly config: LogtoNextConfig) {}

handleSignIn = (redirectUri = `${this.config.baseUrl}/api/sign-in`): NextApiHandler =>
this.withIronSession(async (request, response) => {
const nodeClient = this.createNodeClient(request);
await nodeClient.signIn(redirectUri);
await this.storage?.save();

if (this.navigateUrl) {
response.redirect(this.navigateUrl);
}
});

private createNodeClient(request: NextApiRequest) {
this.storage = new NextStorage(request);

return new NodeClient(this.config, {
storage: this.storage,
navigate: (url) => {
this.navigateUrl = url;
},
});
}

private withIronSession(handler: NextApiHandler) {
return withIronSessionApiRoute(handler, {
cookieName: `logto:${this.config.appId}`,
password: this.config.cookieSecret,
cookieOptions: {
secure: this.config.cookieSecure,
},
});
}
}
46 changes: 46 additions & 0 deletions packages/next/src/storage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import NextStorage from './storage';
import { NextRequestWithIronSession } from './types';

const makeRequest = (): NextRequestWithIronSession => {
const request = {
session: {
save: jest.fn(),
},
};

return request as unknown as NextRequestWithIronSession;
};

describe('NextStorage', () => {
describe('Basic functions', () => {
it('should set and get item', async () => {
const request = makeRequest();
const storage = new NextStorage(request);
await storage.setItem('idToken', 'value');
expect(storage.getItem('idToken')).toBe('value');
});

it('should remove item', async () => {
const request = makeRequest();
const storage = new NextStorage(request);
await storage.setItem('idToken', 'value');
storage.removeItem('idToken');
expect(storage.getItem('idToken')).toBeNull();
});

it('should set and get item (signInSession)', async () => {
const request = makeRequest();
const storage = new NextStorage(request);
await storage.setItem('signInSession', 'value');
expect(storage.getItem('signInSession')).toBe('value');
});

it('should remove item (signInSession)', async () => {
const request = makeRequest();
const storage = new NextStorage(request);
await storage.setItem('signInSession', 'value');
storage.removeItem('signInSession');
expect(storage.getItem('signInSession')).toBeNull();
});
});
});
29 changes: 29 additions & 0 deletions packages/next/src/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Storage, StorageKey } from '@logto/node';

import { NextRequestWithIronSession } from './types';

export default class NextStorage implements Storage {
constructor(private readonly request: NextRequestWithIronSession) {}

async setItem(key: StorageKey, value: string) {
this.request.session[key] = value;
}

getItem(key: StorageKey) {
const value = this.request.session[key];

if (value === undefined) {
return null;
}

return String(value);
}

removeItem(key: StorageKey) {
this.request.session[key] = undefined;
}

async save() {
await this.request.session.save();
}
}
20 changes: 20 additions & 0 deletions packages/next/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { LogtoConfig } from '@logto/node';
import { IronSession } from 'iron-session';
import { NextApiRequest } from 'next';

export type NextRequestWithIronSession = NextApiRequest & { session: IronSession };

declare module 'iron-session' {
interface IronSessionData {
accessToken?: string;
idToken?: string;
signInSession?: string;
refreshToken?: string;
}
}

export type LogtoNextConfig = LogtoConfig & {
cookieSecret: string;
cookieSecure: boolean;
baseUrl: string;
};
Loading

0 comments on commit 04abe94

Please sign in to comment.