Skip to content

Commit 3ad529a

Browse files
committed
Add wrapper scripts to make import and status checking easier.
1 parent eb5bff3 commit 3ad529a

File tree

4 files changed

+552
-7
lines changed

4 files changed

+552
-7
lines changed

docs/import/school_import_csm_guide.md

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22

33
## Prerequisites
44

5-
1. You must have the `experience-cs-admin` or `profile-admin` role
6-
2. School owners must have existing Code Editor for Education accounts
5+
1. You must have the `experience-cs-admin` or `profile-admin` role in Editor-API.
6+
2. School owners must have existing Code Editor for Education accounts.
77
3. School owners must be unique within the CSV, and in the existing database.
88
4. CSV file must be properly formatted (see template)
99

1010
## Step-by-Step Process
1111

1212
### 1. Prepare the CSV File
1313

14-
Download the template: `docs/school_import_template.csv`
14+
Download the template: `docs/import/school_import_template.csv`
1515

1616
Required columns:
1717
- `name` - School name
@@ -33,6 +33,20 @@ Before importing, verify that all owner emails in your CSV correspond to existin
3333

3434
### 3. Upload the CSV
3535

36+
**Prerequisite**
37+
38+
In order to use the scripts, you have to have an OAuth token from `editor-api`.
39+
40+
TODO: Explain how to get such a token, or write a tool to do it.
41+
42+
You can supply this token to the scripts mentioned below by:
43+
44+
```bash
45+
export TOKEN=ory_ac_a2i-3ayKjE_8YcGqRlGXdaKrZ4yWkSdfG6vwQmLEsMg.bUdeff5re2vDLd6kRffaGp8NNX6ry8Yzm7wf7aaMKWM
46+
```
47+
48+
Obviously, don't use this value - use your own.
49+
3650
**Via API:**
3751

3852
```bash
@@ -50,10 +64,46 @@ curl -X POST https://editor-api.raspberrypi.org/api/schools/import \
5064
}
5165
```
5266

67+
**Via Script:**
68+
69+
```bash
70+
$ $scripts/import.sh ./docs/import/school_import_template.csv
71+
════════════════════════════════════════════════════════════════
72+
School Import
73+
════════════════════════════════════════════════════════════════
74+
75+
CSV File: ./docs/import/school_import_template.csv
76+
Schools to Import: 3
77+
API URL: http://localhost:3009/api/schools/import
78+
79+
Starting import...
80+
81+
════════════════════════════════════════════════════════════════
82+
✓ Import Job Started Successfully
83+
════════════════════════════════════════════════════════════════
84+
85+
Job ID: 8a24adf7-c705-451a-8bb3-051ec5d8fdd8
86+
Total Schools: 3
87+
Status: Import job started successfully
88+
89+
────────────────────────────────────────────────────────────────
90+
Next Steps
91+
────────────────────────────────────────────────────────────────
92+
93+
Check import status with:
94+
./scripts/status.sh 8a24adf7-c705-451a-8bb3-051ec5d8fdd8
95+
96+
Or manually with curl:
97+
curl -sS -H "Authorization: $TOKEN" \
98+
http://localhost:3009/api/school_import_jobs/8a24adf7-c705-451a-8bb3-051ec5d8fdd8 | jq '.'
99+
```
100+
53101
### 4. Track Progress
54102

55103
Use the job_id from the response:
56104

105+
**Via API**
106+
57107
```bash
58108
curl https://editor-api.raspberrypi.org/api/school_import_jobs/550e8400-e29b-41d4-a716-446655440000 \
59109
-H "Authorization: YOUR_TOKEN"
@@ -85,9 +135,13 @@ curl https://editor-api.raspberrypi.org/api/school_import_jobs/550e8400-e29b-41d
85135
}
86136
```
87137

88-
### 5. Review Results
138+
**Via Script**
89139

