Skip to content

Commit

Permalink
feat: @builder.io/qwik-auth (#2945)
Browse files Browse the repository at this point in the history
  • Loading branch information
manucorporat authored Feb 12, 2023
1 parent e80e1e5 commit dd4fd92
Show file tree
Hide file tree
Showing 15 changed files with 407 additions and 14 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"scripts": {
"build": "tsm scripts/index.ts --tsc --build --qwikcity --api --platform-binding-wasm-copy",
"build.vite": "tsm scripts/index.ts --tsc --build --qwikcity --platform-binding-wasm-copy",
"build.full": "tsm scripts/index.ts --tsc --build --api --eslint --qwikcity --qwikreact --cli --platform-binding --wasm",
"build.full": "tsm scripts/index.ts --tsc --build --api --eslint --qwikcity --qwikreact --qwikauth --cli --platform-binding --wasm",
"build.qwik-city": "tsm scripts/index.ts --tsc --qwikcity",
"build.platform": "tsm scripts/index.ts --platform-binding",
"build.platform.copy": "tsm scripts/index.ts --platform-binding-wasm-copy",
Expand Down
60 changes: 60 additions & 0 deletions packages/qwik-auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Qwik qwik-react ⚡️

- Create a component library
- Vite.js tooling.
- Prettier code formatter.

## Development Builds

### Client only

During development, the index.html is not a result of server-side rendering, but rather the Qwik app is built using client-side JavaScript only. This is ideal for development with Vite and its ability to reload modules quickly and on-demand. However, this mode is only for development and does not showcase "how" Qwik works since JavaScript is required to execute, and Vite imports many development modules for the app to work.

```
npm run dev
```

### Server-side Rendering (SSR) and Client

Server-side rendered index.html, with client-side modules prefetched and loaded by the browser. This can be used to test out server-side rendered content during development, but will be slower than the client-only development builds.

```
npm run dev.ssr
```

## Production Builds

A production build should generate the client and server modules by running both client and server build commands.

```
npm run build
```

### Client Modules

Production build that creates only the client-side modules that are dynamically imported by the browser.

```
npm run build.client
```

### Server Modules

Production build that creates the server-side render (SSR) module that is used by the server to render the HTML.

```
npm run build.server
```

---

## Related

- [Qwik Docs](https://qwik.builder.io/docs/overview/)
- [Qwik on GitHub](https://github.com/BuilderIO/qwik)
- [@QwikDev](https://twitter.com/QwikDev)
- [Discord](https://qwik.builder.io/chat)
- [Vite](https://vitejs.dev/)
- [Partytown](https://partytown.builder.io/)
- [Mitosis](https://github.com/BuilderIO/mitosis)
- [Builder.io](https://www.builder.io/)
54 changes: 54 additions & 0 deletions packages/qwik-auth/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "@builder.io/qwik-auth",
"version": "0.0.1",
"description": "Qwik Auth is powered by Next's Auth.js a battle tested library for authentication with 3rd party providers",
"keywords": [
"auth.js",
"qwik",
"login",
"authentication",
"ssr",
"plugin"
],
"scripts": {
"build": "vite build --mode lib"
},
"engines": {
"node": ">=16"
},
"exports": {
".": {
"import": "./lib/index.js",
"require": "./lib/index.cjs",
"types": "./lib/types/index.d.ts"
}
},
"files": [
"lib"
],
"license": "MIT",
"homepage": "https://qwik.builder.io/",
"repository": {
"type": "git",
"url": "https://github.com/BuilderIO/qwik.git",
"directory": "packages/qwik-auth"
},
"bugs": {
"url": "https://github.com/BuilderIO/qwik/issues"
},
"type": "module",
"main": "./lib/index.js",
"types": "./lib/types/index.d.ts",
"devDependencies": {
"@builder.io/qwik": "workspace:*",
"@builder.io/qwik-city": "workspace:*",
"@types/set-cookie-parser": "^2.4.2",
"@auth/core": ">=0.4.0",
"set-cookie-parser": "^2.5.1"
},
"peerDependencies": {
"@auth/core": ">=0.4.0",
"@builder.io/qwik": ">=0.17.0",
"@builder.io/qwik-city": ">=0.1.0"
}
}
136 changes: 136 additions & 0 deletions packages/qwik-auth/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { Auth, skipCSRFCheck } from '@auth/core';
import type { AuthAction, AuthConfig, Session } from '@auth/core/types';
import { implicit$FirstArg, QRL } from '@builder.io/qwik';
import { action$, loader$, RequestEvent, RequestEventCommon, z, zod$ } from '@builder.io/qwik-city';
import { isServer } from '@builder.io/qwik/build';
import { parseString, splitCookiesString } from 'set-cookie-parser';
export interface QwikAuthConfig extends AuthConfig {}
const actions: AuthAction[] = [
'providers',
'session',
'csrf',
'signin',
'signout',
'callback',
'verify-request',
'error',
];

export async function authAction(
body: URLSearchParams | undefined,
req: RequestEventCommon,
path: string,
authOptions: QwikAuthConfig
) {
const request = new Request(new URL(path, req.request.url), {
method: req.request.method,
headers: req.request.headers,
body: body,
});
const res = await Auth(request, {
...authOptions,
skipCSRFCheck,
});
res.headers.forEach((value, key) => {
req.headers.set(key, value);
});
fixCookies(req);

return await res.json();
}

export const fixCookies = (req: RequestEventCommon) => {
req.headers.set('set-cookie', req.headers.get('set-cookie') || '');
const cookie = req.headers.get('set-cookie');
if (cookie) {
req.headers.delete('set-cookie');
splitCookiesString(cookie).forEach((cookie) => {
const { name, value, ...rest } = parseString(cookie);
req.cookie.set(name, value, rest as any);
});
}
};

export function serverAuthQrl(authOptions: QRL<(ev: RequestEventCommon) => QwikAuthConfig>) {
const useAuthSignup = action$(
async ({ provider, ...rest }, req) => {
const auth = await authOptions(req);
const body = new URLSearchParams();
Object.entries(rest).forEach(([key, value]) => {
body.set(key, String(value));
});
const data = await authAction(body, req, `/api/auth/signin/${provider}`, auth);
if (data.url) {
throw req.redirect(301, data.url);
}
},
zod$({
provider: z.string(),
})
);

const useAuthLogout = action$(async (_, req) => {
const auth = await authOptions(req);
const body = new URLSearchParams();
return authAction(body, req, `/api/auth/logout`, auth);
});

const useAuthSession = loader$((req) => {
return req.sharedMap.get('session') as Session | null;
});

const onRequest = async (req: RequestEvent) => {
if (isServer) {
const prefix: string = '/api/auth';

const action = req.url.pathname.slice(prefix.length + 1).split('/')[0] as AuthAction;

const auth = await authOptions(req);
if (actions.includes(action) && req.url.pathname.startsWith(prefix + '/')) {
const res = await Auth(req.request, auth);
const cookie = res.headers.get('set-cookie');
if (cookie) {
req.headers.set('set-cookie', cookie);
res.headers.delete('set-cookie');
fixCookies(req);
}
throw req.send(res);
} else {
req.sharedMap.set('session', await getSessionData(req.request, auth));
}
}
};

return {
useAuthSignup,
useAuthLogout,
useAuthSession,
onRequest,
};
}

export const serverAuth$ = implicit$FirstArg(serverAuthQrl);

export const ensureAuthMiddleware = (req: RequestEvent) => {
const isLoggedIn = req.sharedMap.has('session');
if (!isLoggedIn) {
throw req.error(403, 'sfs');
}
};

export type GetSessionResult = Promise<Session | null>;

export async function getSessionData(req: Request, options: AuthConfig): GetSessionResult {
options.secret ??= process.env.AUTH_SECRET;
options.trustHost ??= true;

const url = new URL('/api/auth/session', req.url);
const response = await Auth(new Request(url, { headers: req.headers }), options);

const { status = 200 } = response;

const data = await response.json();
if (!data || !Object.keys(data).length) return null;
if (status === 200) return data;
throw new Error(data.message);
}
23 changes: 23 additions & 0 deletions packages/qwik-auth/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { defineConfig } from 'vite';

export default defineConfig(() => {
return {
build: {
minify: false,
target: 'es2020',
outDir: 'lib',
lib: {
entry: ['./src/index.ts'],
formats: ['es', 'cjs'],
},
rollupOptions: {
external: [
'@builder.io/qwik',
'@builder.io/qwik-city',
'@builder.io/qwik/build',
'@auth/core',
],
},
},
};
});
4 changes: 3 additions & 1 deletion packages/qwik-city/runtime/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { QRL } from '@builder.io/qwik';
import { QwikIntrinsicElements } from '@builder.io/qwik';
import { QwikJSX } from '@builder.io/qwik';
import { RequestEvent } from '@builder.io/qwik-city/middleware/request-handler';
import type { RequestEventAction } from '@builder.io/qwik-city/middleware/request-handler';
import { RequestEventAction } from '@builder.io/qwik-city/middleware/request-handler';
import { RequestEventCommon } from '@builder.io/qwik-city/middleware/request-handler';
import { RequestEventLoader } from '@builder.io/qwik-city/middleware/request-handler';
import { RequestHandler } from '@builder.io/qwik-city/middleware/request-handler';
Expand Down Expand Up @@ -300,6 +300,8 @@ export const QwikCityProvider: Component<QwikCityProps>;

export { RequestEvent }

export { RequestEventAction }

export { RequestEventCommon }

export { RequestEventLoader }
Expand Down
1 change: 1 addition & 0 deletions packages/qwik-city/runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type {
RequestHandler,
RequestEvent,
RequestEventLoader,
RequestEventAction,
RequestEventCommon,
RouteParams,
QwikCityPlan,
Expand Down
1 change: 1 addition & 0 deletions packages/qwik-city/runtime/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type {
RequestEvent,
RequestHandler,
RequestEventLoader,
RequestEventAction,
RequestEventCommon,
DeferReturn,
} from '@builder.io/qwik-city/middleware/request-handler';
Expand Down
Loading

0 comments on commit dd4fd92

Please sign in to comment.