feat: init 🎉

This commit is contained in:
Julian Lam 2023-07-26 15:47:31 -04:00
commit d0753a5ff3
17 changed files with 2696 additions and 0 deletions

3
.eslintrc Normal file
View file

@ -0,0 +1,3 @@
{
"extends": "nodebb"
}

22
.gitattributes vendored Normal file
View file

@ -0,0 +1,22 @@
# Auto detect text files and perform LF normalization
* text=auto

# Custom for Visual Studio
*.cs diff=csharp
*.sln merge=union
*.csproj merge=union
*.vbproj merge=union
*.fsproj merge=union
*.dbproj merge=union

# Standard to msysgit
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain

224
.gitignore vendored Normal file
View file

@ -0,0 +1,224 @@
#################
## Eclipse
#################

*.pydevproject
.project
.metadata
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.classpath
.settings/
.loadpath

# External tool builders
.externalToolBuilders/

# Locally stored "Eclipse launch configurations"
*.launch

# CDT-specific
.cproject

# PDT-specific
.buildpath


#################
## Visual Studio
#################

## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.

# User-specific files
*.suo
*.user
*.sln.docstates

# Build results

[Dd]ebug/
[Rr]elease/
x64/
build/
[Bb]in/
[Oo]bj/

# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*

*_i.c
*_p.c
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.log
*.scc

# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
*.cachefile

# Visual Studio profiler
*.psess
*.vsp
*.vspx

# Guidance Automation Toolkit
*.gpState

# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper

# TeamCity is a build add-in
_TeamCity*

# DotCover is a Code Coverage Tool
*.dotCover

# NCrunch
*.ncrunch*
.*crunch*.local.xml

# Installshield output folder
[Ee]xpress/

# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html

# Click-Once directory
publish/

# Publish Web Output
*.Publish.xml
*.pubxml

# NuGet Packages Directory
## TODO: If you have NuGet Package Restore enabled, uncomment the next line
#packages/

# Windows Azure Build Output
csx
*.build.csdef

# Windows Store app package directory
AppPackages/

# Others
sql/
*.Cache
ClientBin/
[Ss]tyle[Cc]op.*
~$*
*~
*.dbmdl
*.[Pp]ublish.xml
*.pfx
*.publishsettings

# RIA/Silverlight projects
Generated_Code/

# Backup & report files from converting an old project file to a newer
# Visual Studio version. Backup files are not needed, because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm

