Skip to content

Commit

Permalink
Fetch layout and keymap from a github linked github repository
Browse files Browse the repository at this point in the history
  • Loading branch information
nickcoutsos committed Oct 2, 2021
1 parent 25fd989 commit 4a111c2
Show file tree
Hide file tree
Showing 9 changed files with 571 additions and 29 deletions.
5 changes: 5 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
GITHUB_APP_ID=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GITHUB_OAUTH_CALLBACK_URL=
APP_BASE_URL=
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,7 @@ typings/

dist
qmk_firmware
zmk-config
zmk-config

private-key.pem
.env
1 change: 1 addition & 0 deletions api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const app = express()
app.use(bodyParser.json())
applicationInit(app)
app.use(keyboards)
app.use(require('./routes/github'))

module.exports = app

83 changes: 83 additions & 0 deletions api/routes/github.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
const { Router } = require('express')

const {
getOauthToken,
getOauthUser,
getUserToken,
verifyUserToken,
fetchInstallation,
fetchInstallationRepos,
fetchKeyboardFiles,
createOauthFlowUrl,
createOauthReturnUrl
} = require('../services/github')

const router = Router()

const authorize = async (req, res) => {
if (req.query.code) {
const { data: oauth } = await getOauthToken(req.query.code)
const { data: user } = await getOauthUser(oauth.access_token)
const token = getUserToken(oauth, user)
res.redirect(createOauthReturnUrl(token))
} else {
res.redirect(createOauthFlowUrl())
}
}

const authenticate = (req, res, next) => {
const header = req.headers.authorization
const token = (header || '').split(' ')[1]

if (!token) {
return res.sendStatus(401)
}

try {
req.user = verifyUserToken(token)
} catch (err) {
console.error('Failed to verify token', err)
return res.sendStatus(401)
}

next()
}

const getInstallation = async (req, res) => {
const { user } = req

try {
const { data: installation } = await fetchInstallation(user.sub)

if (!installation) {
return res.json({ installation: null })
}

const { data: { repositories } } = await fetchInstallationRepos(user.oauth_access_token, installation.id)

res.json({ installation, repositories })
} catch (err) {
const message = err.response ? err.response.data : err
console.error(message)
res.status(500).json(message)
}
}

const getKeyboardFiles = async (req, res) => {
const { installationId, repository } = req.params

try {
const keyboardFiles = await fetchKeyboardFiles(installationId, repository)
res.json(keyboardFiles)
} catch (err) {
const message = err.response ? err.response.data : err
console.error(message)
res.status(500).json(message)
}
}

router.get('/github/authorize', authorize)
router.get('/github/installation', authenticate, getInstallation)
router.get('/github/keyboard-files/:installationId/:repository', authenticate, getKeyboardFiles)

module.exports = router
149 changes: 149 additions & 0 deletions api/services/github/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
require('dotenv/config')
const fs = require('fs')
const path = require('path')

const axios = require('axios')
const jwt = require('jsonwebtoken')

const pemPath = path.join(__dirname, '..', '..', '..', 'private-key.pem')
const privateKey = fs.readFileSync(pemPath)

function createAppToken () {
return jwt.sign({ iss: process.env.GITHUB_APP_ID }, privateKey, {
algorithm: 'RS256',
expiresIn: '10m'
})
}

function apiRequest (url, token, method='GET') {
const headers = { Accept: 'application/vnd.github.v3+json' }
if (token) {
headers.Authorization = `Bearer ${token}`
}

return axios({ url, method, headers })
}

function createOauthFlowUrl () {
const redirectUrl = new URL('https://github.com/login/oauth/authorize')

redirectUrl.search = new URLSearchParams({
client_id: process.env.GITHUB_CLIENT_ID,
redirect_uri: process.env.GITHUB_OAUTH_CALLBACK_URL,
state: 'foo'
}).toString()

return redirectUrl.toString()
}

function createOauthReturnUrl (token) {
const url = new URL(process.env.APP_BASE_URL)
url.search = new URLSearchParams({ token }).toString()
return url.toString()
}

function getUserToken (oauth, user) {
return jwt.sign({
oauth_access_token: oauth.access_token,
sub: user.login
}, privateKey, {
algorithm: 'RS256'
})
}

function verifyUserToken (token) {
return jwt.verify(token, privateKey, {
algorithms: ['RS256']
})
}

function fetchInstallation (user) {
const token = createAppToken()
return axios({
method: 'GET',
url: `https://api.github.com/users/${user}/installation`,
headers: {
Accept: 'application/vnd.github.v3.raw',
Authorization: `Bearer ${token}`
}
}).catch(err => {
if (err.response && err.response.status === 404) {
return { data: null }
}

throw err
})
}

function fetchInstallationRepos (token, installationId) {
return axios({
method: 'GET',
url: `https://api.github.com/user/installations/${installationId}/repositories`,
headers: {
Accept: 'application/vnd.github.v3.raw',
Authorization: `Bearer ${token}`
}
})
}

function getOauthToken (code) {
return axios({
method: 'POST',
url: 'https://github.com/login/oauth/access_token',
headers: {
Accept: 'application/json'
},
data: {
client_id: process.env.GITHUB_CLIENT_ID,
client_secret: process.env.GITHUB_CLIENT_SECRET,
code
}
})
}