90-
Once the job completes, the results will show:
140+
```bash
141+
[f@rpi] ➜ editor-api (U!@ fs-implement-school-import-endpoint) ./scripts/status.sh 8a24adf7-c705-451a-8bb3-051ec5d8fdd8
142+
════════════════════════════════════════════════════════════════
143+
School Import Job Status
144+
════════════════════════════════════════════════════════════════
91145

92146
```
93147
Job ID: 8a24adf7-c705-451a-8bb3-051ec5d8fdd8
@@ -117,7 +171,7 @@ Capital City Academy 32-91-93 headteacher@capital-city-a
117171
════════════════════════════════════════════════════════════════
118172
```
119173
120-
### 6. Handle Failures
174+
### 5. Handle Failures
121175
122176
Common failure reasons and solutions:
123177
@@ -245,5 +299,5 @@ If you encounter issues:
245299
6. Review results: 148 succeeded, 2 failed
246300
7. Fix issues with 2 failed schools (duplicate references)
247301
8. Create those 2 schools manually or re-import
248-
9. Notify district admin that all schools are ready
302+
9. Notify district admin that all schools are ready and supply the generated codes
249303
10. District admin can now invite teachers to their schools

scripts/get_oauth_token.rb

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
# Script to obtain an OAuth token by going through the same login flow
5+
# that /admin uses. This will open a browser to authenticate and then
6+
# start a local server to capture the callback.
7+
#
8+
# NOTE: This script listens on port 3009 (same as editor-api) at /auth/callback.
9+
# Make sure the editor-api server is NOT running when you use this script.
10+
11+
require 'socket'
12+
require 'uri'
13+
require 'net/http'
14+
require 'json'
15+
require 'securerandom'
16+
require 'dotenv'
17+
18+
# Load environment variables from .env file
19+
Dotenv.load(File.expand_path('../.env', __dir__))
20+
21+
# Configuration
22+
HYDRA_URL = ENV.fetch('HYDRA_PUBLIC_URL', 'http://localhost:9001')
23+
CLIENT_ID = ENV.fetch('HYDRA_CLIENT_ID', 'editor-dashboard-dev')
24+
CLIENT_SECRET = ENV.fetch('HYDRA_CLIENT_SECRET', 'secret')
25+
HOST_URL = ENV.fetch('HOST_URL', 'http://localhost:3009')
26+
CALLBACK_PORT = 3009
27+
CALLBACK_URL = "#{HOST_URL}/auth/callback".freeze
28+
29+
# Disable some Rails rubocop rules for this script since it's not part of the app
30+
# and a very temporary script.
31+
# rubocop:disable Rails/Output, Rails/Exit, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/ParameterLists
32+
# Simple HTTP server using raw sockets
33+
def start_callback_server(port, state, hydra_url, client_id, client_secret, callback_url)
34+
server = TCPServer.new(port)
35+
36+
loop do
37+
client = server.accept
38+
request = client.gets
39+
break unless request
40+
41+
# Parse request line
42+
method, path_with_query, _version = request.split
43+
next unless method == 'GET' && path_with_query.start_with?('/auth/callback')
44+
45+
# Parse query string
46+
uri = URI.parse("http://localhost#{path_with_query}")
47+
query_params = URI.decode_www_form(uri.query || '').to_h
48+
49+
# Handle errors
50+
if query_params['error']
51+
html = <<~HTML
52+
<html><body>
53+
<h1>Authentication Error</h1>
54+
<p>Error: #{query_params['error']}</p>
55+
<p>Description: #{query_params['error_description']}</p>
56+
<p>You can close this window.</p>
57+
</body></html>
58+
HTML
59+
send_response(client, '400 Bad Request', html)
60+
puts "\nError: #{query_params['error']}"
61+
puts "Description: #{query_params['error_description']}"
62+
return nil
63+
end
64+
65+
# Check state
66+
if query_params['state'] != state
67+
html = '<html><body><h1>State mismatch error</h1><p>You can close this window.</p></body></html>'
68+
send_response(client, '400 Bad Request', html)
69+
puts "\nError: State mismatch"
70+
return nil
71+
end
72+
73+
# Get authorization code
74+
code = query_params['code']
75+
unless code
76+
html = '<html><body><h1>No authorization code received</h1><p>You can close this window.</p></body></html>'
77+
send_response(client, '400 Bad Request', html)
78+
puts "\nError: No authorization code received"
79+
return nil
80+
end
81+
82+
# Exchange code for token
83+
token_url = URI("#{hydra_url}/oauth2/token")
84+
token_request = Net::HTTP::Post.new(token_url)
85+
token_request.basic_auth(client_id, client_secret)
86+
token_request.set_form_data(
87+
'grant_type' => 'authorization_code',
88+
'code' => code,
89+
'redirect_uri' => callback_url
90+
)
91+
92+
begin
93+
token_response = Net::HTTP.start(token_url.hostname, token_url.port) do |http|
94+
http.request(token_request)
95+
end
96+
97+
if token_response.code == '200'
98+
token_data = JSON.parse(token_response.body)
99+
result_token = token_data['access_token']
100+
101+
html = <<~HTML
102+
<html><body>
103+
<h1>Authentication Successful!</h1>
104+
<p>You can close this window and return to your terminal.</p>
105+
</body></html>
106+
HTML
107+
send_response(client, '200 OK', html)
108+
return result_token
109+
else
110+
html = <<~HTML
111+
<html><body>
112+
<h1>Token Exchange Error</h1>
113+
<p>Status: #{token_response.code}</p>
114+
<p>#{token_response.body}</p>
115+
<p>You can close this window.</p>
116+
</body></html>
117+
HTML
118+
send_response(client, '500 Internal Server Error', html)
119+
puts "\nToken exchange error: #{token_response.code}"
120+
puts token_response.body
121+
return nil
122+
end
123+
rescue StandardError => e
124+
html = <<~HTML
125+
<html><body>
126+
<h1>Error</h1>
127+
<p>#{e.message}</p>
128+
<p>You can close this window.</p>
129+
</body></html>
130+
HTML
131+
send_response(client, '500 Internal Server Error', html)
132+
puts "\nError exchanging code for token: #{e.message}"
133+
return nil
134+
end
135+
end
136+
ensure
137+
server&.close
138+
end
139+
140+
def send_response(client, status, html)
141+
client.puts "HTTP/1.1 #{status}"
142+
client.puts 'Content-Type: text/html'
143+
client.puts "Content-Length: #{html.bytesize}"
144+
client.puts
145+
client.puts html
146+
ensure
147+
client&.close
148+
end
149+
150+
# OAuth parameters
151+
state = SecureRandom.hex(16)
152+
153+
puts 'Starting OAuth flow...'
154+
puts "Hydra URL: #{HYDRA_URL}"
155+
puts "Client ID: #{CLIENT_ID}"
156+
puts "Callback URL: #{CALLBACK_URL}"
157+
puts
158+
159+
# Build authorization URL
160+
auth_params = URI.encode_www_form(
161+
'client_id' => CLIENT_ID,
162+
'response_type' => 'code',
163+
'scope' => 'openid email profile roles force-consent',
164+
'redirect_uri' => CALLBACK_URL,
165+
'state' => state
166+
)
167+
auth_url = "#{HYDRA_URL}/oauth2/auth?#{auth_params}"
168+
169+
# Open browser
170+
puts 'Opening browser for authentication...'
171+
puts "If the browser doesn't open automatically, visit:"
172+
puts auth_url
173+
puts
174+
175+
case RbConfig::CONFIG['host_os']
176+
when /darwin/
177+
system("open '#{auth_url}'")
178+
when /linux/
179+
system("xdg-open '#{auth_url}'")
180+
when /mswin|mingw|cygwin/
181+
system("start '#{auth_url}'")
182+
else
183+
puts 'Unable to detect OS to open browser. Please open the URL manually.'
184+
end
185+
186+
# Start server and wait for callback
187+
trap('INT') do
188+
puts "\nShutting down..."
189+
exit 1
190+
end
191+
192+
access_token = start_callback_server(CALLBACK_PORT, state, HYDRA_URL, CLIENT_ID, CLIENT_SECRET, CALLBACK_URL)
193+
194+
# Print token
195+
if access_token
196+
puts "\n#{'=' * 80}"
197+
puts 'ACCESS TOKEN:'
198+
puts '=' * 80
199+
puts access_token
200+
puts '=' * 80
201+
puts "\nYou can use this token with:"
202+
puts " curl -H 'Authorization: #{access_token}' http://localhost:3009/api/projects"
203+
else
204+
puts "\nFailed to obtain access token"
205+
exit 1
206+
end
207+
# rubocop:enable Rails/Output, Rails/Exit, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists

0 commit comments

Comments
 (0)