# SQL Server files
App_Data/*.mdf
App_Data/*.ldf

#############
## Windows detritus
#############

# Windows image file caches
Thumbs.db
ehthumbs.db

# Folder config file
Desktop.ini

# Recycle Bin used on file shares
$RECYCLE.BIN/

# Mac crap
.DS_Store


#############
## Python
#############

*.py[co]

# Packages
*.egg
*.egg-info
dist/
build/
eggs/
parts/
var/
sdist/
develop-eggs/
.installed.cfg

# Installer logs
pip-log.txt

# Unit test / coverage reports
.coverage
.tox

#Translations
*.mo

#Mr Developer
.mr.developer.cfg

sftp-config.json
node_modules/

####################
# JetBrains
####################

.idea

2
.npmignore Normal file
View file

@ -0,0 +1,2 @@
sftp-config.json
node_modules/

8
LICENSE Normal file
View file

@ -0,0 +1,8 @@
Copyright (c) 2013-2014, psychobunny <psycho.bunny@hotmail.com>
All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

3
README.md Normal file
View file

@ -0,0 +1,3 @@
# NodeBB OAuth2 Multiple Client SSO

This NodeBB plugin allows you to configure logins to multiple configurable OAuth2 endpoints, via the admin backend.

3
commitlint.config.js Normal file
View file

@ -0,0 +1,3 @@
'use strict';

module.exports = { extends: ['@commitlint/config-angular'] };

59
lib/controllers.js Normal file
View file

@ -0,0 +1,59 @@
'use strict';

const db = require.main.require('./src/database');
const slugify = require.main.require('./src/slugify');
const helpers = require.main.require('./src/controllers/helpers');

const main = require('../library');

const Controllers = module.exports;

Controllers.renderAdminPage = async (req, res) => {
const strategies = await main.listStrategies();
res.render('admin/plugins/sso-oauth2-multiple', {
title: 'Multiple OAuth2',
strategies,
});
};

Controllers.getStrategy = async (req, res) => {
const name = slugify(req.params.name);

const strategy = await db.getObject(`oauth2-multiple:strategies:${name}`);
strategy.name = name;

helpers.formatApiResponse(200, res, { strategy });
};

Controllers.editStrategy = async (req, res) => {
const name = slugify(req.params.name || req.body.name);
const payload = { ...req.body };
delete payload.name;

const valuesOk = ['authUrl', 'tokenUrl', 'id', 'secret'].every(prop => payload.hasOwnProperty(prop) && payload[prop]);
if (!name || !valuesOk) {
throw new Error('[[error:invalid-data]]');
}

payload.enabled = !!req.body.enabled;

await Promise.all([
db.sortedSetAdd('oauth2-multiple:strategies', Date.now(), name),
db.setObject(`oauth2-multiple:strategies:${name}`, payload),
]);

const strategies = await main.listStrategies();
helpers.formatApiResponse(200, res, { strategies });
};

Controllers.deleteStrategy = async (req, res) => {
const name = slugify(req.params.name);

await Promise.all([
db.sortedSetRemove('oauth2-multiple:strategies', name),
db.delete(`oauth2-multiple:strategies:${name}`),
]);

const strategies = await main.listStrategies();
helpers.formatApiResponse(200, res, { strategies });
};

303
library.js Normal file
View file

@ -0,0 +1,303 @@
'use strict';

/*
Welcome to the SSO OAuth plugin! If you're inspecting this code, you're probably looking to
hook up NodeBB with your existing OAuth endpoint.

Step 1: Fill in the "constants" section below with the requisite informaton. Either the "oauth"
or "oauth2" section needs to be filled, depending on what you set "type" to.

Step 2: Give it a whirl. If you see the congrats message, you're doing well so far!

Step 3: Customise the `parseUserReturn` method to normalise your user route's data return into
a format accepted by NodeBB. Instructions are provided there. (Line 146)

Step 4: If all goes well, you'll be able to login/register via your OAuth endpoint credentials.
*/

const User = require.main.require('./src/user');
const Groups = require.main.require('./src/groups');
const db = require.main.require('./src/database');
const authenticationController = require.main.require('./src/controllers/authentication');
const routeHelpers = require.main.require('./src/routes/helpers');

const async = require('async');

const passport = module.parent.require('passport');
const nconf = module.parent.require('nconf');
const winston = module.parent.require('winston');

/**
* REMEMBER
* Never save your OAuth Key/Secret or OAuth2 ID/Secret pair in code! It could be published and leaked accidentally.
* Save it into your config.json file instead:
*
* {
* ...
* "oauth": {
* "id": "someoauthid",
* "secret": "youroauthsecret"
* }
* ...
* }
*
* ... or use environment variables instead:
*
* `OAUTH__ID=someoauthid OAUTH__SECRET=youroauthsecret node app.js`
*/

const constants = Object.freeze({
type: '', // Either 'oauth' or 'oauth2'
name: '', // Something unique to your OAuth provider in lowercase, like "github", or "nodebb"
oauth: {
requestTokenURL: '',
accessTokenURL: '',
userAuthorizationURL: '',
consumerKey: nconf.get('oauth:key'), // don't change this line
consumerSecret: nconf.get('oauth:secret'), // don't change this line
},
oauth2: {
authorizationURL: '',
tokenURL: '',
clientID: nconf.get('oauth:id'), // don't change this line
clientSecret: nconf.get('oauth:secret'), // don't change this line
},
userRoute: '', // This is the address to your app's "user profile" API endpoint (expects JSON)
});

const OAuth = module.exports;
let configOk = false;
let passportOAuth;
let opts;