function getOauthUser (token) {
return axios({
method: 'GET',
url: 'https://api.github.com/user',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
}
})
}

async function fetchKeyboardFiles (installationId, repository) {
const token = createAppToken()
const accessTokensUrl = `https://api.github.com/app/installations/${installationId}/access_tokens`
const contentsUrl = `https://api.github.com/repos/${repository}/contents`

const { data: { token: installationToken } } = await apiRequest(accessTokensUrl, token, 'POST')
const { data: info } = await axios({
url: `${contentsUrl}/config/info.json`,
headers: {
Accept: 'application/vnd.github.v3.raw',
Authorization: `Bearer ${installationToken}`
}
})
const { data: keymap } = await axios({
url: `${contentsUrl}/config/keymap.json`,
headers: {
Accept: 'application/vnd.github.v3.raw',
Authorization: `Bearer ${installationToken}`
}
})

return { info, keymap }
}

module.exports = {
createOauthFlowUrl,
createOauthReturnUrl,
getOauthToken,
getOauthUser,
getUserToken,
verifyUserToken,
fetchInstallation,
fetchInstallationRepos,
fetchKeyboardFiles
}
8 changes: 0 additions & 8 deletions application/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,9 @@ export function loadKeycodes() {
export function loadKeymap() {
return fetch(`/keymap?firmware=${config.library}`)
.then(response => response.json())
.then(keymap => Object.assign(keymap, {
layer_names: keymap.layer_names || keymap.layers.map((_, i) => `Layer ${i}`)
}))
}

export function loadLayout() {
return fetch(`/layout?firmware=${config.library}`)
.then(response => response.json())
.then(layout => (
layout.map(key => (
{ ...key, u: key.u || key.w || 1, h: key.h || 1 }
))
))
}
89 changes: 71 additions & 18 deletions application/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,70 @@ import App from './components/app.vue'
import { loadKeymap } from './keymap.js'
import { loadLayout } from './layout.js'

let installation
let repositories

async function init () {
const token = new URLSearchParams(location.search).get('token')
if (!localStorage.auth_token && token) {
history.replaceState({}, null, '/application')
localStorage.auth_token = token
}

if (localStorage.auth_token) {
const token = localStorage.auth_token

const data = await fetch(`http://localhost:8080/github/installation`, {
headers: {
Authorization: `Bearer ${token}`
}
}).then(res => res.json())

if (!data.installation) {
console.log('no installation found for authenticated user')
location.href = 'https://github.com/apps/zmk-keymap-editor-dev/installations/new'
}

installation = data.installation
repositories = data.repositories

const logout = document.createElement('button')
logout.setAttribute('style', 'display: inline-block; z-index: 100; position: absolute; right: 0px')
logout.textContent = 'Logout'
logout.addEventListener('click', () => {
localStorage.removeItem('auth_token')
location.reload()
})

document.body.appendChild(logout)
} else {
const login = document.createElement('button')
login.setAttribute('style', 'display: inline-block; z-index: 100; position: absolute; right: 0px')
login.textContent = 'Login'
login.addEventListener('click', () => {
localStorage.removeItem('auth_token')
location.href = 'http://localhost:8080/github/authorize'
})

document.body.appendChild(login)
}
}

async function main() {
const layout = await loadLayout()
const keymap = await loadKeymap()
let layout, keymap
await init()

if (installation && repositories[0]) {
const data = await fetch(
`http://localhost:8080/github/keyboard-files/${encodeURIComponent(installation.id)}/${encodeURIComponent(repositories[0].full_name)}`,
{ headers: { Authorization: `Bearer ${localStorage.auth_token}`} }
).then(res => res.json())
layout = data.info.layouts.LAYOUT.layout
keymap = data.keymap
} else {
layout = await loadLayout()
keymap = await loadKeymap()
}

const app = Vue.createApp(App)
const vm = app.mount('#app')
Expand All @@ -22,24 +83,16 @@ async function main() {

setInterval(() => socket.send('ping'), 10000)

vm.keymap = keymap
vm.layout = layout
vm.layers = keymap.layers
vm.socket = socket

// document.querySelector('#export').addEventListener('click', () => {
// const keymap = buildKeymap()
// const file = new File([JSON.stringify(keymap, null, 2)], 'default.json', {
// type: 'application/octet-stream'
// })

// location.href = URL.createObjectURL(file)
// })
vm.keymap = Object.assign(keymap, {
layer_names: keymap.layer_names || keymap.layers.map((_, i) => `Layer ${i}`)
})

// document.querySelector('#compile').addEventListener('click', () => compile())
// document.querySelector('#flash').addEventListener('click', () => compile({ flash: true }))
vm.layout = layout.map(key => (
{ ...key, u: key.u || key.w || 1, h: key.h || 1 }
))

// document.querySelector('#toggle').addEventListener('click', () => toggleTerminal())
vm.layers = keymap.layers
vm.socket = socket
}

main()
Loading

0 comments on commit 4a111c2

Please sign in to comment.