diff --git a/.gitignore b/.gitignore index 0798930ccb4dd74f0d3a55ee2452bd7322ef27f1..efdfc92830476b1c1e661937b031245a179b0e87 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ node_modules .env *.iml **/dist/ -public/js/ +public/scripts/ stats.json client-legacy/ .dck/ diff --git a/database/migrations/20220329000001-create-server-tokens-table.js b/database/migrations/20220329000001-create-server-tokens-table.js new file mode 100644 index 0000000000000000000000000000000000000000..43f85d2a3ab4e70715629b911ae40e2eb569daf1 --- /dev/null +++ b/database/migrations/20220329000001-create-server-tokens-table.js @@ -0,0 +1,56 @@ +module.exports = { + up: (migration, Types) => { + return migration.createTable('server_tokens', { + id: { + type: Types.UUID, + primaryKey: true, + defaultValue: Types.UUIDV4, + allowNull: false, + }, + value: { + type: Types.TEXT, + allowNull: false, + }, + name: { + type: Types.TEXT, + allowNull: false, + }, + description: { + type: Types.TEXT, + allowNull: true, + }, + owner_id: { + type: Types.UUID, + allowNull: false, + references: { + model: 'users', + key: 'id', + }, + }, + meta: { + type: Types.JSONB, + defaultValue: {}, + allowNull: false, + }, + created_at: { + type: Types.DATE, + defaultValue: Types.fn('now'), + allowNull: false, + }, + updated_at: { + type: Types.DATE, + defaultValue: Types.fn('now'), + allowNull: false, + }, + deleted_at: { + type: Types.DATE, + defaultValue: null, + allowNull: true, + }, + }) + }, + + down: (migration, Types) => { + return migration.dropTable('server_tokens') + }, +} diff --git a/public/icons/plus.svg b/public/icons/plus.svg new file mode 100644 index 0000000000000000000000000000000000000000..a8b122b94f2a8994ed06c607071f2599b1529f2d --- /dev/null +++ b/public/icons/plus.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="white" stroke="white"> + <path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" /> +</svg> \ No newline at end of file diff --git a/public/logo.svg b/public/logo.svg new file mode 100644 index 0000000000000000000000000000000000000000..b2b998ab8912ef5f33da64817116681e186ee9fd --- /dev/null +++ b/public/logo.svg @@ -0,0 +1,4 @@ +<svg width="300" height="300" viewBox="0 0 300 300" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M0 24C0 10.7452 10.7452 0 24 0H276C289.255 0 300 10.7452 300 24V276C300 289.255 289.255 300 276 300H24C10.7452 300 0 289.255 0 276V24Z" fill="#FF5E82"/> +<path d="M159.986 49.6563L201.071 44.9422M112.032 61.4361L128.357 50.9985L130.245 40.8337L140.983 39.4861L144.791 31.3401L157.367 32.5404L173.267 20.6897L179.672 33.7953L201.349 44.6312L204.437 74.9782L171.924 138.564L174.09 179.747L187.818 212.986V233.217L175.536 260.672L150.967 277.292H133.622L105.441 268.278L101.829 243.333L122.781 227.439L133.617 233.943L132.847 246.617L132.176 239.001L122.781 235.389L114.831 243.338L119.169 259.957L133.622 263.569L149.516 254.174L158.186 228.885L150.962 205.762L117.723 168.191V129.894L133.617 103.159L166.13 74.9782L146.619 72.0865L114.106 73.5323L101.824 78.5902L95.3202 72.0865L98.9322 62.691L112.032 61.4361ZM133.72 102.859L172.11 138.553L133.72 102.859ZM117.554 168.185L172.104 138.553L117.554 168.185ZM173.452 170.204L145.167 153.366L173.452 170.204ZM139.106 192.432L180.861 196.475L139.106 192.432ZM145.167 153.372L159.986 194.457L145.167 153.372ZM166.043 75.2456L203.084 61.1033L166.043 75.2456ZM157.291 32.8187L159.986 49.6563L157.291 32.8187ZM141.124 39.808L159.981 49.6563L141.124 39.808ZM149.21 254.392L175.476 260.454L149.21 254.392ZM157.962 228.798L175.471 260.449L157.962 228.798ZM151.229 205.904L187.6 213.313L151.229 205.904ZM171.433 211.96L166.719 244.959L171.433 211.96ZM179.514 123.063L155.267 84.0027L179.514 123.063ZM118.902 259.777L104.083 244.959L118.902 259.777ZM133.72 263.82V279.31V263.82ZM159.986 49.6563L166.048 75.251L159.986 49.6563ZM128.33 50.9985L166.043 75.2456L128.33 50.9985ZM114.188 73.2268L145.839 62.451L114.188 73.2268Z" stroke="#FAF9F9" stroke-width="4" stroke-miterlimit="10"/> +</svg> diff --git a/src/app.js b/src/app.js index 7304126d994ea09ee5d46edcf88382e2f5af3265..91bf6668fdfa0f6813e5630c7a8a6dd29425374a 100644 --- a/src/app.js +++ b/src/app.js @@ -19,6 +19,7 @@ const debug = require('debug')('server:boot') const requestLog = require('debug')('server:request') const serviceProvider = require('core/injection/ServiceProvider') +const notFound = require("http/middleware/NotFoundHandler"); module.exports = async function createApp(app = new Koa()) { const { fs } = require('bootstrap') @@ -35,6 +36,7 @@ module.exports = async function createApp(app = new Koa()) { app.use(bodyparser()) app.use(logger(s => requestLog(s))) app.use(static(pathutil.resolve(__dirname + '/../public'))) + app.use(notFound) app.use(async (ctx, next) => { if (ctx.method === 'OPTIONS') { diff --git a/src/database/models/OAuthClient.js b/src/database/models/OAuthClient.js index 5eaa4d320f7645e1ea3bad60fdb51bf134613ce0..9032d6234814814d1e7298f7af8400f54ac0c1e8 100644 --- a/src/database/models/OAuthClient.js +++ b/src/database/models/OAuthClient.js @@ -21,6 +21,7 @@ class OAuthClient extends BaseModel { owner_id: userId, name: params.name ?? '', description: params.description ?? '', + redirect_uris: params.redirect_uris ?? [], secret, grant_types: ['authorization_code', 'refresh_token'], internal, diff --git a/src/database/models/ServerToken.js b/src/database/models/ServerToken.js new file mode 100644 index 0000000000000000000000000000000000000000..40986a0a48b7127ea07392252f7ab46446fd4a03 --- /dev/null +++ b/src/database/models/ServerToken.js @@ -0,0 +1,60 @@ +const timestamps = require('./properties/timestamps') +const BaseModel = require('./BaseModel') + +class ServerToken extends BaseModel { + static associate(models) { + this.belongsTo(models.User, { foreignKey: 'owner_id' }) + } + + toJSON() { + const user = this.user ? { user: this.user } : {} + return { + id: this.id, + value: this.value, + name: this.name, + description: this.description, + expires_at: this.expires_at, + ...user, + meta: this.meta, + created_at: this.created_at, + updated_at: this.updated_at, + } + } +} + +module.exports = (sequelize, DataTypes) => { + ServerToken.init( + Object.assign( + { + id: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + validate: { + isUUID: 4, + }, + }, + value: { + type: DataTypes.TEXT, + }, + name: { + type: DataTypes.TEXT, + }, + description: { + type: DataTypes.TEXT, + }, + meta: { + type: DataTypes.JSONB, + }, + }, + timestamps(DataTypes), + ), + { + sequelize, + paranoid: true, + tableName: 'server_tokens', + }, + ) + + return ServerToken +} diff --git a/src/database/models/User.js b/src/database/models/User.js index 7aa94e4010e14a00f44c48462010043a869e9d9d..ac7d3ec51cc4439a59b738c7676c69e70378d0d9 100644 --- a/src/database/models/User.js +++ b/src/database/models/User.js @@ -7,6 +7,7 @@ class User extends BaseModel { this.hasMany(models.AuthorizationCode, { foreignKey: 'user_id' }) this.hasMany(models.OAuthClient, { foreignKey: 'owner_id' }) this.hasMany(models.AccessToken, { foreignKey: 'user_id' }) + this.hasMany(models.ServerToken, { foreignKey: 'owner_id' }) this.hasMany(models.RefreshToken, { foreignKey: 'user_id' }) this.hasMany(models.File, { foreignKey: 'user_id' }) this.hasMany(models.Upload, { foreignKey: 'user_id' }) diff --git a/src/domain/auth/AuthenticationService.js b/src/domain/auth/AuthenticationService.js index 9da4baa1d939a6e18294980a372b31ef1759ada7..6ad4ea3e722b8b0a677d9dd03d69eceffeb97c24 100644 --- a/src/domain/auth/AuthenticationService.js +++ b/src/domain/auth/AuthenticationService.js @@ -1,6 +1,6 @@ const ContextualModule = require('core/injection/ContextualModule') const crypto = require('core/utils/crypto') -const { User, AccessToken } = require('database/models') +const { User, AccessToken, ServerToken } = require('database/models') const HttpError = require('core/errors/HttpError') const { reportContextError } = require('../../vendor/sentry') @@ -17,6 +17,8 @@ module.exports = class AuthenticationService extends ContextualModule { init() { this._user = null + this._method = null + this._model = null } async attemptLogin(email, password) { @@ -33,7 +35,7 @@ module.exports = class AuthenticationService extends ContextualModule { const user = await this.ctx.services['core.users'].findByEmail(email) if (user) { if (await user.checkPassword(password)) { - this.authenticateAs(user) + this.authenticateAs(user, 'email') return user } else { throw new HttpError(403, 'Invalid username or password') @@ -52,23 +54,39 @@ module.exports = class AuthenticationService extends ContextualModule { const user = await User.findByPk(value.id) if (user) { - this.authenticateAs(user) + this.authenticateAs(user, 'cookie') return this._user } } else if (this.ctx.get('Authorization')) { const token = this.ctx.get('Authorization').substr(HEADER_PREFIX.length) - let accessToken = await AccessToken.findOne({ - where: { token }, + let accessToken = await ServerToken.findOne({ + where: { value: token }, include: [{ model: User }], }) if (accessToken == null) { - accessToken =await this.ctx.services['auth.oidc'].withProvider(provider => provider.AccessToken.find(token)) + accessToken = await AccessToken.findOne({ + where: { token }, + include: [{ model: User }] + }) + } else { + if (accessToken?.User) { + this.authenticateAs(accessToken.User, 'server_token', accessToken) + } + } + + if (accessToken == null) { + console.log('oidc token', token) + accessToken = await this.ctx.services['auth.oidc'].withProvider(provider => provider.AccessToken.find(token)) + } else { + if (accessToken?.User) { + this.authenticateAs(accessToken.User, 'oauth_token', accessToken) + } } - if (accessToken.User) { - this.authenticateAs(accessToken.User) + if (accessToken?.User) { + this.authenticateAs(accessToken.User, 'oidc_token', accessToken) return this._user } } else if (this.ctx.get('x-api-token')) { @@ -76,7 +94,7 @@ module.exports = class AuthenticationService extends ContextualModule { try { const user = await User.fromToken(token, this.ctx.get('x-token-type')) if (user) { - this.authenticateAs(user) + this.authenticateAs(user, `xat_${ this.ctx.get('x-token-type') }`) return this._user } } catch (e) { @@ -88,8 +106,18 @@ module.exports = class AuthenticationService extends ContextualModule { return null } - authenticateAs(user) { + async getMethod() { + await this.getUser() + return { + model: this._model, + method: this._method, + } + } + + authenticateAs(user, method = null, model = null) { this._user = user + this._method = method + this._model = model } async saveToSession(logoutIfEmpty = true) { diff --git a/src/domain/auth/OAuthFlow.js b/src/domain/auth/OAuthFlow.js index ce25c979f4e629ce78b374d5feb6cacd34ac6016..4029033198f417aa2f5b67085bfe6e4558919f4d 100644 --- a/src/domain/auth/OAuthFlow.js +++ b/src/domain/auth/OAuthFlow.js @@ -189,6 +189,11 @@ const scopeDescriptionMap = exports.validScopes = { description: 'The ability to see information about your account stats, including your points and citizen scientist level', }, + 'developer:admin': { + name: 'Developer Settings Admin Access', + description: 'Read / Write access to your server tokens and OAuth applications. Granting this permission to an' + + ' application could leak confidential information' + } } /** diff --git a/src/domain/auth/oidc/OIDCServer.js b/src/domain/auth/oidc/OIDCServer.js index 02084f998cc69f503e19f5c8ff29737963224a82..0c59af73298b4b8077554921a84589ad0a225663 100644 --- a/src/domain/auth/oidc/OIDCServer.js +++ b/src/domain/auth/oidc/OIDCServer.js @@ -63,6 +63,10 @@ module.exports = async function createOIDCServer() { pkce: { required: () => false, }, + ttl: { + AccessToken: 3600 * 24, + IdToken: 3600 * 24, + }, routes: { authorization: '/oidc/auth', backchannel_authentication: '/oidc/backchannel', diff --git a/src/http/controllers/api/user.js b/src/http/controllers/api/user.js index 7c100458b8f026af5fddecdeb2a4409b351fbf6f..7d080180739a26afdd808128c7487c1b082e5d79 100644 --- a/src/http/controllers/api/user.js +++ b/src/http/controllers/api/user.js @@ -7,11 +7,7 @@ exports.self = async ctx => { user: await user.serialise(), } } else { - throw new HttpError({ - status: 404, - title: 'No such user', - description: 'No user is currently logged in', - }) + throw new HttpError(404, 'No user is currently logged in') } } diff --git a/src/http/controllers/auth.js b/src/http/controllers/auth.js index fe8927530bc5916e61aaf0f5baea715cc1c9afbe..3e911f1454cc61c35da6d436c0edb1d294375871 100644 --- a/src/http/controllers/auth.js +++ b/src/http/controllers/auth.js @@ -10,7 +10,7 @@ exports.login = async ctx => { } exports.logout = async ctx => { await ctx.services['core.auth'].clearSessionAuth() - return ctx.redirect('/') + return ctx.redirect('https://jetsam.tech') } exports.showLogin = async ctx => { @@ -31,25 +31,6 @@ exports.showLogin = async ctx => { exports.handleLoginRedirect = async ctx => { const redirectTo = await createRedirectedUrl(ctx) return ctx.redirect(redirectTo) - - // const query = ctx.request.query - // - // if (query.login_state || query.auth_state) { - // const { login_state, auth_state } = ctx.request.query - // const state = login_state ?? auth_state - // - // const values = JSON.parse(await crypto.decrypt(state)) - // - // if (values.redirect === 'authorize') { - // return ctx.redirect( - // `/auth/authorize?auth_state=${state}`, - // ) - // } else { - // return ctx.redirect('/') - // } - // } else { - // return ctx.redirect('/') - // } } const resetErrorMessages = { diff --git a/src/http/controllers/dev/clients.js b/src/http/controllers/dev/clients.js index f0e86113055e60f209bd921d6d283fa2468b1d2b..9e59939329f291d281d20517413cdc85d3aea015 100644 --- a/src/http/controllers/dev/clients.js +++ b/src/http/controllers/dev/clients.js @@ -1,3 +1,154 @@ +const HttpError = require("../../../core/errors/HttpError"); +const {validScopes} = require("../../../domain/auth/OAuthFlow"); +const {OAuthClient} = require("database/models"); + exports.showClients = async ctx => { - return await ctx.render('developers/index') + const user = await ctx.services['core.auth'].getUser() + const serverTokens = await user.getServerTokens() + const clients = await user.getOAuthClients() + + return await ctx.render('developers/index', { + tokens: serverTokens.map(t => t.toJSON()), + clients: clients.map(c => c.toJSON()), + }) +} +exports.showCreateToken = async ctx => { + return await ctx.render('developers/token/new') +} +exports.showCreateApplication = async ctx => { + const scopes = Object.entries(validScopes) + .filter(([scope]) => scope !== '*' && !scope.includes('admin')) + .map(([scope, data]) => ({ + key: scope, + ...data, + })) + + return await ctx.render('developers/application/new', { scopes }) +} + +exports.tokenAction = async ctx => { + const method = ctx.request.body?._method + + switch (method) { + case 'delete': + await exports.deleteToken(ctx) + return + default: + return + } +} + +exports.deleteToken = async ctx => { + const user = await ctx.services['core.auth'].getUser() + const tokenId = ctx.params.id + + const token = await user.getServerTokens({ where: { id: tokenId } }) + + if (token?.length > 0) { + await token[0].destroy() + } + + ctx.redirect('/developers') +} + +exports.createToken = async ctx => { + const user = await ctx.services['core.auth'].getUser() + + const { name, description } = ctx.request.body + + const token = await user.asJWTToken() + const serverToken = await user.createServerToken({ + value: token, + name, + description, + }) + + if (serverToken) { + return ctx.redirect('/developers') + } + + throw new HttpError(500, 'Failed to create server token') +} + +exports.createApplication = async ctx => { + const user = await ctx.services['core.auth'].getUser() + + const { name, description, redirects } = ctx.request.body + const redirectList = redirects.split(',') + .map(r => r.trim()) + .filter(Boolean) + + const client = await OAuthClient.generateClient(user.id, { name, description, redirect_uris: redirectList }) + if (client) { + return ctx.redirect('/developers') + } + + throw new HttpError(500, 'Failed to create an OAuth Application') +} + +exports.applicationAction = async ctx => { + const method = ctx.request.body?._method + + switch (method) { + case 'delete': + await exports.deleteApplication(ctx) + return + case 'put': + await exports.updateApplication(ctx) + default: + return + } +} + +exports.deleteApplication = async ctx => { + const user = await ctx.services['core.auth'].getUser() + const appId = ctx.params.id + + const apps = await user.getOAuthClients({ where: { id: appId } }) + + if (apps?.length > 0) { + await apps[0].destroy() + } + + ctx.redirect('/developers') +} + +exports.updateApplication = async ctx => { + const user = await ctx.services['core.auth'].getUser() + const appId = ctx.params.id + + const apps = await user.getOAuthClients({ where: { id: appId } }) + + if (apps.length > 0) { + const application = apps[0] + + const { name, description, redirects } = ctx.request.body + const redirectList = redirects.split(',') + .map(r => r.trim()) + .filter(Boolean) + + application.name = name + application.description = description + application.redirect_uris = redirectList + + await application.save() + ctx.redirect('/developers') + } +} +exports.showEditApplication = async ctx => { + const user = await ctx.services['core.auth'].getUser() + const appId = ctx.params.id + + const apps = await user.getOAuthClients({ where: { id: appId } }) + if (apps.length > 0) { + const application = apps[0] + return await ctx.render('developers/application/edit', { + application: { + id: application.id, + name: application.name, + description: application.description ?? '', + redirects: application.redirect_uris.join(',') + } + }) + } } \ No newline at end of file diff --git a/src/http/middleware/NotFoundHandler.js b/src/http/middleware/NotFoundHandler.js new file mode 100644 index 0000000000000000000000000000000000000000..4c2f0eff0841872856813bd2e7b7ded2699f2a66 --- /dev/null +++ b/src/http/middleware/NotFoundHandler.js @@ -0,0 +1,48 @@ +const SentryReporter = require('./SentryReporter') + +module.exports = async (ctx, next) => { + try { + await next() + await applyResponse(ctx) + } catch(e) { + ctx.status = 500 + await applyResponse(ctx, e) + } +} + +async function applyResponse(ctx, e = null) { + if (e) { + await SentryReporter.report(e, ctx) + } + + if (ctx.status >= 400) { + const initialStatus = ctx.status + + if (ctx.accepts('html')) { + if (ctx.status === 404 && ctx.body == null) { + await ctx.render('404', { status: '404', title: 'Page not found', description: 'Sorry, we couldn\'t find the page you were looking for.' }) + } + + if (ctx.status === 500) { + await ctx.render('404', { status: '500', title: 'Something went wrong', description: 'There was a problem with the action that you just attempted. Sorry about that.' }) + } + } else if (ctx.accepts('json')) { + if (ctx.status === 404 && ctx.body == null) { + ctx.body = { + errors: { + general: ['The requested resource does not exist'], + }, + } + } + if (ctx.status === 500 && ctx.body == null) { + ctx.body = { + errors: { + general: ['There was a problem with the action that you just attempted'], + }, + } + } + } + + ctx.status = initialStatus + } +} diff --git a/src/http/routes.js b/src/http/routes.js index f206ce01b5d6c8511668eeeafb54ef206958df46..e71aecafe5bb33d5ba4a6b0c8be9fa094ae639d5 100644 --- a/src/http/routes.js +++ b/src/http/routes.js @@ -14,6 +14,7 @@ const errors = require('http/middleware/ErrorHandler') const includes = require('http/middleware/ParseIncludes') const profiling = require('http/middleware/Profiler') const loaders = require('http/middleware/MountLoaders') +const notFound = require('http/middleware/NotFoundHandler') const userGate = require('http/middleware/RequiresAuth') const authRedirect = require('http/middleware/RedirectToLogin') const device = require('http/middleware/DeviceProperties').extractDevice @@ -65,6 +66,14 @@ web.post('/auth/authorize', AuthServer.authorize) web.post('/auth/token', AuthServer.token) web.get('/developers', authRedirect, controller('dev/clients', 'showClients')) +web.get('/developers/tokens/new', authRedirect, controller('dev/clients', 'showCreateToken')) +web.get('/developers/applications/new', authRedirect, controller('dev/clients', 'showCreateApplication')) +web.get('/developers/applications/:id', authRedirect, controller('dev/clients', 'showEditApplication')) + +web.post('/developers/tokens', authRedirect, controller('dev/clients', 'createToken')) +web.post('/developers/tokens/:id', authRedirect, controller('dev/clients', 'tokenAction')) +web.post('/developers/applications', authRedirect, controller('dev/clients', 'createApplication')) +web.post('/developers/applications/:id', authRedirect, controller('dev/clients', 'applicationAction')) env('FS_DRIVER', 'local') === 'local' && (function () { diff --git a/src/vendor/koa-handlebars.js b/src/vendor/koa-handlebars.js index 363b9293e19b13afa5c58e348759198cb7c971f1..28c97f11eeec5334a3ae56b29d8afe922a9820f0 100644 --- a/src/vendor/koa-handlebars.js +++ b/src/vendor/koa-handlebars.js @@ -109,7 +109,7 @@ module.exports = function createRenderMiddleware(root, opts = {}) { data = {}, opts = {}, ) { - const content = await instance.render(template, data, opts) + const content = await instance.render(template, { ...data, ctx }, opts) if (content == null) { this.status = 404 return diff --git a/views/404.hbs b/views/404.hbs new file mode 100644 index 0000000000000000000000000000000000000000..6d14251f6ed315b66c5f689a49462f9d70f800db --- /dev/null +++ b/views/404.hbs @@ -0,0 +1,67 @@ +<!DOCTYPE html> + +<html class="h-full bg-white" lang="en"> +<head> + <title>Not Found | Jetsam</title> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + <meta name="description" content=""> + <meta name="author" content=""> + <!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css">--> + <script src="https://cdn.tailwindcss.com"></script> + <script> + tailwind.config = { + theme: { + extend: { + colors: { + jetsam: '#FF5E82', + 'jetsam-dark': '#FF1D3C', + 'jetsam-light': '#FF7BAC', + } + } + } + } + </script> + + <style> + body, body > div { + height: 100%; + } + </style> +</head> +<body class="h-full"> +<div class="min-h-full pt-16 pb-12 flex flex-col bg-white"> + <main class="flex-grow flex flex-col max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8"> + <div class="flex-shrink-0 flex justify-center mt-auto"> + <a href="/" class="inline-flex"> + <span class="sr-only">Workflow</span> + <img class="h-48 aspect-square" src="/logo.svg" alt="Jetsam"> + </a> + </div> + <div class="py-16 mb-auto"> + <div class="text-center"> + <p class="text-sm font-semibold text-jetsam uppercase tracking-wide">{{ status }} error</p> + <h1 class="mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl">{{ title }}</h1> + <p class="mt-2 text-base text-gray-500">{{ description }}</p> + <p class="text-base text-gray-500">Here are a few other things you can do:</p> + <div class="mt-6 space-y-3 flex flex-col"> + <a href="https://jetsam.tech" class="text-base font-medium text-jetsam hover:text-jetsam-light">Visit jetsam.tech<span aria-hidden="true"> →</span></a> + <a href="https://play.google.com/store/apps/details?id=tech.jetsam.catalog&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1" class="text-base font-medium text-jetsam hover:text-jetsam-light">Download Jetsam for Android<span aria-hidden="true"> →</span></a> + <a href="https://apps.apple.com/gb/app/jetsam/id1494342033?ls=1" class="text-base font-medium text-jetsam hover:text-jetsam-light">Download Jetsam for iOS<span aria-hidden="true"> →</span></a> + </div> + </div> + </div> + </main> + <footer class="flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 pb-8"> + <nav class="flex justify-center space-x-4"> + <a href="https://www.facebook.com/JetsamTech" class="text-sm font-medium text-gray-500 hover:text-gray-600">Facebook</a> + <span class="inline-block border-l border-gray-300" aria-hidden="true"></span> + <a href="https://instagram.com/jetsam.tech_app" class="text-sm font-medium text-gray-500 hover:text-gray-600">Instagram</a> + <span class="inline-block border-l border-gray-300" aria-hidden="true"></span> + <a href="https://twitter.com/jetsam_tech" class="text-sm font-medium text-gray-500 hover:text-gray-600">Twitter</a> + </nav> + </footer> +</div> + +</body> +</html> diff --git a/views/developers/application/edit.hbs b/views/developers/application/edit.hbs new file mode 100644 index 0000000000000000000000000000000000000000..136209e986de8ef2cdd63c4cb5905a054449583b --- /dev/null +++ b/views/developers/application/edit.hbs @@ -0,0 +1,153 @@ +<!DOCTYPE html> + +<html class="h-full bg-gray-100" lang="en"> +<head> + <title>Edit OAuth Application | Jetsam</title> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + <meta name="description" content=""> + <meta name="author" content=""> + + <script src="/scripts/edit-application.js"></script> + <script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio"></script> + <script> + tailwind.config = { + theme: { + extend: { + colors: { + jetsam: '#FF5E82', + 'jetsam-dark': '#FF1D3C', + 'jetsam-light': '#FF7BAC', + } + } + } + } + </script> + + <style> + body, body > div { + height: 100%; + } + </style> +</head> +<body class="h-full"> + +<div class="flex flex-col md:flex-row"> + + {{> developer-sidebar }} + + <div class="flex flex-col flex-1"> + <main class="flex-1"> + <div class="py-6"> + <div class="max-w-7xl mx-auto px-4 sm:px-6 md:px-8 space-y-4"> + <!-- Content --> + <div class="bg-white shadow sm:rounded-lg"> + <div class="px-4 py-5 sm:p-6"> + + <h3 class="text-lg leading-6 font-medium text-gray-900">Create your OAuth Application</h3> + + <div class="mt-2 text-sm text-gray-500 bg-blue-200 p-4 border-radius-8"> + <p><span class="font-bold">Remember:</span> + When authenticating, users will be shown the application name, the application description, and a short piece of information about each scope that you request. + </p> + </div> + + <form id="new-app-form" class="mt-5 flex flex-col space-y-3" method="post" action="/developers/applications/{{application.id}}"> + <input type="hidden" name="_method" value="put" /> + + <div> + <label for="name" class="block text-sm font-medium text-gray-700">App Name</label> + <div class="mt-1"> + <input + type="text" + name="name" + id="name" + value="{{application.name}}" + minlength="1" + maxlength="50" + required + class="shadow-sm focus:ring-jetsam focus:border-jetsam block w-full sm:text-sm border-gray-300 rounded-md" + placeholder="Litterbug 9000" + aria-describedby="name-description"> + </div> + <p class="mt-2 text-sm text-gray-500" id="name-description">The name of your business or application</p> + </div> + + <div> + <label for="description" class="block text-sm font-medium text-gray-700">App Description</label> + <div class="mt-1"> + <textarea + type="text" + rows="4" + name="description" + id="description" + maxlength="500" + class="shadow-sm focus:ring-jetsam focus:border-jetsam block w-full sm:text-sm border-gray-300 rounded-md" + placeholder="This app saves the world" + aria-describedby="description-description">{{application.description}}</textarea> + </div> + <p class="mt-2 text-sm text-gray-500" id="description-description">Describe what your application does with a user's Jetsam account</p> + </div> + + <div id="redirects-container"> + <label for="redirects" class="block text-sm font-medium text-gray-700"> + <p>Redirect URIs</p> + <p id="no-js-description" class="text-sm italic text-gray-600">Provide a comma seperated list of redirect URIs</p> + </label> + <div class="mt-1 flex" id="redirects-wrapper"> + <input + type="text" + name="redirects" + id="redirects" + value="{{application.redirects}}" + maxlength="500" + class="flex-1 shadow-sm focus:ring-jetsam focus:border-jetsam block w-full sm:text-sm border-gray-300 rounded-md" + placeholder="https://example.com" + aria-describedby="redirects-description"> + </div> + <p class="mt-2 text-sm text-gray-500" id="redirects-description">The auth process will only successfully complete if the redirect URI provided during the OAuth exchange exactly matches the protocol, host and path of one of these URIs.</p> + <p id="redirects-error" class="hidden p-4 bg-red-200 text-red-800 rounded-md mt-2"></p> + <div id="redirects-list" class="hidden space-x-2 space-y-2 flex-wrap flex items-center border border-dashed border-gray-300 mt-4 p-4" style="min-height: 2rem;"></div> + </div> + + {{!-- + <fieldset class="space-y-5"> + <legend class="sr-only">Scopes</legend> + <h2> + App Permissions + </h2> + <p class="text-sm">Users will be asked to approve the selected permissions when authenticating, and will be shown the following descriptions of each:</p> + {{#each scopes}} + <div class="relative flex items-start"> + <div class="flex items-center h-5"> + <input id="scope-{{key}}" aria-describedby="scope-{{key}}-description" name="scope:{{key}}" type="checkbox" class="focus:ring-jetsam h-4 w-4 text-jetsam border-gray-300 rounded"> + </div> + <div class="ml-3 text-sm"> + <label for="scope-{{key}}" class="flex flex-col space-y-2 "> + <span class="inline-flex items-center space-x-2"> + <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800"> {{key}} </span> + <span class="font-medium text-gray-700">{{name}}</span> + </span> + <span id="scope-{{key}}-description" class="text-gray-500">{{description}}</span> + </label> + </div> + </div> + {{/each}} + </fieldset> + --}} + + <button type="submit" + class="mt-3 w-full inline-flex items-center justify-center px-4 py-2 border border-transparent shadow-sm font-medium rounded-md text-white bg-emerald-600 hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-emerald-500 sm:mt-0 sm:w-auto sm:text-sm"> + Save + </button> + </form> + </div> + </div> + </div> + </div> + </main> + </div> +</div> + +</body> +</html> diff --git a/views/developers/application/new.hbs b/views/developers/application/new.hbs new file mode 100644 index 0000000000000000000000000000000000000000..8f065e5db8bd1dd83ce8308421b9608edc26c68d --- /dev/null +++ b/views/developers/application/new.hbs @@ -0,0 +1,150 @@ +<!DOCTYPE html> + +<html class="h-full bg-gray-100" lang="en"> +<head> + <title>New OAuth Application | Jetsam</title> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + <meta name="description" content=""> + <meta name="author" content=""> + + <script src="/scripts/create-application.js"></script> + <script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio"></script> + <script> + tailwind.config = { + theme: { + extend: { + colors: { + jetsam: '#FF5E82', + 'jetsam-dark': '#FF1D3C', + 'jetsam-light': '#FF7BAC', + } + } + } + } + </script> + + <style> + body, body > div { + height: 100%; + } + </style> +</head> +<body class="h-full"> + +<div class="flex flex-col md:flex-row"> + + {{> developer-sidebar }} + + <div class="flex flex-col flex-1"> + <main class="flex-1"> + <div class="py-6"> + <div class="max-w-7xl mx-auto px-4 sm:px-6 md:px-8 space-y-4"> + <!-- Content --> + <div class="bg-white shadow sm:rounded-lg"> + <div class="px-4 py-5 sm:p-6"> + + <h3 class="text-lg leading-6 font-medium text-gray-900">Create your OAuth Application</h3> + + <div class="mt-2 text-sm text-gray-500 bg-blue-200 p-4 border-radius-8"> + <p><span class="font-bold">Remember:</span> + When authenticating, users will be shown the application name, the application description, and a short piece of information about each scope that you request. + </p> + </div> + + <form id="new-app-form" class="mt-5 flex flex-col space-y-3" method="post" action="/developers/applications"> + + <div> + <label for="name" class="block text-sm font-medium text-gray-700">App Name</label> + <div class="mt-1"> + <input + type="text" + name="name" + id="name" + minlength="1" + maxlength="50" + required + class="shadow-sm focus:ring-jetsam focus:border-jetsam block w-full sm:text-sm border-gray-300 rounded-md" + placeholder="Litterbug 9000" + aria-describedby="name-description"> + </div> + <p class="mt-2 text-sm text-gray-500" id="name-description">The name of your business or application</p> + </div> + + <div> + <label for="description" class="block text-sm font-medium text-gray-700">App Description</label> + <div class="mt-1"> + <textarea + type="text" + rows="4" + name="description" + id="description" + maxlength="500" + class="shadow-sm focus:ring-jetsam focus:border-jetsam block w-full sm:text-sm border-gray-300 rounded-md" + placeholder="This app saves the world" + aria-describedby="description-description"></textarea> + </div> + <p class="mt-2 text-sm text-gray-500" id="description-description">Describe what your application does with a user's Jetsam account</p> + </div> + + <div id="redirects-container"> + <label for="redirects" class="block text-sm font-medium text-gray-700"> + <p>Redirect URIs</p> + <p id="no-js-description" class="text-sm italic text-gray-600">Provide a comma seperated list of redirect URIs</p> + </label> + <div class="mt-1 flex" id="redirects-wrapper"> + <input + type="text" + name="redirects" + id="redirects" + maxlength="500" + class="flex-1 shadow-sm focus:ring-jetsam focus:border-jetsam block w-full sm:text-sm border-gray-300 rounded-md" + placeholder="https://example.com" + aria-describedby="redirects-description"> + </div> + <p class="mt-2 text-sm text-gray-500" id="redirects-description">The auth process will only successfully complete if the redirect URI provided during the OAuth exchange exactly matches the protocol, host and path of one of these URIs.</p> + <p id="redirects-error" class="hidden p-4 bg-red-200 text-red-800 rounded-md mt-2"></p> + <div id="redirects-list" class="hidden space-x-2 space-y-2 flex-wrap flex items-center border border-dashed border-gray-300 mt-4 p-4" style="min-height: 2rem;"></div> + </div> + + {{!-- + <fieldset class="space-y-5"> + <legend class="sr-only">Scopes</legend> + <h2> + App Permissions + </h2> + <p class="text-sm">Users will be asked to approve the selected permissions when authenticating, and will be shown the following descriptions of each:</p> + {{#each scopes}} + <div class="relative flex items-start"> + <div class="flex items-center h-5"> + <input id="scope-{{key}}" aria-describedby="scope-{{key}}-description" name="scope:{{key}}" type="checkbox" class="focus:ring-jetsam h-4 w-4 text-jetsam border-gray-300 rounded"> + </div> + <div class="ml-3 text-sm"> + <label for="scope-{{key}}" class="flex flex-col space-y-2 "> + <span class="inline-flex items-center space-x-2"> + <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800"> {{key}} </span> + <span class="font-medium text-gray-700">{{name}}</span> + </span> + <span id="scope-{{key}}-description" class="text-gray-500">{{description}}</span> + </label> + </div> + </div> + {{/each}} + </fieldset> + --}} + + <button type="submit" + class="mt-3 w-full inline-flex items-center justify-center px-4 py-2 border border-transparent shadow-sm font-medium rounded-md text-white bg-emerald-600 hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-emerald-500 sm:mt-0 sm:w-auto sm:text-sm"> + Save + </button> + </form> + </div> + </div> + </div> + </div> + </main> + </div> +</div> + +</body> +</html> diff --git a/views/developers/index.hbs b/views/developers/index.hbs index 6a00a70c71e6d410227588e5f90d5e7e69e74cc4..acaabc11196785e583a2cae7f20acc0f37f2f415 100644 --- a/views/developers/index.hbs +++ b/views/developers/index.hbs @@ -2,13 +2,27 @@ <html class="h-full bg-gray-100" lang="en"> <head> - <title>Login | Jetsam</title> + <title>Developer Settings | Jetsam</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content=""> <meta name="author" content=""> <!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css">--> - <script src="https://cdn.tailwindcss.com"></script> + <script src="/scripts/developers.js"></script> + <script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio"></script> + <script> + tailwind.config = { + theme: { + extend: { + colors: { + jetsam: '#FF5E82', + 'jetsam-dark': '#FF1D3C', + 'jetsam-light': '#FF7BAC', + } + } + } + } + </script> <style> body, body > div { @@ -17,329 +31,155 @@ </style> </head> <body class="h-full"> -<aside class="lg:grid lg:grid-cols-12 lg:gap-x-5"> - <div class="flex-1 flex flex-col min-h-0 bg-indigo-700"> - <div class="flex-1 flex flex-col pt-5 pb-4 overflow-y-auto"> - <div class="flex items-center flex-shrink-0 px-4"> - <img class="h-8 w-auto" src="https://tailwindui.com/img/logos/workflow-logo-indigo-300-mark-white-text.svg" alt="Workflow"> - </div> - <nav class="mt-5 flex-1 px-2 space-y-1"> - <!-- Current: "bg-indigo-800 text-white", Default: "text-white hover:bg-indigo-600 hover:bg-opacity-75" --> - <a href="#" class="bg-indigo-800 text-white group flex items-center px-2 py-2 text-sm font-medium rounded-md"> - <!-- Heroicon name: outline/home --> - <svg class="mr-3 flex-shrink-0 h-6 w-6 text-indigo-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" /> - </svg> - Dashboard - </a> - <a href="#" class="text-white hover:bg-indigo-600 hover:bg-opacity-75 group flex items-center px-2 py-2 text-sm font-medium rounded-md"> - <!-- Heroicon name: outline/users --> - <svg class="mr-3 flex-shrink-0 h-6 w-6 text-indigo-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /> - </svg> - Team - </a> +<div class="flex flex-col md:flex-row"> - <a href="#" class="text-white hover:bg-indigo-600 hover:bg-opacity-75 group flex items-center px-2 py-2 text-sm font-medium rounded-md"> - <!-- Heroicon name: outline/folder --> - <svg class="mr-3 flex-shrink-0 h-6 w-6 text-indigo-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" /> - </svg> - Projects - </a> + {{> developer-sidebar}} - <a href="#" class="text-white hover:bg-indigo-600 hover:bg-opacity-75 group flex items-center px-2 py-2 text-sm font-medium rounded-md"> - <!-- Heroicon name: outline/calendar --> - <svg class="mr-3 flex-shrink-0 h-6 w-6 text-indigo-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /> - </svg> - Calendar - </a> + <div class="flex flex-col flex-1 "> + <main class="flex-1"> + <div class="py-6"> + <div class="max-w-7xl mx-auto px-4 sm:px-6 md:px-8 space-y-4"> - <a href="#" class="text-white hover:bg-indigo-600 hover:bg-opacity-75 group flex items-center px-2 py-2 text-sm font-medium rounded-md"> - <!-- Heroicon name: outline/inbox --> - <svg class="mr-3 flex-shrink-0 h-6 w-6 text-indigo-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" /> - </svg> - Documents - </a> - - <a href="#" class="text-white hover:bg-indigo-600 hover:bg-opacity-75 group flex items-center px-2 py-2 text-sm font-medium rounded-md"> - <!-- Heroicon name: outline/chart-bar --> - <svg class="mr-3 flex-shrink-0 h-6 w-6 text-indigo-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /> - </svg> - Reports - </a> - </nav> - </div> - <div class="flex-shrink-0 flex border-t border-indigo-800 p-4"> - <a href="#" class="flex-shrink-0 w-full group block"> - <div class="flex items-center"> - <div> - <img class="inline-block h-9 w-9 rounded-full" src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt=""> - </div> - <div class="ml-3"> - <p class="text-sm font-medium text-white">Tom Cook</p> - <p class="text-xs font-medium text-indigo-200 group-hover:text-white">View profile</p> - </div> - </div> - </a> - </div> - </div> -</aside> -<!--<div class="md:pl-64 flex flex-col flex-1">--> -<!-- <div class="sticky top-0 z-10 md:hidden pl-1 pt-1 sm:pl-3 sm:pt-3 bg-gray-100">--> -<!-- <button type="button" class="-ml-0.5 -mt-0.5 h-12 w-12 inline-flex items-center justify-center rounded-md text-gray-500 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500">--> -<!-- <span class="sr-only">Open sidebar</span>--> -<!-- <!– Heroicon name: outline/menu –>--> -<!-- <svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">--> -<!-- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />--> -<!-- </svg>--> -<!-- </button>--> -<!-- </div>--> - - <div class="flex-1 space-y-6 sm:px-6 lg:px-0 lg:col-span-9"> - <form action="#" method="POST"> - <div class="shadow sm:rounded-md sm:overflow-hidden"> - <div class="bg-white py-6 px-4 space-y-6 sm:p-6"> - <div> - <h3 class="text-lg leading-6 font-medium text-gray-900">Profile</h3> - <p class="mt-1 text-sm text-gray-500">This information will be displayed publicly so be careful - what you share.</p> - </div> - - <div class="grid grid-cols-3 gap-6"> - <div class="col-span-3 sm:col-span-2"> - <label for="company-website" class="block text-sm font-medium text-gray-700"> - Username </label> - <div class="mt-1 rounded-md shadow-sm flex"> - <span class="bg-gray-50 border border-r-0 border-gray-300 rounded-l-md px-3 inline-flex items-center text-gray-500 sm:text-sm"> workcation.com/ </span> - <input type="text" name="username" id="username" autocomplete="username" - class="focus:ring-indigo-500 focus:border-indigo-500 flex-grow block w-full min-w-0 rounded-none rounded-r-md sm:text-sm border-gray-300"> - </div> - </div> - - <div class="col-span-3"> - <label for="about" class="block text-sm font-medium text-gray-700"> About </label> - <div class="mt-1"> - <textarea id="about" name="about" rows="3" - class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 mt-1 block w-full sm:text-sm border border-gray-300 rounded-md" - placeholder="you@example.com"></textarea> - </div> - <p class="mt-2 text-sm text-gray-500">Brief description for your profile. URLs are - hyperlinked.</p> + <div class="bg-white overflow-hidden shadow rounded-lg divide-y divide-gray-200"> + <div class="px-4 py-5 sm:px-6"> + <p class="text-md font-semibold text-gray-700">Learn how to use your tokens and OAuth applications by visiting the <a class="text-jetsam hover:text-jetsam-dark underline" href="https://docs.jetsam.tech">developer documentation</a></p> </div> + </div> - <div class="col-span-3"> - <label class="block text-sm font-medium text-gray-700"> Photo </label> - <div class="mt-1 flex items-center"> - <span class="inline-block bg-gray-100 rounded-full overflow-hidden h-12 w-12"> - <svg class="h-full w-full text-gray-300" fill="currentColor" viewBox="0 0 24 24"> - <path d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z"/> - </svg> - </span> - <button type="button" - class="ml-5 bg-white border border-gray-300 rounded-md shadow-sm py-2 px-3 text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> - Change - </button> - </div> + <!-- Card --> + <div class="bg-white overflow-hidden shadow rounded-lg divide-y divide-gray-200"> + <!-- Header --> + <div class="px-4 py-5 sm:px-6"> + <h2 class="text-2xl font-semibold text-gray-900 text-right">Your Server Tokens</h2> </div> - <div class="col-span-3"> - <label class="block text-sm font-medium text-gray-700"> Cover photo </label> - <div class="mt-1 border-2 border-gray-300 border-dashed rounded-md px-6 pt-5 pb-6 flex justify-center"> - <div class="space-y-1 text-center"> - <svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" - viewBox="0 0 48 48" aria-hidden="true"> - <path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" - stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> - </svg> - <div class="flex text-sm text-gray-600"> - <label for="file-upload" - class="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500"> - <span>Upload a file</span> - <input id="file-upload" name="file-upload" type="file" class="sr-only"> - </label> - <p class="pl-1">or drag and drop</p> + <!-- Content --> + <div class="px-4 py-5 sm:p-6 "> + <!-- Token List --> + {{#if tokens}} + <ul role="list" class="divide-y divide-gray-200"> + {{#each tokens}} + <!-- Token Details --> + <li id="{{id}}" class="flex flex-col space-y-2"> + <div class="relative flex px-4 py-4"> + <div class="flex-1 flex flex-col space-y-2"> + <h3 class="text-sm font-medium text-jetsam truncate">{{ name }}</h3> + <p>{{ description }}</p> + </div> + + <div class="w-max flex items-end space-x-2"> + <button data-show-token="{{id}}" data-token-value="{{value}}" type="button" class="ml-auto inline-flex items-center px-3 py-2 border border-transparent shadow-sm text-sm leading-4 font-medium rounded-md text-white bg-sky-600 hover:bg-sky-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-sky-500"> + Show Token + </button> + <form action="/developers/tokens/{{id}}" method="post"> + <input type="hidden" value="delete" name="_method" /> + <button type="submit" class="ml-auto inline-flex items-center px-3 py-2 border border-transparent shadow-sm text-sm leading-4 font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"> + Delete Token + </button> + </form> + </div> </div> - <p class="text-xs text-gray-500">PNG, JPG, GIF up to 10MB</p> + </li> + {{/each}} + </ul> + {{else}} + <div class="text-center"> + <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> + <path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" /> + </svg> + <h3 class="mt-2 text-sm font-medium text-gray-900">No Tokens</h3> + <p class="mt-1 text-sm text-gray-500">Get started by creating a new server token.</p> </div> - </div> - </div> - </div> - </div> - <div class="px-4 py-3 bg-gray-50 text-right sm:px-6"> - <button type="submit" - class="bg-indigo-600 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> - Save - </button> - </div> - </div> - </form> - - <form action="#" method="POST"> - <div class="shadow sm:rounded-md sm:overflow-hidden"> - <div class="bg-white py-6 px-4 space-y-6 sm:p-6"> - <div> - <h3 class="text-lg leading-6 font-medium text-gray-900">Personal Information</h3> - <p class="mt-1 text-sm text-gray-500">Use a permanent address where you can recieve mail.</p> - </div> - - <div class="grid grid-cols-6 gap-6"> - <div class="col-span-6 sm:col-span-3"> - <label for="first-name" class="block text-sm font-medium text-gray-700">First name</label> - <input type="text" name="first-name" id="first-name" autocomplete="given-name" - class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"> - </div> - - <div class="col-span-6 sm:col-span-3"> - <label for="last-name" class="block text-sm font-medium text-gray-700">Last name</label> - <input type="text" name="last-name" id="last-name" autocomplete="family-name" - class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"> + {{/if}} </div> - <div class="col-span-6 sm:col-span-4"> - <label for="email-address" class="block text-sm font-medium text-gray-700">Email - address</label> - <input type="text" name="email-address" id="email-address" autocomplete="email" - class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"> - </div> - - <div class="col-span-6 sm:col-span-3"> - <label for="country" class="block text-sm font-medium text-gray-700">Country</label> - <select id="country" name="country" autocomplete="country-name" - class="mt-1 block w-full bg-white border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"> - <option>United States</option> - <option>Canada</option> - <option>Mexico</option> - </select> - </div> - - <div class="col-span-6"> - <label for="street-address" class="block text-sm font-medium text-gray-700">Street - address</label> - <input type="text" name="street-address" id="street-address" autocomplete="street-address" - class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"> + <!-- Footer --> + <div class="px-4 py-4 sm:px-6 flex"> + <a href="/developers/tokens/new" class="ml-auto inline-flex items-center px-3 py-2 border border-transparent shadow-sm text-sm leading-4 font-medium rounded-md text-white bg-emerald-600 hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-emerald-500"> + Create A Token + <svg class="ml-3 -mr-1 h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd" /> + </svg> + </a> </div> + </div> - <div class="col-span-6 sm:col-span-6 lg:col-span-2"> - <label for="city" class="block text-sm font-medium text-gray-700">City</label> - <input type="text" name="city" id="city" autocomplete="address-level2" - class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"> + <!-- Card --> + <div class="bg-white overflow-hidden shadow rounded-lg divide-y divide-gray-200"> + <!-- Header --> + <div class="px-4 py-5 sm:px-6"> + <h2 class="text-2xl font-semibold text-gray-900 text-right">Your OAuth Applications</h2> </div> - <div class="col-span-6 sm:col-span-3 lg:col-span-2"> - <label for="region" class="block text-sm font-medium text-gray-700">State / Province</label> - <input type="text" name="region" id="region" autocomplete="address-level1" - class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"> + <!-- Content --> + <div class="px-4 py-5 sm:p-6"> + + <!-- Application List --> + {{#if clients}} + <ul role="list" class="divide-y divide-gray-200"> + {{#each clients}} + <li id="{{id}}" class="flex flex-col space-y-2"> + <div class="relative flex px-4 py-4"> + <div class="flex-1 flex flex-col space-y-2"> + <h3 class="text-sm font-medium text-jetsam truncate">{{ name }}</h3> + <p>{{ description }}</p> + </div> + + <div class="w-max gap-2 grid grid-cols-2"> + <div class="flex justify-center items-center"> + <button data-show-token="{{id}}" data-token-value="{{id}}" data-postfix="-app-id" type="button" class="inline-flex items-center px-3 py-2 border border-transparent shadow-sm text-sm leading-4 font-medium rounded-md text-white bg-sky-600 hover:bg-sky-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-sky-500"> + Show App ID + </button> + </div> + <div class="flex justify-center items-center"> + <button data-show-token="{{id}}" data-token-value="{{secret}}" data-postfix="-app-secret" type="button" class="inline-flex items-center px-3 py-2 border border-transparent shadow-sm text-sm leading-4 font-medium rounded-md text-white bg-sky-600 hover:bg-sky-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-sky-500"> + Show App Secret + </button> + </div> + <div class="flex justify-center items-center"> + <a href="/developers/applications/{{id}}" class="inline-flex items-center px-3 py-2 border border-transparent shadow-sm text-sm leading-4 font-medium rounded-md text-white bg-amber-600 hover:bg-amber-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-amber-500"> + Edit App + </a> + </div> + + <form action="/developers/applications/{{id}}" method="post" class="flex justify-center items-center"> + <input type="hidden" value="delete" name="_method" /> + <button type="submit" class="inline-flex items-center px-3 py-2 border border-transparent shadow-sm text-sm leading-4 font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"> + Delete App + </button> + </form> + </div> + </div> + </li> + {{/each}} + </ul> + {{else}} + <div class="text-center"> + <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> + <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /> + </svg> + <h3 class="mt-2 text-sm font-medium text-gray-900">No Applications</h3> + <p class="mt-1 text-sm text-gray-500">Get started by creating a new OAuth Application.</p> + </div> + {{/if}} </div> - <div class="col-span-6 sm:col-span-3 lg:col-span-2"> - <label for="postal-code" class="block text-sm font-medium text-gray-700">ZIP / Postal - code</label> - <input type="text" name="postal-code" id="postal-code" autocomplete="postal-code" - class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"> + <!-- Footer --> + <div class="px-4 py-4 sm:px-6 flex"> + <a href="/developers/applications/new" class="ml-auto inline-flex items-center px-3 py-2 border border-transparent shadow-sm text-sm leading-4 font-medium rounded-md text-white bg-emerald-600 hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-emerald-500"> + Create An Application + <svg class="ml-3 -mr-1 h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd" /> + </svg> + </a> </div> </div> </div> - <div class="px-4 py-3 bg-gray-50 text-right sm:px-6"> - <button type="submit" - class="bg-indigo-600 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> - Save - </button> - </div> </div> - </form> - - <form action="#" method="POST"> - <div class="shadow sm:rounded-md sm:overflow-hidden"> - <div class="bg-white py-6 px-4 space-y-6 sm:p-6"> - <div> - <h3 class="text-lg leading-6 font-medium text-gray-900">Notifications</h3> - <p class="mt-1 text-sm text-gray-500">Provide basic informtion about the job. Be specific with - the job title.</p> - </div> - - <fieldset> - <legend class="text-base font-medium text-gray-900">By Email</legend> - <div class="mt-4 space-y-4"> - <div class="flex items-start"> - <div class="h-5 flex items-center"> - <input id="comments" name="comments" type="checkbox" - class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"> - </div> - <div class="ml-3 text-sm"> - <label for="comments" class="font-medium text-gray-700">Comments</label> - <p class="text-gray-500">Get notified when someones posts a comment on a - posting.</p> - </div> - </div> - <div> - <div class="flex items-start"> - <div class="h-5 flex items-center"> - <input id="candidates" name="candidates" type="checkbox" - class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"> - </div> - <div class="ml-3 text-sm"> - <label for="candidates" class="font-medium text-gray-700">Candidates</label> - <p class="text-gray-500">Get notified when a candidate applies for a job.</p> - </div> - </div> - </div> - <div> - <div class="flex items-start"> - <div class="h-5 flex items-center"> - <input id="offers" name="offers" type="checkbox" - class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"> - </div> - <div class="ml-3 text-sm"> - <label for="offers" class="font-medium text-gray-700">Offers</label> - <p class="text-gray-500">Get notified when a candidate accepts or rejects an - offer.</p> - </div> - </div> - </div> - </div> - </fieldset> - <fieldset class="mt-6"> - <legend class="text-base font-medium text-gray-900">Push Notifications</legend> - <p class="text-sm text-gray-500">These are delivered via SMS to your mobile phone.</p> - <div class="mt-4 space-y-4"> - <div class="flex items-center"> - <input id="push-everything" name="push-notifications" type="radio" - class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300"> - <label for="push-everything" class="ml-3"> - <span class="block text-sm font-medium text-gray-700">Everything</span> - </label> - </div> - <div class="flex items-center"> - <input id="push-email" name="push-notifications" type="radio" - class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300"> - <label for="push-email" class="ml-3"> - <span class="block text-sm font-medium text-gray-700">Same as email</span> - </label> - </div> - <div class="flex items-center"> - <input id="push-nothing" name="push-notifications" type="radio" - class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300"> - <label for="push-nothing" class="ml-3"> - <span class="block text-sm font-medium text-gray-700">No push notifications</span> - </label> - </div> - </div> - </fieldset> - </div> - <div class="px-4 py-3 bg-gray-50 text-right sm:px-6"> - <button type="submit" - class="bg-indigo-600 border border-transparent rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-600"> - Save - </button> - </div> - </div> - </form> + </main> </div> </div> + </body> </html> diff --git a/views/developers/token/new.hbs b/views/developers/token/new.hbs new file mode 100644 index 0000000000000000000000000000000000000000..62a3bccf03f27207086b1fb0cc2dfaf88787d854 --- /dev/null +++ b/views/developers/token/new.hbs @@ -0,0 +1,101 @@ +<!DOCTYPE html> + +<html class="h-full bg-gray-100" lang="en"> +<head> + <title>New Server Token | Jetsam</title> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + <meta name="description" content=""> + <meta name="author" content=""> + <!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css">--> + <script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio"></script> + <script> + tailwind.config = { + theme: { + extend: { + colors: { + jetsam: '#FF5E82', + 'jetsam-dark': '#FF1D3C', + 'jetsam-light': '#FF7BAC', + } + } + } + } + </script> + + <style> + body, body > div { + height: 100%; + } + </style> +</head> +<body class="h-full"> + +<div class="flex flex-col md:flex-row"> + + {{> developer-sidebar }} + + <div class="flex flex-col flex-1"> + <main class="flex-1"> + <div class="py-6"> + <div class="max-w-7xl mx-auto px-4 sm:px-6 md:px-8 space-y-4"> + <!-- Content --> + <div class="bg-white shadow sm:rounded-lg"> + <div class="px-4 py-5 sm:p-6"> + + <h3 class="text-lg leading-6 font-medium text-gray-900">Create your token</h3> + + <div class="mt-2 text-sm text-gray-500 bg-red-200 p-4 border-radius-8"> + <p><span class="font-bold">Remember:</span> A server token acts just like you've logged in to Jetsam as yourself, giving access to your account. + It should be treated with as much care as any other secret key or password.</p> + </div> + + <form class="mt-5 flex flex-col space-y-3" method="post" action="/developers/tokens"> + + <div> + <label for="name" class="block text-sm font-medium text-gray-700">Token Name</label> + <div class="mt-1"> + <input + type="text" + name="name" + id="name" + minlength="1" + maxlength="50" + required + class="shadow-sm focus:ring-jetsam focus:border-jetsam block w-full sm:text-sm border-gray-300 rounded-md" + placeholder="Litterbug 9000" + aria-describedby="name-description"> + </div> + <p class="mt-2 text-sm text-gray-500" id="name-description">A short name to identify the token at a glance</p> + </div> + + <div> + <label for="description" class="block text-sm font-medium text-gray-700">Token Description</label> + <div class="mt-1"> + <input + type="text" + name="description" + id="description" + maxlength="200" + class="shadow-sm focus:ring-jetsam focus:border-jetsam block w-full sm:text-sm border-gray-300 rounded-md" + placeholder="Tracking litter, left right up and down" + aria-describedby="description-description"> + </div> + <p class="mt-2 text-sm text-gray-500" id="description-description">A short description that can remind you about the use of this token</p> + </div> + + <button type="submit" + class="mt-3 w-full inline-flex items-center justify-center px-4 py-2 border border-transparent shadow-sm font-medium rounded-md text-white bg-emerald-600 hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-emerald-500 sm:mt-0 sm:w-auto sm:text-sm"> + Save + </button> + </form> + </div> + </div> + </div> + </div> + </main> + </div> +</div> + +</body> +</html> diff --git a/views/helpers/bootstrap.js b/views/helpers/bootstrap.js index 4376c1d25b9dfd302300d6a6c9f21d183c93589c..1c1e3c219e662a3918b3538360be7d90f5f39d0a 100644 --- a/views/helpers/bootstrap.js +++ b/views/helpers/bootstrap.js @@ -40,6 +40,17 @@ exports.ifconf = function(name, fallback, options) { } } +exports.isroute = function(path, options) { + const actualPath = options.data.root.ctx.path + + if (path === actualPath) { + return options.fn(this) + } else { + return options.inverse(this) + } + +} + exports.current_year = function() { const date = new Date() return new Handlebars.SafeString(String(date.getFullYear())) diff --git a/views/partials/developer-sidebar.hbs b/views/partials/developer-sidebar.hbs new file mode 100644 index 0000000000000000000000000000000000000000..fc43e6c9c5bdacd2146390222b9ced1f059fdba0 --- /dev/null +++ b/views/partials/developer-sidebar.hbs @@ -0,0 +1,22 @@ +<div class="w-full h-max md:h-full flex-shrink-0 md:w-64 flex flex-col md:sticky md:top-0 bg-jetsam p-4 md:pt-5"> + + <div class="hidden md:flex items-center flex-shrink-0 px-4 mb-4"> + <img class="aspect-square w-full max-w-4 mx-auto" src="/logo.svg" alt="Jetsam"> + </div> + + <a href="/developers" class="{{#isroute "/developers"}}bg-jetsam-dark {{/isroute}}text-white hover:bg-jetsam-light hover:bg-opacity-75 group flex items-center px-2 py-2 text-sm font-medium rounded-md"> + <!-- Heroicon name: outline/users --> + <span class="mr-auto"> + Developer Settings + </span> + <svg class="mr-3 flex-shrink-0 h-6 w-6 text-emerald-300" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="white" aria-hidden="true"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /> + </svg> + </a> + + <a href="/logout" class="text-white hover:bg-jetsam-light hover:bg-opacity-75 group flex items-center px-2 py-2 md:px-5 md:py-5 text-sm font-medium rounded-md mt-auto md:border md:border-white"> + <span class="mx-auto"> + Log Out + </span> + </a> +</div> \ No newline at end of file