if (!constants.name) {
winston.error('[sso-oauth] Please specify a name for your OAuth provider (library.js:32)');
} else if (!constants.type || (constants.type !== 'oauth' && constants.type !== 'oauth2')) {
winston.error('[sso-oauth] Please specify an OAuth strategy to utilise (library.js:31)');
} else if (!constants.userRoute) {
winston.error('[sso-oauth] User Route required (library.js:31)');
} else {
configOk = true;
}

OAuth.init = async (params) => {
const { router /* , middleware , controllers */ } = params;
const controllers = require('./lib/controllers');

routeHelpers.setupAdminPageRoute(router, '/admin/plugins/sso-oauth2-multiple', controllers.renderAdminPage);
};

OAuth.addRoutes = async ({ router, middleware }) => {
const controllers = require('./lib/controllers');
const middlewares = [
middleware.ensureLoggedIn,
middleware.admin.checkPrivileges,
];

routeHelpers.setupApiRoute(router, 'post', '/oauth2-multiple/strategies', middlewares, controllers.editStrategy);
routeHelpers.setupApiRoute(router, 'get', '/oauth2-multiple/strategies/:name', middlewares, controllers.getStrategy);
routeHelpers.setupApiRoute(router, 'delete', '/oauth2-multiple/strategies/:name', middlewares, controllers.deleteStrategy);
};

OAuth.addAdminNavigation = (header) => {
header.authentication.push({
route: '/plugins/sso-oauth2-multiple',
icon: 'fa-tint',
name: 'Multiple OAuth2',
});

return header;
};

OAuth.listStrategies = async () => {
const names = await db.getSortedSetMembers('oauth2-multiple:strategies');
const strategies = await db.getObjects(names.map(name => `oauth2-multiple:strategies:${name}`), ['enabled']);
strategies.forEach((strategy, idx) => {
strategy.name = names[idx];
strategy.enabled = strategy.enabled === 'true';
});

return strategies;
};

OAuth.getStrategy = function (strategies, callback) {
if (configOk) {
passportOAuth = require('passport-oauth')[constants.type === 'oauth' ? 'OAuthStrategy' : 'OAuth2Strategy'];

if (constants.type === 'oauth') {
// OAuth options
opts = constants.oauth;
opts.callbackURL = `${nconf.get('url')}/auth/${constants.name}/callback`;

passportOAuth.Strategy.prototype.userProfile = function (token, secret, params, done) {
// If your OAuth provider requires the access token to be sent in the query parameters
// instead of the request headers, comment out the next line:
this._oauth._useAuthorizationHeaderForGET = true;

this._oauth.get(constants.userRoute, token, secret, (err, body/* , res */) => {
if (err) {
return done(err);
}

try {
const json = JSON.parse(body);
OAuth.parseUserReturn(json, (err, profile) => {
if (err) return done(err);
profile.provider = constants.name;

done(null, profile);
});
} catch (e) {
done(e);
}
});
};
} else if (constants.type === 'oauth2') {
// OAuth 2 options
opts = constants.oauth2;
opts.callbackURL = `${nconf.get('url')}/auth/${constants.name}/callback`;

passportOAuth.Strategy.prototype.userProfile = function (accessToken, done) {
// If your OAuth provider requires the access token to be sent in the query parameters
// instead of the request headers, comment out the next line:
this._oauth2._useAuthorizationHeaderForGET = true;

this._oauth2.get(constants.userRoute, accessToken, (err, body/* , res */) => {
if (err) {
return done(err);
}

try {
const json = JSON.parse(body);
OAuth.parseUserReturn(json, (err, profile) => {
if (err) return done(err);
profile.provider = constants.name;

done(null, profile);
});
} catch (e) {
done(e);
}
});
};
}

opts.passReqToCallback = true;

passport.use(constants.name, new passportOAuth(opts, async (req, token, secret, profile, done) => {
const user = await OAuth.login({
oAuthid: profile.id,
handle: profile.displayName,
email: profile.emails[0].value,
isAdmin: profile.isAdmin,
});

authenticationController.onSuccessfulLogin(req, user.uid);
done(null, user);
}));

strategies.push({
name: constants.name,
url: `/auth/${constants.name}`,
callbackURL: `/auth/${constants.name}/callback`,
icon: 'fa-check-square',
scope: (constants.scope || '').split(','),
});

callback(null, strategies);
} else {
callback(new Error('OAuth Configuration is invalid'));
}
};

