Skip to content

Commit 143b0cb

Browse files
committed
added documentation to README, and some examples
1 parent 7b14034 commit 143b0cb

File tree

4 files changed

+290
-1
lines changed

4 files changed

+290
-1
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,5 @@ typings/
6060
# next.js build output
6161
.next
6262

63-
tokens.json
63+
tokens.json
64+
grades.json

README.md

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,155 @@
11
# khan-api-wrapper
22
A simple wrapper around the Khan Academy API for use in node
3+
4+
5+
------------------------------------------------
6+
7+
## About
8+
This is a simple implementation of using the Khan Academy API with node. If you
9+
are only interested in viewing your personal or students' data, you can use it without a browser by calling the `authorizeSelf` method. This will use the alternative method
10+
of logging in with your own account. This also supports browser based authentication
11+
for use with a node server like [Express](https://expressjs.com/).
12+
13+
#### Dependencies
14+
* `axios` to handle http requests
15+
* `oauth-1.0a` to create the oauth params and signature
16+
17+
#### Set up:
18+
19+
Install:
20+
21+
```
22+
$ yarn add khan-api-wrapper
23+
```
24+
25+
or
26+
27+
```
28+
$ npm install khan-api-wrapper
29+
```
30+
31+
32+
[Register your app with Khan Academy](https://www.khanacademy.org/api-apps/register), to get the necessary tokens. That is it, you should now be able to use the wrapper in your node application.
33+
34+
#### General use:
35+
Without a browser:
36+
37+
```javascript
38+
39+
const { KhanOauth, KhanAPIWrapper } = requires("khan-api-wrapper")
40+
41+
// Config variables
42+
const KHAN_PASSWORD = "password_of_account_used_to_register_app"
43+
const KHAN_IDENTIFIER = "username_of_account_used_to_register_app"
44+
const KHAN_CONSUMER_SECRET = "Secret from registering app"
45+
const KHAN_CONSUMER_KEY = "Key from registering app"
46+
47+
// Instantiate the oauth class that will be used to get the authentication tokens
48+
const kauth = new KhanOauth(
49+
KHAN_CONSUMER_KEY,
50+
KHAN_CONSUMER_SECRET,
51+
KHAN_IDENTIFIER,
52+
KHAN_PASSWORD
53+
)
54+
55+
// First get tokens that can be used to access protected data
56+
kauth.authorizeSelf()
57+
.then(async ([token, secret]) => {
58+
// With fresh tokens, we now instantiate the wrapper
59+
const kapi = new KhanAPIWrapper(
60+
KHAN_CONSUMER_KEY,
61+
KHAN_CONSUMER_SECRET,
62+
token,
63+
secret
64+
)
65+
66+
// Use a convenience method to fetch the user. Check out the details in
67+
// "./lib/api-v1.js"
68+
const user = await kapi.user()
69+
console.log(user) // should see your user metadata
70+
71+
// Use an undocumented endpoint
72+
const missionUrl = "/api/internal/user/missions"
73+
const missions = await kapi.fetchResource(missionUrl, true)
74+
console.log(missions) // should show your missions
75+
})
76+
```
77+
78+
The available helper methods can be found in `./lib/api-v1.js` and `./lib/api-v2.js`.
79+
80+
Checkout the `examples` folder for ideas on how to use in your application.
81+
82+
#### Token freshness:
83+
84+
Through trial I have discovered that the access token and secret are valid for 2 weeks. So you may consider storing them in a separate file or database, and write a function to only fetch tokens if they are expired.
85+
86+
```javascript
87+
const fs = require("fs")
88+
const { promisify } = require("util")
89+
const readAsync = promisify(fs.readFile)
90+
const writeAsync = promisify(fs.writeFile)
91+
92+
const getFreshTokens = async () => {
93+
const kauth = new KhanOauth(
94+
KHAN_CONSUMER_KEY,
95+
KHAN_CONSUMER_SECRET,
96+
KHAN_IDENTIFIER,
97+
KHAN_PASSWORD
98+
)
99+
100+
// get fresh tokens from Khan Academy.
101+
const [token, secret] = await kauth.authorizeSelf()
102+
103+
// Save those tokens to the disk, and return them
104+
return await writeAsync(
105+
"tokens.json",
106+
JSON.stringify({
107+
token,
108+
secret,
109+
timestamp: new Date().getTime(),
110+
}),
111+
{ encoding: "utf8" }
112+
)
113+
.then(() => [token, secret])
114+
.catch(err => {
115+
throw err
116+
})
117+
}
118+
119+
const getAccessTokens = async () => {
120+
// Fetch token data from saved json file
121+
return await readAsync("tokens.json", { encoding: "utf8" })
122+
.then(jsonString => JSON.parse(jsonString))
123+
.then(({ token, secret, timestamp }) => {
124+
// Check if tokens are expired
125+
const now = new Date().getTime()
126+
if (now - timestamp > 14 * 24 * 3600 * 1000) {
127+
return getFreshTokens()
128+
}
129+
130+
// Otherwise just return valid tokens from disk
131+
return [token, secret]
132+
})
133+
.catch(err => {
134+
if (err.code === "ENOENT") {
135+
// file not found, which should happen the first time
136+
return getFreshTokens()
137+
}
138+
139+
throw err
140+
})
141+
}
142+
143+
// Then use the function to ensure we only use fresh tokens when necessary
144+
getAccessTokens()
145+
.then(([token, secret]) => {
146+
const kapi = new KhanAPIWrapper(
147+
KHAN_CONSUMER_KEY,
148+
KHAN_CONSUMER_SECRET,
149+
token,
150+
secret
151+
)
152+
153+
...
154+
})
155+
```

examples/browerless.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
const fs = require("fs")
2+
const { promisify } = require("util")
3+
const writeAsync = promisify(fs.writeFile)
4+
const { KhanOauth, KhanAPIWrapper } = require("khan-api-wrapper")
5+
6+
// Config variables. Fill these in with the correct data to make the example work.
7+
const KHAN_PASSWORD = "password_of_account_used_to_register_app"
8+
const KHAN_IDENTIFIER = "username_of_account_used_to_register_app"
9+
const KHAN_CONSUMER_SECRET = "Secret from registering app"
10+
const KHAN_CONSUMER_KEY = "Key from registering app"
11+
const TEST_USERNAME = "username of your student"
12+
13+
// Instantiate the oauth class that will be used to get the authentication tokens
14+
const kauth = new KhanOauth(
15+
KHAN_CONSUMER_KEY,
16+
KHAN_CONSUMER_SECRET,
17+
KHAN_IDENTIFIER,
18+
KHAN_PASSWORD
19+
)
20+
21+
// First get tokens that can be used to access protected data
22+
kauth.authorizeSelf().then(async ([token, secret]) => {
23+
// With fresh tokens, we now instantiate the wrapper
24+
const kapi = new KhanAPIWrapper(
25+
KHAN_CONSUMER_KEY,
26+
KHAN_CONSUMER_SECRET,
27+
token,
28+
secret
29+
)
30+
31+
// Get user exercise data for one of my students
32+
const exerciseData = await kapi.userExercisesName("addition_1", {
33+
username: TEST_USERNAME,
34+
})
35+
36+
const grades = {
37+
practiced: 70,
38+
mastery1: 80,
39+
mastery2: 90,
40+
mastery3: 100,
41+
}
42+
43+
const masteryLevel = exerciseData.maximum_exercise_progress.level
44+
45+
await syncGradeToGradebook({
46+
username: TEST_USERNAME,
47+
skill: "addition_1",
48+
grade: grades[masteryLevel],
49+
})
50+
})
51+
52+
// A simple example that creates a gradebook file. In production you probably
53+
// have another API that can be called to sync the grades into your LMS gradebook.
54+
const syncGradeToGradebook = async gradeObj =>
55+
await writeAsync("grades.json", JSON.stringify(gradeObj), {
56+
encoding: "utf8",
57+
}).catch(err => {
58+
throw err
59+
})

examples/with-express.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
const express = require("express")
2+
const session = require("express-session") // we will store the tokens in a session
3+
const { KhanOauth, KhanAPIWrapper } = require("khan-api-wrapper")
4+
5+
// Config variables. Fill these in with the correct data to make the example work.
6+
const KHAN_CONSUMER_SECRET = "Secret from registering app"
7+
const KHAN_CONSUMER_KEY = "Key from registering app"
8+
9+
// Set up the express app
10+
const app = express()
11+
app.use(express.json())
12+
app.use(express.urlencoded({ extended: false }))
13+
app.use(
14+
session({
15+
secret: "change me for production use",
16+
resave: false,
17+
saveUninitialized: false,
18+
})
19+
)
20+
21+
const kauth = new KhanOauth(KHAN_CONSUMER_KEY, KHAN_CONSUMER_SECRET)
22+
23+
app.get("/", async (req, res) => {
24+
// When first accessing the page, we will check for tokens to determine if
25+
// user needs to login
26+
if (!req.session.tokens) {
27+
res.redirect("/login")
28+
} else {
29+
const [token, secret] = req.session.tokens
30+
const kapi = new KhanAPIWrapper(
31+
KHAN_CONSUMER_KEY,
32+
KHAN_CONSUMER_SECRET,
33+
token,
34+
secret
35+
)
36+
const user = await kapi.user()
37+
res.send(`<!doctype html>
38+
<head></head>
39+
<body>
40+
<h1>Welcome ${user.nickname}</h1>
41+
<h3>Checkout your protected user data from Khan Academy:</h3>
42+
<pre style="background: #ddd;">
43+
${JSON.stringify(user, null, 2)}
44+
</pre>
45+
</body>
46+
`)
47+
}
48+
})
49+
50+
app.get("/login", (req, res) => {
51+
// Note that this callbackUrl corresponds to a route that will be defined
52+
// later, and that will handle setting the fresh tokens into the session
53+
54+
const callBackUrl = `${req.protocol}://${req.get("host")}/authenticate_khan`
55+
kauth.authorize(res, callBackUrl)
56+
})
57+
58+
app.get("/authenticate_khan", async (req, res) => {
59+
// This is the route that Khan Academy will return to after the user
60+
// has given permission in the browser. It is the callbackUrl defined in the
61+
// login route.
62+
const { oauth_token_secret, oauth_verifier, oauth_token } = req.query
63+
64+
const [token, secret] = await kauth.getAccessTokens(
65+
oauth_token,
66+
oauth_token_secret,
67+
oauth_verifier
68+
)
69+
70+
req.session.tokens = [token, secret]
71+
72+
res.redirect("/")
73+
})
74+
75+
const port = process.env.PORT || 4000
76+
app.listen(port, () => console.log(`🚀 Server ready at ${port}`))

0 commit comments

Comments
 (0)