├── .deno_plugins └── deno_mongo_b46c3052210113f9e1610280c5cde542.dylib ├── .gitignore ├── LICENSE ├── README.md ├── example ├── deps.ts ├── server │ ├── .DS_Store │ ├── models │ │ └── userModels.ts │ ├── onyx-setup.ts │ ├── routes.ts │ ├── server.tsx │ └── ssrConstants.tsx ├── test │ └── server.test.ts └── views │ ├── App.tsx │ ├── assets │ └── style.css │ └── components │ ├── Home.tsx │ ├── Inputs.tsx │ ├── MainContainer.tsx │ ├── Message.tsx │ ├── NavBar.tsx │ └── Protected.tsx ├── mod.ts ├── src ├── middleware │ └── authenticate.ts ├── onyx.ts ├── sessionManager.ts ├── strategies │ └── local-strategy │ │ └── local-strategy.ts ├── strategy.ts └── tests │ ├── local-strategy.test.ts │ └── onyx.test.ts └── test_deps.ts /.deno_plugins/deno_mongo_b46c3052210113f9e1610280c5cde542.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/onyx/903a302bb77accf9fe7a2ef5369fe67ff7e6e31d/.deno_plugins/deno_mongo_b46c3052210113f9e1610280c5cde542.dylib -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | scripts.json 2 | settings.json 3 | .DS_Store 4 | dump.rdb -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Onyx Logo](https://i.imgur.com/SglpX1j.png) 2 | 3 | Welcome to Onyx! 4 | 5 | Onyx is authentication middleware for Deno, inspired by [Passport.js](http://www.passportjs.org/). Like Passport, Onyx prioritizes modularization and flexibility — it abstracts much of the authentication process away yet leaves exact control of the verification procedure up to the developer. 6 | 7 | Onyx's primary concern is keeping code clean and organized. Through the use of specialized instructions called strategies, which are held in individual modules, you can streamline your authentication process without importing unnecessary dependencies. 8 | 9 | All you need is one line to get started. 10 | 11 | ```typescript 12 | import onyx from 'https://deno.land/x/onyx/mod.ts' 13 | ``` 14 | 15 | When you import the Onyx module, what you're really importing is an instance of 'Onyx' that has a number of built-in methods. While some of these methods you will invoke yourself, others are primarily used by Onyx under the hood to assist with the authentication process. However, if you are a developer interested in creating new or custom strategies for Onyx, it will likely be important to understand how these work. 16 | 17 | By the way, you will need to use the [Oak](https://deno.land/x/oak@v6.3.1) framework for Deno to use Onyx. Additionally, you will need to set up the [session module](https://deno.land/x/session@1.1.0) on the server to use persistent sessions with Onyx. 18 | 19 | ## Where to Start 20 | 21 | Before doing anything else, it's important to import the authentication strategies you want to use in your application. These strategies are available on our [website](http://onyxts.land) and on our [Deno.land page](https://deno.land/x/onyx/src/strategies). 22 | 23 | For example, importing in the local strategy looks like this. 24 | 25 | ```typescript 26 | import LocalStrategy from 'https://deno.land/x/onyx/src/strategies/local-strategy/local-strategy.ts' 27 | ``` 28 | 29 | Next, let's go over Onyx's most vital methods: `onyx.use()`, `onyx.authenticate()` and `onyx.initialize()`. 30 | 31 | ### onyx.use 32 | 33 | `onyx.use()` configures and stores a strategy to to be implemented later on by Onyx. This step must be completed first in order to continue authentication process. After all, without a strategy, Onyx doesn't have anything to use to complete the authentication process. 34 | 35 | ```typescript 36 | onyx.use( 37 | new LocalStrategy( 38 | async (username: string, password: string, done: Function) => { 39 | // const { username, password } = context.state.onyx.user; 40 | console.log( 41 | `verify function invoked with username ${username} and password ${password}` 42 | ); 43 | try { 44 | const user = await User.findOne({ username }); 45 | if (user && password === user.password) await done(null, user); 46 | else await done(null); 47 | } catch (error) { 48 | await done(error); 49 | } 50 | } 51 | ) 52 | ); 53 | ``` 54 | To be clear, the developer must provide the user verification callback that the strategy will use, so that it will work for your particular application. 55 | 56 | ### onyx.authenticate 57 | 58 | `onyx.authenticate()` is the heart of Onyx — it's what you will use to initiate an authenticate process. 59 | 60 | When you want to authenticate a user, simply invoke `onyx.authenticate()` and pass in a reference to the strategy you stored with `onyx.use()` above. 61 | 62 | ```typescript 63 | onyx.authenticate('local'); 64 | ``` 65 | 66 | ### onyx.initialize 67 | 68 | As you might expect, `onyx.initialize()` creates a new instance of Onyx for each user and sets up its initial state. Though it's a simple line of code, it is vital for ensuring Onyx autehnticates each individual user properly. 69 | 70 | ```typescript 71 | app.use(onyx.initialize()); 72 | ``` 73 | Because a new instance should be created for each user, in this example, we are using Oak's `app.use()` function, which will invoke `onyx.initialize()` when a user makes an initial get request to the website/application. 74 | 75 | ## Serialization and Deserialization 76 | 77 | Use the following two functions if you are creating persistent sessions for your users. 78 | 79 | ### onyx.serializeUser 80 | 81 | Similar to `onyx.use()`, `onyx.serializeUser()` stores a callback that will be invoked later upon successful verification and authentication. This callback should serialize and store user information in some sort of session database. 82 | 83 | ```typescript 84 | onyx.serializeUser(async function (user: any, cb: Function) { 85 | await cb(null, user._id.$oid); 86 | }); 87 | ``` 88 | 89 | Once again, the developer must provide the serializer callback that `serializeUser()` will store. 90 | 91 | ### onyx.deserializeUser 92 | 93 | Stores a callback you write that will be invoked later upon successful verification and authentication. This callback should deserialize user information, checking to see if the user has an existing session. If so, it should then grab the relevant user data from wherever you stored it. 94 | 95 | ```typescript 96 | onyx.deserializeUser(async function (id: string, cb: Function) { 97 | const _id = { $oid: id }; 98 | try { 99 | const user = await User.findOne({ _id }); 100 | await cb(null, user); 101 | } catch (error) { 102 | await cb(error, null); 103 | } 104 | }); 105 | ``` 106 | 107 | And yes, the developer must provide the deserializer callback that `deserializeUser()` will store. 108 | 109 | ## Digging Deeper 110 | 111 | While the following methods are not required to authenticate with Onyx, you may find them useful to understand if you are creating a custom strategy. 112 | 113 | ### onyx.unuse 114 | 115 | `onyx.unuse()` does exactly what it sounds like it does: It deletes the strategy you stored when using `onyx.use()`. 116 | 117 | ```typescript 118 | onyx.unuse('local') 119 | ``` 120 | 121 | ### onyx.init 122 | 123 | `onyx.init()` is invoked every time a new instance of an Onyx object is created — it's actually in Onyx's constructor, so you probably won't have to worry about calling it yourself. 124 | 125 | It creates an instance of the session manager, which controls and creates user sessions. 126 | 127 | ## Developed by 128 | [Connie Cho](https://github.com/chcho2), [Alice Fu](https://github.com/alicejfu), [Chris Kopcow](https://github.com/opennoise1), [GK](https://github.com) and [Cedric Lee](https://github.com/leeced94) 129 | 130 | Logo by [Amanda Maduri](https://www.linkedin.com/in/amanda-maduri/) 131 | -------------------------------------------------------------------------------- /example/deps.ts: -------------------------------------------------------------------------------- 1 | // Standard Library Dependencies 2 | export { join, dirname } from 'https://deno.land/std@0.74.0/path/mod.ts'; 3 | export * as log from 'https://deno.land/std@0.74.0/log/mod.ts'; 4 | 5 | // Third Party Dependencies 6 | 7 | // onyx 8 | export { default as onyx } from 'https://deno.land/x/onyx@v1.0/mod.ts'; 9 | export { default as LocalStrategy } from 'https://deno.land/x/onyx@v1.0/src/strategies/local-strategy/local-strategy.ts'; 10 | 11 | // oak 12 | export { 13 | Application, 14 | Router, 15 | send, 16 | } from 'https://deno.land/x/oak@v6.3.1/mod.ts'; 17 | 18 | // dotenv 19 | export { config } from 'https://deno.land/x/dotenv/mod.ts'; 20 | 21 | // mongo 22 | export { MongoClient } from 'https://deno.land/x/mongo@v0.12.1/mod.ts'; 23 | 24 | // deno session 25 | export { Session } from 'https://deno.land/x/session@1.1.0/mod.ts'; 26 | 27 | // react 28 | export { default as React } from 'https://dev.jspm.io/react@16.14.0'; 29 | export { default as ReactDOMServer } from 'https://dev.jspm.io/react-dom@16.14.0/server'; 30 | -------------------------------------------------------------------------------- /example/server/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/onyx/903a302bb77accf9fe7a2ef5369fe67ff7e6e31d/example/server/.DS_Store -------------------------------------------------------------------------------- /example/server/models/userModels.ts: -------------------------------------------------------------------------------- 1 | import { MongoClient } from '../../deps.ts'; 2 | 3 | const client = new MongoClient(); 4 | const Mongo_URI: string = Deno.env.get('MONGOURI')!; 5 | client.connectWithUri(Mongo_URI); 6 | 7 | interface UserSchema { 8 | _id: { $oid: string }; 9 | username: string; 10 | password: string; 11 | } 12 | 13 | const db = client.database('userdatabase'); 14 | const User = db.collection('user'); 15 | 16 | export default User; 17 | -------------------------------------------------------------------------------- /example/server/onyx-setup.ts: -------------------------------------------------------------------------------- 1 | import { onyx, LocalStrategy } from '../deps.ts'; 2 | import User from './models/userModels.ts'; 3 | 4 | // Configure the Strategy, constructor takes up to 2 arguments 5 | // 1: optional: options 6 | // 2: required: provide verify function that will receive username, password, and a callback function. Verify the username/password is correct before invoking the callback function. 7 | onyx.use( 8 | new LocalStrategy( 9 | async (username: string, password: string, done: Function) => { 10 | try { 11 | const user = await User.findOne({ username }); 12 | if (user && password === user.password) await done(null, user); 13 | else await done(null); 14 | } catch (error) { 15 | await done(error); 16 | } 17 | } 18 | ) 19 | ); 20 | 21 | // user needs to provide the serializer function that will specify what to store in the session db - typically just the user id 22 | onyx.serializeUser(async function (user: any, cb: Function) { 23 | await cb(null, user._id.$oid); 24 | }); 25 | 26 | // user needs to provide the deserializer function that will receive what was stored in the session db as the first argument to query the user db for the user object 27 | onyx.deserializeUser(async function (id: string, cb: Function) { 28 | const _id = { $oid: id }; 29 | try { 30 | const user = await User.findOne({ _id }); 31 | await cb(null, user); 32 | } catch (error) { 33 | await cb(error, null); 34 | } 35 | }); 36 | -------------------------------------------------------------------------------- /example/server/routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from '../deps.ts'; 2 | import { onyx } from '../deps.ts'; 3 | import User from './models/userModels.ts'; 4 | 5 | const router = new Router(); 6 | 7 | router.post('/register', async (ctx) => { 8 | const body = await ctx.request.body(); 9 | const { username, password } = await body.value; 10 | const _id = await User.insertOne({ username, password }); 11 | 12 | // option 1: construct a user object and invoke ctx.state.logIn 13 | const user = { username, _id }; 14 | 15 | await ctx.state.logIn(ctx, user, async (err: any) => { 16 | if (err) return ctx.throw(err); 17 | else { 18 | ctx.response.body = { 19 | success: true, 20 | username, 21 | isAuth: true, 22 | }; 23 | } 24 | }); 25 | 26 | // option 2: invoke onyx.authenticate - see login route for reference 27 | }); 28 | 29 | // invoke onyx.authenticate with the name of the strategy, invoke the result with context 30 | router.post('/login', async (ctx) => { 31 | await (await onyx.authenticate('local'))(ctx); 32 | 33 | if (await ctx.state.isAuthenticated()) { 34 | const { username } = await ctx.state.getUser(); 35 | ctx.response.body = { 36 | success: true, 37 | username, 38 | isAuth: true, 39 | }; 40 | } else { 41 | const message = ctx.state.onyx.errorMessage || 'login unsuccessful'; 42 | ctx.response.body = { 43 | success: false, 44 | message, 45 | isAuth: false, 46 | }; 47 | } 48 | }); 49 | 50 | // invoke ctx.state.logOut in the logout path 51 | router.get('/logout', async (ctx) => { 52 | await ctx.state.logOut(ctx); 53 | ctx.response.body = { 54 | success: true, 55 | isAuth: false, 56 | }; 57 | }); 58 | 59 | // isAuthenticated will return true if user if Authenticated 60 | router.get('/protected', async (ctx) => { 61 | if (await ctx.state.isAuthenticated()) { 62 | const user = await ctx.state.getUser(); 63 | const { username } = user; 64 | ctx.response.body = { 65 | success: true, 66 | isAuth: true, 67 | username, 68 | }; 69 | } else { 70 | ctx.response.body = { 71 | success: true, 72 | isAuth: false, 73 | }; 74 | } 75 | }); 76 | export default router; 77 | -------------------------------------------------------------------------------- /example/server/server.tsx: -------------------------------------------------------------------------------- 1 | import { Application, send, join, log } from '../deps.ts'; 2 | import { Session } from '../deps.ts'; 3 | 4 | // Import in onyx and setup 5 | import { onyx } from '../deps.ts'; 6 | import './onyx-setup.ts'; 7 | 8 | // Server Middlewares 9 | import router from './routes.ts'; 10 | 11 | // SSR 12 | import { html, browserBundlePath, js } from './ssrConstants.tsx'; 13 | 14 | const port: number = Number(Deno.env.get('PORT')) || 4000; 15 | const app: Application = new Application(); 16 | 17 | // session with Server Memory 18 | // const session = new Session({ framework: 'oak' }); 19 | 20 | // session with Redis Database 21 | const session = new Session({ 22 | framework: 'oak', 23 | store: 'redis', 24 | hostname: '127.0.0.1', 25 | port: 6379, 26 | }); 27 | 28 | // Initialize Session 29 | await session.init(); 30 | app.use(session.use()(session)); 31 | 32 | // Initialize onyx after session 33 | app.use(onyx.initialize()); 34 | 35 | // Error Notification 36 | app.addEventListener('error', (event) => { 37 | log.error(event.error); 38 | }); 39 | 40 | // Error Handling 41 | app.use(async (ctx, next) => { 42 | try { 43 | await next(); 44 | } catch (error) { 45 | console.log('in error handling with error', error); 46 | throw error; 47 | } 48 | }); 49 | 50 | // Track response time in headers of responses 51 | app.use(async (ctx, next) => { 52 | await next(); 53 | const rt = ctx.response.headers.get('X-Response-Time'); 54 | console.log( 55 | `${ctx.request.method} ${ctx.request.url} - Response Time = ${rt}` 56 | ); 57 | }); 58 | 59 | app.use(async (ctx, next) => { 60 | const start = Date.now(); 61 | await next(); 62 | const ms = Date.now() - start; 63 | ctx.response.headers.set('X-Response-Time', `${ms}ms`); 64 | }); 65 | 66 | // router 67 | app.use(router.routes()); 68 | app.use(router.allowedMethods()); 69 | 70 | app.use(async (ctx, next) => { 71 | const filePath = ctx.request.url.pathname; 72 | if (filePath === '/') { 73 | ctx.response.type = `text/html`; 74 | ctx.response.body = html; 75 | } else if (filePath === browserBundlePath) { 76 | ctx.response.type = 'application/javascript'; 77 | ctx.response.body = js; 78 | } else if (filePath === '/style.css') { 79 | ctx.response.type = 'text/css'; 80 | await send(ctx, filePath, { 81 | root: join(Deno.cwd(), 'example/views/assets'), 82 | }); 83 | } else await next(); 84 | }); 85 | 86 | // Error handler 87 | app.use(async (ctx) => { 88 | ctx.throw(500); 89 | }); 90 | 91 | if (import.meta.main) { 92 | log.info(`Server is up and running on ${port}`); 93 | await app.listen({ port }); 94 | } 95 | 96 | export { app }; 97 | // use this command to run example server 98 | // deno run --allow-env --allow-net --allow-read --allow-write --allow-plugin --unstable example/server/server.tsx 99 | -------------------------------------------------------------------------------- /example/server/ssrConstants.tsx: -------------------------------------------------------------------------------- 1 | import { React, ReactDOMServer } from '../deps.ts'; 2 | import App from '../views/App.tsx'; 3 | import Inputs from '../views/components/Inputs.tsx'; 4 | import Message from '../views/components/Message.tsx'; 5 | import Home from '../views/components/Home.tsx'; 6 | import MainContainer from '../views/components/MainContainer.tsx'; 7 | import NavBar from '../views/components/NavBar.tsx'; 8 | import Protected from '../views/components/Protected.tsx'; 9 | 10 | const browserBundlePath: string = '/browser.js'; 11 | 12 | const html: string = `
${(ReactDOMServer as any).renderToString( 13 | 14 | )}
`; 15 | 16 | const js: string = `import React from "https://dev.jspm.io/react@16.14.0"; 17 | \nimport ReactDOM from "https://dev.jspm.io/react-dom@16.14.0"; 18 | \nconst Message = ${Message}; 19 | \nconst Inputs = ${Inputs}; 20 | \nconst Protected = ${Protected}; 21 | \nconst Home = ${Home}; 22 | \nconst NavBar = ${NavBar}; 23 | \nconst MainContainer = ${MainContainer}; 24 | \nReactDOM.hydrate(React.createElement(${App}), document.getElementById("root"));`; 25 | 26 | export { browserBundlePath, html, js }; 27 | -------------------------------------------------------------------------------- /example/test/server.test.ts: -------------------------------------------------------------------------------- 1 | import { superoak, describe, it, expect } from '../../test_deps.ts'; 2 | import { app } from '../server/server.tsx'; 3 | 4 | describe('GET request to root url', () => { 5 | it('Sends 200 Status and Content Type text/html', async (done: any) => { 6 | (await superoak(app)).get('/').end((err, res) => { 7 | expect(res.status).toEqual(200); 8 | expect(res.type).toEqual('text/html'); 9 | done(); 10 | }); 11 | }); 12 | }); 13 | 14 | describe('GET request to hydrate front end', () => { 15 | it('Sends 200 Status and Content Type application/javascript', async (done: any) => { 16 | (await superoak(app)).get('/browser.js').end((err, res) => { 17 | expect(res.status).toEqual(200); 18 | expect(res.type).toEqual('application/javascript'); 19 | done(); 20 | }); 21 | }); 22 | }); 23 | 24 | describe('GET request to serve css file', () => { 25 | it('Sends 200 Status and Content Type text/css', async (done: any) => { 26 | (await superoak(app)).get('/style.css').end((err, res) => { 27 | expect(res.status).toEqual(200); 28 | expect(res.type).toEqual('text/css'); 29 | done(); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /example/views/App.tsx: -------------------------------------------------------------------------------- 1 | import { React } from '../deps.ts'; 2 | import MainContainer from './components/MainContainer.tsx'; 3 | import NavBar from './components/NavBar.tsx'; 4 | 5 | declare global { 6 | namespace JSX { 7 | interface IntrinsicElements { 8 | button: any; 9 | img: any; 10 | input: any; 11 | div: any; 12 | h1: any; 13 | h3: any; 14 | p: any; 15 | } 16 | } 17 | } 18 | 19 | const App = () => { 20 | const [page, setPage] = (React as any).useState('home'); 21 | 22 | return ( 23 |
24 | 25 | 26 |
27 | ); 28 | }; 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /example/views/assets/style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Montserrat&display=swap'); 2 | 3 | ::placeholder { 4 | /* Chrome, Firefox, Opera, Safari 10.1+ */ 5 | color: #b3b3b3; 6 | } 7 | 8 | :-ms-input-placeholder { 9 | /* Internet Explorer 10-11 */ 10 | color: #b3b3b3; 11 | } 12 | 13 | ::-ms-input-placeholder { 14 | /* Microsoft Edge */ 15 | color: #b3b3b3; 16 | } 17 | 18 | body { 19 | background-image: url('https://i.imgur.com/Ij90P0z.jpg'); 20 | background-position: center top; 21 | background-repeat: no-repeat; 22 | background-size: 2000px auto; 23 | } 24 | 25 | .page { 26 | display: flex; 27 | flex-direction: column; 28 | align-items: center; 29 | } 30 | 31 | .everything { 32 | margin-top: 80px; 33 | } 34 | 35 | #formBox { 36 | background-image: url('https://i.imgur.com/SvOsXcq.png'); 37 | background-position: center; 38 | background-repeat: no-repeat; 39 | background-size: 500px auto; 40 | height: 650px; 41 | width: 500px; 42 | display: flex; 43 | flex-direction: column; 44 | justify-content: center; 45 | align-items: center; 46 | } 47 | 48 | .input-border { 49 | position: relative; 50 | margin-right: 7px; 51 | margin-top: 1.5em; 52 | margin-bottom: 1.5em; 53 | border: 1.8px solid white; 54 | border-radius: 17px; 55 | width: 350px; 56 | height: 48px; 57 | transition: box-shadow 0.3s; 58 | background-color: white; 59 | } 60 | 61 | .input-border:hover { 62 | box-shadow: 0 0 10px white; 63 | } 64 | 65 | .input-field { 66 | position: absolute; 67 | left: 35px; 68 | top: 9px; 69 | width: 275px; 70 | outline: none; 71 | border: none; 72 | background-color: white; 73 | 74 | font-family: 'Montserrat', sans-serif; 75 | font-size: 16pt; 76 | color: black; 77 | } 78 | 79 | .buttonDiv { 80 | margin-right: 7px; 81 | display: flex; 82 | width: 350px; 83 | justify-content: space-between; 84 | align-items: center; 85 | } 86 | 87 | .buttons { 88 | padding-bottom: 12px; 89 | padding-top: 10px; 90 | border: 2.2px solid white; 91 | border-radius: 17px; 92 | font-size: 16pt; 93 | font-family: 'Montserrat', sans-serif; 94 | color: white; 95 | width: 150px; 96 | text-align: center; 97 | transition: background-color 0.3s; 98 | cursor: pointer; 99 | } 100 | 101 | .buttons:hover { 102 | background-color: white; 103 | color: black; 104 | } 105 | 106 | .message { 107 | margin-top: 35px; 108 | display: flex; 109 | flex-direction: column; 110 | justify-content: center; 111 | align-items: center; 112 | color: white; 113 | font-size: 12pt; 114 | font-family: 'Montserrat', sans-serif; 115 | } 116 | 117 | .navBtn { 118 | background-color: black; 119 | color: white; 120 | border: 2.2px solid white; 121 | border-radius: 17px; 122 | transition: background-color 0.3s; 123 | cursor: pointer; 124 | padding-bottom: 5px; 125 | padding-top: 5px; 126 | width: 100px; 127 | text-align: center; 128 | margin: 3px; 129 | } 130 | 131 | .navBtn:hover { 132 | background-color: white; 133 | color: black; 134 | } 135 | .navBtnText { 136 | font-family: 'Montserrat', sans-serif; 137 | } -------------------------------------------------------------------------------- /example/views/components/Home.tsx: -------------------------------------------------------------------------------- 1 | import { React } from '../../deps.ts'; 2 | 3 | const Home: any = () => { 4 | return ( 5 |
6 |

Home

7 |
8 | ); 9 | }; 10 | 11 | export default Home; 12 | -------------------------------------------------------------------------------- /example/views/components/Inputs.tsx: -------------------------------------------------------------------------------- 1 | import { React } from '../../deps.ts'; 2 | import Message from './Message.tsx'; 3 | 4 | const Inputs: any = (props: any) => { 5 | const { setPage } = props; 6 | const [username, setUsername] = (React as any).useState(''); 7 | const [password, setPassword] = (React as any).useState(''); 8 | const [message, setMessage] = (React as any).useState(''); 9 | 10 | const usernameOnChange = (e: any) => { 11 | setUsername(e.target.value); 12 | }; 13 | 14 | const passwordOnChange = (e: any) => { 15 | setPassword(e.target.value); 16 | }; 17 | 18 | const submit = (path: string) => { 19 | if (username.trim() === '' || password.trim() === '') { 20 | return; 21 | } 22 | 23 | fetch(`/${path}`, { 24 | method: 'POST', 25 | body: JSON.stringify({ username, password }), 26 | headers: { 27 | 'Content-type': 'Application/json', 28 | }, 29 | }) 30 | .then((data) => data.json()) 31 | .then((data) => { 32 | setMessage(data.success); 33 | setUsername(''); 34 | setPassword(''); 35 | if (data.isAuth) setPage('protected'); 36 | }) 37 | .catch((e) => { 38 | console.log(e); 39 | }); 40 | }; 41 | 42 | return ( 43 |
44 |
45 |
46 | 54 |
55 |

56 |
57 | 65 |
66 |

67 |
68 |
69 |
{ 73 | submit(evt.target.id); 74 | }} 75 | > 76 | Log in 77 |
78 |
{ 82 | submit(evt.target.id); 83 | }} 84 | > 85 | Sign up 86 |
87 |
88 |

89 | 90 |
91 | ); 92 | }; 93 | 94 | export default Inputs; 95 | -------------------------------------------------------------------------------- /example/views/components/MainContainer.tsx: -------------------------------------------------------------------------------- 1 | import { React } from '../../deps.ts'; 2 | import Inputs from './Inputs.tsx'; 3 | import Home from './Home.tsx'; 4 | import Protected from './Protected.tsx'; 5 | 6 | const MainContainer: any = (props: any) => { 7 | const { page, setPage } = props; 8 | 9 | let curPage: any; 10 | 11 | if (page === 'home') curPage = ; 12 | if (page === 'protected') curPage = ; 13 | if (page === 'entry') 14 | curPage = ( 15 |
16 | 17 |
18 | ); 19 | 20 | return
{curPage}
; 21 | }; 22 | 23 | export default MainContainer; 24 | -------------------------------------------------------------------------------- /example/views/components/Message.tsx: -------------------------------------------------------------------------------- 1 | import { React } from '../../deps.ts'; 2 | 3 | const Message: any = (props: any) => { 4 | if (props.success === false) { 5 | return ( 6 | <> 7 |
Hmmm, doesn't look right. Try again!
8 | 9 | ); 10 | } else { 11 | return <>; 12 | } 13 | }; 14 | 15 | export default Message; 16 | -------------------------------------------------------------------------------- /example/views/components/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import { React } from '../../deps.ts'; 2 | 3 | const NavBar: any = (props: any) => { 4 | const { setPage } = props; 5 | 6 | const checkAuth = () => { 7 | console.log('submit!'); 8 | fetch(`/protected`, { 9 | method: 'GET', 10 | headers: { 11 | 'Content-type': 'Application/json', 12 | }, 13 | }) 14 | .then((data) => data.json()) 15 | .then((data) => { 16 | if (data.isAuth) { 17 | setPage('protected'); 18 | } else { 19 | setPage('entry'); 20 | } 21 | }) 22 | .catch((e) => { 23 | console.log(e); 24 | }); 25 | }; 26 | 27 | return ( 28 |
29 | 37 | 45 |
46 | ); 47 | }; 48 | 49 | export default NavBar; 50 | -------------------------------------------------------------------------------- /example/views/components/Protected.tsx: -------------------------------------------------------------------------------- 1 | import { React } from '../../deps.ts'; 2 | 3 | const Protected: any = (props: any) => { 4 | const { setPage } = props; 5 | const logout = () => { 6 | fetch('/logout', { 7 | method: 'GET', 8 | headers: { 9 | 'Content-type': 'Application/json', 10 | }, 11 | }) 12 | .then((data) => data.json()) 13 | .then((data) => { 14 | if (data.success) { 15 | setPage('entry'); 16 | } 17 | }) 18 | .catch((err) => console.log(JSON.stringify(err))); 19 | }; 20 | return ( 21 |
22 |

Protected Page

23 | 24 |
{ 28 | logout(); 29 | }} 30 | > 31 | Log out 32 |
33 |
34 | ); 35 | }; 36 | 37 | export default Protected; 38 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | import Onyx from './src/onyx.ts'; 2 | export default new Onyx(); 3 | -------------------------------------------------------------------------------- /src/middleware/authenticate.ts: -------------------------------------------------------------------------------- 1 | export default function authenticate( 2 | onyx: any, 3 | name: string | Array, 4 | options?: any, 5 | callback?: Function 6 | ) { 7 | if (typeof options === 'function') { 8 | callback = options; 9 | options = {}; 10 | } 11 | options = options || {}; 12 | 13 | let multi = true; 14 | 15 | if (!Array.isArray(name)) { 16 | name = [name]; 17 | multi = false; 18 | } 19 | 20 | return async function authenticate(context: any, next?: Function) { 21 | interface failureObj { 22 | challenge: any; 23 | status?: number; 24 | } 25 | const failures: Array = []; 26 | 27 | async function allFailed() { 28 | if (callback) { 29 | if (!multi) { 30 | return callback( 31 | null, 32 | false, 33 | failures[0].challenge, 34 | failures[0].status 35 | ); 36 | } else { 37 | const challenges = failures.map((failure) => failure.challenge); 38 | const statuses = failures.map((failure) => failure.status); 39 | return callback(null, false, challenges, statuses); 40 | } 41 | } 42 | 43 | let msg: string; 44 | const failure: failureObj = failures[0] || {}; 45 | 46 | let challenge: any = failure.challenge || {}; 47 | if (options.failureMessage) { 48 | if (typeof options.failureMessage === 'boolean') { 49 | msg = challenge.message || challenge; 50 | } else msg = options.failureMessage; 51 | if (!context.state.onyx.session.message) { 52 | context.state.onyx.session.message = []; 53 | } 54 | context.state.onyx.session.message.push(msg); 55 | } 56 | 57 | if (options.failureRedirect) { 58 | context.response.redirect(options.failureRedirect); 59 | } 60 | 61 | const rchallenge: Array = []; 62 | let rstatus: undefined | number; 63 | 64 | failures.forEach((failure) => { 65 | const challenge = failure.challenge; 66 | const status = failure.status; 67 | 68 | rstatus = rstatus || status; 69 | 70 | if (typeof challenge === 'string') { 71 | rchallenge.push(challenge); 72 | } 73 | }); 74 | context.response.status = rstatus || 401; 75 | if (context.response.status === 401 && rchallenge.length) { 76 | context.response.headers.set('WWW-Authenticate', rchallenge); 77 | } 78 | } 79 | 80 | await (async function attempt(i) { 81 | const layer = name[i]; 82 | if (!layer) return allFailed(); 83 | 84 | const prototype = onyx._strategies[layer]; 85 | 86 | if (!prototype) { 87 | return context.throw( 88 | new Error(`Unknown authentication strategy ${layer}`) 89 | ); 90 | } 91 | 92 | const strategy = Object.create(prototype); 93 | 94 | strategy.funcs.success = async function (user: object, info?: any) { 95 | if (callback) return callback(null, user, info); 96 | 97 | info = info || {}; 98 | let msg; 99 | 100 | if (options?.successMessage) { 101 | if (typeof options.successMessage === 'boolean') { 102 | msg = info.message || info; 103 | } else msg = options.successMessage; 104 | if (typeof msg === 'string') { 105 | context.state.onyx.session.message = 106 | context.state.onyx.session.message || []; 107 | context.state.onyx.session.message.push(msg); 108 | } 109 | } 110 | 111 | await context.state.logIn(context, user, async function (err: any) { 112 | if (err) { 113 | throw new Error(err); 114 | } 115 | 116 | async function complete() { 117 | if (options.successRedirect) { 118 | return context.response.redirect(options.successRedirect); 119 | } 120 | next && (await next()); 121 | } 122 | 123 | await complete(); 124 | }); 125 | }; 126 | 127 | strategy.funcs.fail = async function (challenge: any, status?: number) { 128 | if (typeof challenge === 'number') { 129 | status = challenge; 130 | challenge = undefined; 131 | } 132 | failures.push({ challenge, status }); 133 | attempt(i + 1); 134 | }; 135 | 136 | // for anonymous 137 | strategy.funcs.pass = async function () { 138 | next && (await next()); 139 | }; 140 | 141 | strategy.funcs.error = async function (err: any) { 142 | if (callback) { 143 | return callback(err); 144 | } 145 | next && (await next()); 146 | }; 147 | 148 | await strategy.authenticate(context, options); 149 | })(0); 150 | }; 151 | } 152 | -------------------------------------------------------------------------------- /src/onyx.ts: -------------------------------------------------------------------------------- 1 | import SessionManager from './sessionManager.ts'; 2 | import Strategy from './strategy.ts'; 3 | import authenticate from './middleware/authenticate.ts'; 4 | 5 | export default class Onyx { 6 | private _sm: any; 7 | private _strategies: any; 8 | private _framework: { authenticate: Function }; 9 | public funcs: any; 10 | 11 | constructor() { 12 | this._strategies = {}; 13 | this.funcs = {}; 14 | this._framework = { authenticate }; 15 | this.init(); 16 | } 17 | 18 | init() { 19 | this._sm = new SessionManager(this.serializeUser.bind(this)); 20 | } 21 | 22 | // gives developer an option to customize their strategy name 23 | use(name: string | Strategy, strategy?: Strategy) { 24 | if (typeof name !== 'string') { 25 | strategy = name; 26 | name = strategy.name; 27 | } else { 28 | if (!strategy) throw new Error('Strategy needs to be provided!'); 29 | } 30 | if (!name || typeof name !== 'string') { 31 | throw new Error('Authentication strategies must have a name!'); 32 | } 33 | this._strategies[name] = strategy; 34 | return this; 35 | } 36 | 37 | // Allows the developer to remove added strategies - not necessary in normal situations 38 | unuse(name: string) { 39 | delete this._strategies[name]; 40 | return this; 41 | } 42 | 43 | authenticate( 44 | strategy: string, 45 | options?: { 46 | successRedirect?: string; 47 | failureRedirect?: string; 48 | successMessage?: string; 49 | failureMessage?: string; 50 | }, 51 | callback?: Function 52 | ) { 53 | return this._framework.authenticate(this, strategy, options, callback); 54 | } 55 | 56 | serializeUser(fn?: Function) { 57 | if (typeof fn === 'function') { 58 | return (this.funcs.serializer = fn); 59 | } 60 | if (!this.funcs.serializer) { 61 | throw new Error('Serialize Function not registered!'); 62 | } 63 | return this.funcs.serializer; 64 | } 65 | 66 | deserializeUser(fn?: Function) { 67 | if (typeof fn === 'function') { 68 | return (this.funcs.deserializer = fn); 69 | } 70 | if (!this.funcs.deserializer) { 71 | throw new Error('Deserialize Function not registered!'); 72 | } 73 | return this.funcs.deserializer; 74 | } 75 | 76 | initialize() { 77 | return async (context: any, next: Function) => { 78 | if (!context.state) { 79 | throw new Error('Please use onyx.initialize in app.use()'); 80 | } 81 | 82 | context.state.onyx = new Onyx(); 83 | 84 | // Check if Session has been set up for the server 85 | if (context.state.session === undefined) { 86 | throw new Error('Must set up Session before Onyx'); 87 | } 88 | 89 | // LogIn - invoke after successful registration 90 | context.state.logIn = context.state.login = this._sm.logIn; 91 | 92 | // LogOut - invoke to log out user 93 | context.state.logOut = context.state.logout = this._sm.logOut; 94 | 95 | // isAuthenticated returns true if user is Authenticated 96 | context.state.isAuthenticated = function () { 97 | if (context.state.onyx.session !== undefined) return true; 98 | else return false; 99 | }; 100 | 101 | // isUnauthenticated returns true if user is Not Authenticated 102 | context.state.isUnauthenticated = function () { 103 | return !context.state.isAuthenticated(); 104 | }; 105 | 106 | // getUser returns the user info from User Database if user is Authenticated, if not it will return undefined 107 | // this is different from Passport as any info we store on context.state will persist (passport uses req[this.userProperty]) 108 | context.state.getUser = function () { 109 | if (!context.state.onyx.session) return; 110 | return context.state.onyx.session.user; 111 | }; 112 | 113 | const userIDVal = await context.state.session.get('userIDKey'); 114 | 115 | if (userIDVal) { 116 | if (!context.state.onyx.session) context.state.onyx.session = {}; 117 | context.state.onyx.session.userID = userIDVal; 118 | 119 | await this.funcs.deserializer(userIDVal, async function ( 120 | err: any, 121 | user: any 122 | ) { 123 | if (err) throw new Error(err); 124 | else if (!user) { 125 | delete context.state.onyx.session; 126 | 127 | const sidCookie = await context.cookies.get('sid'); 128 | if (context.state.session._session._store._sessionRedisStore) { 129 | await context.state.session._session._store._sessionRedisStore.del( 130 | sidCookie 131 | ); 132 | } else 133 | context.state.session._session._store.deleteSession(sidCookie); 134 | } else { 135 | if (!context.state.onyx.session) context.state.onyx.session = {}; 136 | context.state.onyx.session.user = user; 137 | } 138 | }); 139 | } 140 | await next(); 141 | }; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/sessionManager.ts: -------------------------------------------------------------------------------- 1 | export default class SessionManager { 2 | private _serializeUser: Function; 3 | 4 | constructor(serializeUser: Function) { 5 | this._serializeUser = serializeUser; 6 | } 7 | 8 | logIn = async (context: any, user: any, cb: Function) => { 9 | const serializer = await this._serializeUser(); 10 | 11 | await serializer(user, async (err: any, id: any) => { 12 | if (err) { 13 | return cb(err); 14 | } 15 | if (!context.state.onyx.session) { 16 | context.state.onyx.session = {}; 17 | } 18 | 19 | context.state.onyx.session.user = user; 20 | context.state.onyx.session.userID = id; 21 | 22 | await context.state.session.set('userIDKey', id); 23 | 24 | const userIDVal = await context.state.session.get('userIDKey'); 25 | 26 | await cb(); 27 | }); 28 | }; 29 | 30 | logOut = async (context: any, cb?: Function) => { 31 | if (context.state.onyx && context.state.onyx.session) { 32 | delete context.state.onyx.session.userID; 33 | 34 | const sidCookie = await context.cookies.get('sid'); 35 | 36 | // if using Redis Memory for Session Store 37 | if (context.state.session._session._store._sessionRedisStore) { 38 | await context.state.session._session._store._sessionRedisStore.del( 39 | sidCookie 40 | ); 41 | } 42 | // if using Server Memory for Session Store 43 | else context.state.session._session._store.deleteSession(sidCookie); 44 | } 45 | cb && cb(); 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/strategies/local-strategy/local-strategy.ts: -------------------------------------------------------------------------------- 1 | import Strategy from '../../strategy.ts'; 2 | 3 | /* Create local strategy 4 | * 5 | * options provides the field names for where the username and password are found, defaults to 'username' & 'password' 6 | */ 7 | export default class LocalStrategy extends Strategy { 8 | private _usernameField: string; 9 | private _passwordField: string; 10 | private _verify: Function; 11 | public funcs: any; 12 | 13 | constructor( 14 | verifyCB: Function, 15 | options?: { usernameField?: string; passwordField?: string } 16 | ) { 17 | super(); 18 | this._usernameField = options?.usernameField || 'username'; 19 | this._passwordField = options?.passwordField || 'password'; 20 | this.name = 'local'; 21 | this._verify = verifyCB; 22 | this.funcs = {}; 23 | } 24 | 25 | authenticate = async (context: any, options?: any) => { 26 | options = options || {}; 27 | 28 | const body = await context.request.body().value; 29 | 30 | const username: string = body[this._usernameField]; 31 | const password: string = body[this._passwordField]; 32 | 33 | if (!username || !password) { 34 | context.state.onyx.isFailure = true; 35 | return this.funcs.fail( 36 | { message: options.badRequestMessage || 'Missing Credentials' }, 37 | 400 38 | ); 39 | } 40 | 41 | context.state.onyx.user = { username, password }; 42 | const self = this; 43 | 44 | try { 45 | await this._verify(username, password, verified); 46 | } catch (err) { 47 | return self.funcs.error(err); 48 | } 49 | 50 | async function verified(err: any, user?: any, info?: any) { 51 | if (err) { 52 | return self.funcs.error(err); 53 | } 54 | if (!user) { 55 | return self.funcs.fail(info); 56 | } 57 | await self.funcs.success(user, info); 58 | } 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /src/strategy.ts: -------------------------------------------------------------------------------- 1 | // Creates an instance of a Strategy 2 | 3 | class Strategy { 4 | public name: any; 5 | 6 | constructor() {} 7 | authenticate(context: any, options?: any) { 8 | throw new Error('Strategy must be overwritten by a subclass strategy'); 9 | } 10 | } 11 | 12 | export default Strategy; 13 | -------------------------------------------------------------------------------- /src/tests/local-strategy.test.ts: -------------------------------------------------------------------------------- 1 | import LocalStrategy from './../strategies/local-strategy/local-strategy.ts'; 2 | import { describe, it, expect } from './../../test_deps.ts'; 3 | 4 | describe('Local Strategy', () => { 5 | const verifyFunc = () => { 6 | 'verifyFunc'; 7 | }; 8 | const strategy = new LocalStrategy(verifyFunc); 9 | 10 | it('local-strategy should be named local', function () { 11 | expect(strategy.name).toEqual('local'); 12 | }); 13 | 14 | it('local-strategy should store the provided function in this._verify', function () { 15 | expect(strategy['_verify']).toBe(verifyFunc); 16 | }); 17 | 18 | it('local-strategy - usernameField should default to "username"', function () { 19 | expect(strategy['_usernameField']).toEqual('username'); 20 | }); 21 | 22 | it('local-strategy - passwordField should default to "password"', function () { 23 | expect(strategy['_passwordField']).toEqual('password'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/tests/onyx.test.ts: -------------------------------------------------------------------------------- 1 | import Onyx from './../onyx.ts'; 2 | import Strategy from './../strategy.ts'; 3 | import { describe, it, expect, assertThrowsAsync } from './../../test_deps.ts'; 4 | 5 | describe('Onyx', () => { 6 | describe('#use', () => { 7 | describe('with instance name', () => { 8 | class testStrategy extends Strategy { 9 | public name: string; 10 | 11 | constructor() { 12 | super(); 13 | this.name = 'default'; 14 | } 15 | } 16 | const onyx = new Onyx(); 17 | onyx.use(new testStrategy()); 18 | 19 | it('Onyx #use should register strategy', async (done: any) => { 20 | expect(typeof onyx['_strategies']['default']).toEqual('object'); 21 | 22 | done(); 23 | }); 24 | 25 | it('Onyx #use should throw an error if Strategy is not provided in onyx.use', async (done: any) => { 26 | assertThrowsAsync( 27 | (): Promise => { 28 | return new Promise((): void => { 29 | onyx.use('default'); 30 | }); 31 | }, 32 | Error, 33 | 'Strategy needs to be provided!' 34 | ); 35 | done(); 36 | }); 37 | 38 | it('Onyx #use should throw an error if input Strategy does not have name', async (done: any) => { 39 | class namelessStrategy extends Strategy { 40 | constructor() { 41 | super(); 42 | } 43 | } 44 | 45 | assertThrowsAsync( 46 | (): Promise => { 47 | return new Promise((): void => { 48 | onyx.use(new namelessStrategy()); 49 | }); 50 | }, 51 | Error, 52 | 'Authentication strategies must have a name!' 53 | ); 54 | done(); 55 | }); 56 | 57 | it('Onyx #use should register strategy with custom name', async (done: any) => { 58 | const stratInstance: any = new testStrategy(); 59 | const stratInstance2: any = new testStrategy(); 60 | onyx.use('new name', stratInstance); 61 | expect(typeof onyx['_strategies']['new name']).toEqual('object'); 62 | done(); 63 | }); 64 | }); 65 | }); 66 | 67 | describe('#serializeUser', () => { 68 | describe('missing setup', () => { 69 | const onyx = new Onyx(); 70 | it('Onyx #serializeUser - missing setup - should throw an Error if no serialize function was registered', (done) => { 71 | assertThrowsAsync( 72 | (): Promise => { 73 | return new Promise((): void => { 74 | onyx.serializeUser(); 75 | }); 76 | }, 77 | Error, 78 | 'Serialize Function not registered!' 79 | ); 80 | done(); 81 | }); 82 | }); 83 | 84 | describe('with setup', () => { 85 | const onyx = new Onyx(); 86 | 87 | const serializer = async function (user: any, cb: Function) { 88 | await cb(null, user.id); 89 | }; 90 | onyx.serializeUser(serializer); 91 | 92 | it('Onyx #serializeUser - should should store the passed in function', async (done) => { 93 | expect(typeof onyx.funcs.serializer).toEqual('function'); 94 | expect(onyx.funcs.serializer).toBe(serializer); 95 | done(); 96 | }); 97 | 98 | it('Onyx #serializeUser - should returned the stored function when invoked without an argument', async (done) => { 99 | expect(typeof onyx.serializeUser()).toEqual('function'); 100 | expect(onyx.serializeUser()).toBe(serializer); 101 | done(); 102 | }); 103 | }); 104 | }); 105 | 106 | describe('#deserializeUser', () => { 107 | describe('missing setup', () => { 108 | const onyx = new Onyx(); 109 | it('Onyx #deserializeUser - missing setup - should throw an Error if no deserialize function was registered', (done) => { 110 | assertThrowsAsync( 111 | (): Promise => { 112 | return new Promise((): void => { 113 | onyx.deserializeUser(); 114 | }); 115 | }, 116 | Error, 117 | 'Deserialize Function not registered!' 118 | ); 119 | done(); 120 | }); 121 | }); 122 | 123 | describe('with setup', () => { 124 | const onyx = new Onyx(); 125 | 126 | const serializer = async function (user: any, cb: Function) { 127 | await cb(null, user.id); 128 | }; 129 | 130 | const deserializer = async function (id: string, cb: Function) { 131 | const _id = { $oid: id }; 132 | try { 133 | const user = { username: 'Alice', _id }; 134 | await cb(null, user); 135 | } catch (error) { 136 | await cb(error, null); 137 | } 138 | }; 139 | onyx.deserializeUser(deserializer); 140 | 141 | it('Onyx #deserializeUser - should should store the passed in function', async (done) => { 142 | expect(typeof onyx.funcs.deserializer).toEqual('function'); 143 | expect(onyx.funcs.deserializer).toBe(deserializer); 144 | done(); 145 | }); 146 | 147 | it('Onyx #deserializeUser - should returned the stored function when invoked without an argument', async (done) => { 148 | expect(typeof onyx.deserializeUser()).toEqual('function'); 149 | expect(onyx.deserializeUser()).toBe(deserializer); 150 | done(); 151 | }); 152 | }); 153 | }); 154 | 155 | describe('#initialize', () => { 156 | class Context { 157 | public state: any; 158 | constructor() { 159 | this.state = {}; 160 | } 161 | } 162 | 163 | const ctx = new Context(); 164 | const onyx = new Onyx(); 165 | 166 | it('Onyx #initialize should create a new instance of Onyx', async (done) => { 167 | ctx.state.session = { 168 | get: (str: string) => undefined, 169 | }; 170 | onyx.initialize()(ctx, () => {}); 171 | expect(ctx.state.onyx).toBeInstanceOf(Onyx); 172 | done(); 173 | }); 174 | 175 | it('Onyx #initialize should add logIn, logOut, isAuthenticated, isUnauthenticated, getUser functions to context.state', async (done) => { 176 | ctx.state.session = { 177 | get: (str: string) => undefined, 178 | }; 179 | onyx.initialize()(ctx, () => {}); 180 | expect(typeof ctx.state.logIn).toEqual('function'); 181 | expect(typeof ctx.state.logOut).toEqual('function'); 182 | expect(typeof ctx.state.isAuthenticated).toEqual('function'); 183 | expect(typeof ctx.state.isUnauthenticated).toEqual('function'); 184 | expect(typeof ctx.state.getUser).toEqual('function'); 185 | done(); 186 | }); 187 | }); 188 | 189 | describe('#unuse', () => { 190 | class testStrategy extends Strategy {} 191 | const onyx = new Onyx(); 192 | onyx.use('one', new testStrategy()); 193 | onyx.use('two', new testStrategy()); 194 | 195 | expect(typeof onyx['_strategies']['one']).toEqual('object'); 196 | expect(typeof onyx['_strategies']['two']).toEqual('object'); 197 | 198 | onyx.unuse('one'); 199 | 200 | it('should unregister strategy', async (done: any) => { 201 | expect(onyx['_strategies']['one']).toBeUndefined(); 202 | expect(typeof onyx['_strategies']['two']).toEqual('object'); 203 | done(); 204 | }); 205 | }); 206 | 207 | describe('#authenticate', () => { 208 | class testStrategy extends Strategy {} 209 | const onyx = new Onyx(); 210 | onyx.use('one', new testStrategy()); 211 | 212 | it('Onyx #authenticate should return a function', async (done: any) => { 213 | expect(typeof onyx.authenticate('one')).toEqual('function'); 214 | done(); 215 | }); 216 | }); 217 | }); 218 | -------------------------------------------------------------------------------- /test_deps.ts: -------------------------------------------------------------------------------- 1 | // Deno Tests 2 | export { 3 | assertEquals, 4 | assertNotEquals, 5 | assertObjectMatch, 6 | assertThrowsAsync, 7 | assertThrows, 8 | } from 'https://deno.land/std@0.77.0/testing/asserts.ts'; 9 | 10 | // SuperOak 11 | export { superoak } from 'https://deno.land/x/superoak@2.3.1/mod.ts'; 12 | export { describe, it } from 'https://deno.land/x/superoak@2.3.1/test/utils.ts'; 13 | export { expect } from 'https://deno.land/x/superoak@2.3.1/test/deps.ts'; 14 | --------------------------------------------------------------------------------