OAuth.parseUserReturn = function (data, callback) {
// Alter this section to include whatever data is necessary
// NodeBB *requires* the following: id, displayName, emails.
// Everything else is optional.

// Find out what is available by uncommenting this line:
// console.log(data);

const profile = {};
profile.id = data.id;
profile.displayName = data.name;
profile.emails = [{ value: data.email }];

// Do you want to automatically make somebody an admin? This line might help you do that...
// profile.isAdmin = data.isAdmin ? true : false;

// Delete or comment out the next TWO (2) lines when you are ready to proceed
process.stdout.write('===\nAt this point, you\'ll need to customise the above section to id, displayName, and emails into the "profile" object.\n===');
return callback(new Error('Congrats! So far so good -- please see server log for details'));

// eslint-disable-next-line
callback(null, profile);
};

OAuth.login = async (payload) => {
let uid = await OAuth.getUidByOAuthid(payload.oAuthid);
if (uid !== null) {
// Existing User
return ({
uid: uid,
});
}

// Check for user via email fallback
uid = await User.getUidByEmail(payload.email);
if (!uid) {
/**
* The email retrieved from the user profile might not be trusted.
* Only you would know — it's up to you to decide whether or not to:
* - Send the welcome email which prompts for verification (default)
* - Bypass the welcome email and automatically verify the email (commented out, below)
*/
const { email } = payload;

// New user
uid = await User.create({
username: payload.handle,
email, // if you uncomment the block below, comment this line out
});

// Automatically confirm user email
// await User.setUserField(uid, 'email', email);
// await UserEmail.confirmByUid(uid);
}

// Save provider-specific information to the user
await User.setUserField(uid, `${constants.name}Id`, payload.oAuthid);
await db.setObjectField(`${constants.name}Id:uid`, payload.oAuthid, uid);

if (payload.isAdmin) {
await Groups.join('administrators', uid);
}

return {
uid: uid,
};
};

OAuth.getUidByOAuthid = async oAuthid => db.getObjectField(`${constants.name}Id:uid`, oAuthid);

OAuth.deleteUserData = function (data, callback) {
async.waterfall([
async.apply(User.getUserField, data.uid, `${constants.name}Id`),
function (oAuthIdToDelete, next) {
db.deleteObjectField(`${constants.name}Id:uid`, oAuthIdToDelete, next);
},
], (err) => {
if (err) {
winston.error(`[sso-oauth] Could not remove OAuthId data for uid ${data.uid}. Error: ${err}`);
return callback(err);
}

callback(null, data);
});
};

// If this filter is not there, the deleteUserData function will fail when getting the oauthId for deletion.
OAuth.whitelistFields = function (params, callback) {
params.whitelist.push(`${constants.name}Id`);
callback(null, params);
};

59
package.json Normal file
View file

@ -0,0 +1,59 @@
{
"name": "nodebb-plugin-sso-oauth2-multiple",
"version": "0.5.0",
"description": "NodeBB Multiple OAuth2 SSO",
"main": "library.js",
"repository": {
"type": "git",
"url": "https://github.com/julianlam/nodebb-plugin-sso-oauth"
},
"keywords": [
"nodebb",
"plugin",
"oauth",
"oauth2",
"sso",
"single sign on",
"login",
"registration"
],
"author": {
"name": "Julian Lam",
"email": "julian@nodebb.org"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/julianlam/nodebb-plugin-sso-oauth/issues"
},
"readme": "",
"readmeFilename": "README.md",
"dependencies": {
"async": "^3.2.0",
"eslint": "8.x",
"passport-oauth": "~1.0.0"
},
"nbbpm": {
"compatibility": "^1.0.1",
"index": false
},
"devDependencies": {
"@commitlint/config-angular": "15.0.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-nodebb": "0.2.1",
"eslint-plugin-import": "2.x",
"husky": "8.0.3",
"lint-staged": "13.2.3"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"*.js": [
"eslint --fix",
"git add"
]
}
}

19
plugin.json Normal file
View file

@ -0,0 +1,19 @@
{
"id": "nodebb-plugin-sso-oauth2-multiple",
"name": "NodeBB Multiple OAuth2 SSO",
"description": "NodeBB Plugin that configures multiple OAuth2 login endpoints",
"url": "https://github.com/nodebb/nodebb-plugin-sso-oauth2-multiple",
"library": "./library.js",
"hooks": [
{ "hook": "static:app.load", "method": "init" },
{ "hook": "static:api.routes", "method": "addRoutes" },
{ "hook": "filter:admin.header.build", "method": "addAdminNavigation" },
{ "hook": "static:user.delete", "method": "deleteUserData" },
{ "hook": "filter:user.whitelistFields", "method": "whitelistFields" },
{ "hook": "filter:auth.init", "method": "getStrategy" }
],
"modules": {
"../admin/plugins/sso-oauth2-multiple.js": "./static/lib/admin.js"
},
"templates": "static/templates"
}

5
renovate.json Normal file
View file

@ -0,0 +1,5 @@
{
"extends": [
"config:base"
]
}

3
static/.eslintrc Normal file
View file

@ -0,0 +1,3 @@
{
"extends": "nodebb/public"
}

104
static/lib/admin.js Normal file
View file

@ -0,0 +1,104 @@
'use strict';

// import * as settings from 'settings';
import { confirm } from 'bootbox';
import { get, post, del } from 'api';
import { error } from 'alerts';
import { render } from 'benchpress';

// eslint-disable-next-line import/prefer-default-export
export function init() {
// settings.load('sso-oauth2-multiple', $('.sso-oauth2-multiple-settings'));
// $('#save').on('click', saveSettings);

const formEl = document.querySelector('.sso-oauth2-multiple-settings');
formEl.addEventListener('click', async (e) => {
const subselector = e.target.closest('[data-action]');
if (subselector) {
const action = subselector.getAttribute('data-action');

switch (action) {
case 'new': {
const title = 'New OAuth2 Strategy';
const message = await app.parseAndTranslate('partials/edit-oauth2-strategy', {});
confirm({
title,
message,
callback: handleEditStrategy,
});

break;
}

case 'edit': {
const name = subselector.closest('[data-name]').getAttribute('data-name');
const { strategy } = await get(`/plugins/oauth2-multiple/strategies/${name}`);
const title = 'Edit OAuth2 Strategy';
const message = await app.parseAndTranslate('partials/edit-oauth2-strategy', { ...strategy });
confirm({
title,
message,
callback: handleEditStrategy,
});

break;
}

case 'delete': {
const name = subselector.closest('[data-name]').getAttribute('data-name');

if (!name) {
break;
}

confirm({
title: 'Delete Strategy',
message: `Are you sure you wish to delete the OAuth2 strategy named <strong>${name}</strong>?`,
callback: function (ok) {
handleDeleteStrategy.call(this, ok, name);
},
});
}
}
}
});
}

function handleEditStrategy(ok) {
if (!ok) {
return;
}

const $modal = this;
const modalEl = this.get(0);
const formEl = modalEl.querySelector('form');
const data = new FormData(formEl);

post('/plugins/oauth2-multiple/strategies', data).then(async ({ strategies }) => {
const html = await render('admin/plugins/sso-oauth2-multiple', { strategies }, 'strategies');
const tbodyEl = document.querySelector('#strategies tbody');
tbodyEl.innerHTML = html;
$modal.modal('hide');
}).catch(error);

return false;
}

function handleDeleteStrategy(ok, name) {
if (!ok) {
return;
}

const $modal = this;

del(`/plugins/oauth2-multiple/strategies/${name}`).then(async ({ strategies }) => {
const html = await render('admin/plugins/sso-oauth2-multiple', { strategies }, 'strategies');
const tbodyEl = document.querySelector('#strategies tbody');
tbodyEl.innerHTML = html;
$modal.modal('hide');
}).catch(error);
}

// function saveSettings() {
// settings.save('sso-oauth2-multiple', $('.sso-oauth2-multiple-settings'));
// }

View file

@ -0,0 +1,56 @@
<div class="acp-page-container">
<!-- IMPORT admin/partials/settings/header.tpl -->

<div class="row m-0">
<div id="spy-container" class="col-12 col-md-8 px-0 mb-4" tabindex="0">
<form role="form" class="sso-oauth2-multiple-settings">
<div class="mb-4">
<h5 class="fw-bold tracking-tight settings-header">Connections</h5>

<p class="lead">
The following OAuth2 endpoints have been configured.
</p>

<table class="table small" id="strategies">
<thead>
<th>Name</th>
<th>Enabled</th>
<th><span class="visually-hidden">Actions</span></th>
</thead>
<tbody>
{{{ if !strategies.length }}}
<tr>
<td colspan="3">
<div class="alert alert-info text-center mb-0"><em>No OAuth2 endpoints configured.</em></div>
</td>
</tr>
{{{ end }}}
{{{ each strategies }}}
<tr data-name="{./name}">
<td>{./name}</td>
<td>
{{{ if ./enabled }}}&check;{{{ else }}}&cross;{{{ end }}}
</td>
<td class="text-end">
<a href="#" data-action="edit">Edit</a>
&nbsp;&nbsp;&nbsp;
<a href="#" data-action="delete" class="text-danger">Delete</a>
</td>
</tr>
{{{ end }}}
</tbody>
<tfoot>
<tr>
<td colspan="3">
<button type="button" class="btn btn-success btn-sm pull-right" data-action="new"><i class="fa fa-plus"></i> New Endpoint</button>
</td>
</tr>
</tfoot>
</table>
</div>
</form>
</div>

<!-- IMPORT admin/partials/settings/toc.tpl -->
</div>
</div>

View file

@ -0,0 +1,44 @@
<form role="form">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="enabled" name="enabled" {{{ if (./enabled == "true") }}}checked{{{ end }}}>
<label for="enabled" class="form-check-label">Enabled</label>
</div>

<hr />

<div class="mb-3">
<label class="form-label" for="name">Name</label>
<input type="text" id="name" name="name" title="Name" class="form-control" placeholder="Name" value="{./name}">
<p class="form-text">
Enter something unique to your OAuth provider in lowercase, like <code>github</code>, or <code>nodebb</code>.
</p>
</div>

<div class="mb-3">
<label class="form-label" for="authUrl">Authorization URL</label>
<input type="text" id="authUrl" name="authUrl" title="Authorization URL" class="form-control" placeholder="https://..." value="{./authUrl}">
</div>

<div class="mb-3">
<label class="form-label" for="tokenUrl">Token URL</label>
<input type="text" id="tokenUrl" name="tokenUrl" title="Token URL" class="form-control" placeholder="https://..." value="{./tokenUrl}">
</div>

<div class="mb-3">
<label class="form-label" for="id">Client ID</label>
<input type="text" id="id" name="id" title="Client ID" class="form-control" value="{./id}">
</div>

<div class="mb-3">
<label class="form-label" for="secret">Client Secret</label>
<input type="text" id="secret" name="secret" title="Client Secret" class="form-control" value="{./secret}">
</div>

<div class="mb-3">
<label class="form-label" for="userRoute">User Info URL</label>
<input type="text" id="userRoute" name="userRoute" title="User Info URL" class="form-control" placeholder="/userinfo" value="{./userRoute}">
<p class="form-text">
If a relative path is specified here, we will assume the hostname from the authorization URL.
</p>
</div>
</form>

1779
yarn.lock Normal file

File diff suppressed because it is too large Load diff