|
5 | 5 | from pathlib import Path |
6 | 6 | from functools import update_wrapper |
7 | 7 |
|
8 | | -ORG = 'aitomaticinc.us.auth0.com'; |
| 8 | +ORG = 'aitomaticinc.us.auth0.com' |
9 | 9 | CLIENT_ID = "zk9AB0KtNqJY0gVeF1p0ZmUb2tlcXpYq" |
10 | 10 | AUDIENCE = "https://apps.aitomatic.com/dev" |
11 | 11 | SCOPE = "openid profile email offline_access" |
12 | | -CONFIG_FILE = Path.home().joinpath('.aitomatic') |
| 12 | +CREDENTIAL_FILE = Path.home().joinpath('.aitomatic/credentials') |
13 | 13 |
|
14 | | -@click.command(help=''' |
15 | | - Login to Aitomatic cloud |
16 | | -''') |
| 14 | + |
| 15 | +@click.command() |
17 | 16 | @click.pass_obj |
18 | 17 | def login(obj): |
19 | | - if obj.get("at") is not None or CONFIG_FILE.exists(): |
20 | | - relogin = click.confirm("You're logged in. Do you want to log in again?", default=False, abort=False, prompt_suffix=': ', show_default=True, err=False) |
| 18 | + '''Login to Aitomatic cloud''' |
| 19 | + if obj.get("access_token") is not None or CREDENTIAL_FILE.exists(): |
| 20 | + re_login = click.confirm( |
| 21 | + "You're logged in. Do you want to log in again?", |
| 22 | + default=False, |
| 23 | + abort=False, |
| 24 | + prompt_suffix=': ', |
| 25 | + show_default=True, |
| 26 | + err=False, |
| 27 | + ) |
21 | 28 |
|
22 | | - if not relogin: |
| 29 | + if not re_login: |
23 | 30 | exit(0) |
24 | 31 |
|
25 | 32 | do_login() |
26 | 33 |
|
| 34 | + |
27 | 35 | def do_login(): |
28 | 36 | click.echo('Logging into Aitomatic cloud...') |
29 | 37 | device_info = request_device_code() |
30 | 38 | display_device_info(device_info) |
31 | 39 | poll_authentication_status(device_info) |
32 | 40 |
|
| 41 | + |
33 | 42 | def request_device_code(): |
34 | 43 | res = requests.post( |
35 | 44 | url="https://{}/oauth/device/code".format(ORG), |
36 | 45 | data={"client_id": CLIENT_ID, "scope": SCOPE, "audience": AUDIENCE}, |
37 | | - headers={ "content-type": "application/x-www-form-urlencoded" } |
| 46 | + headers={"content-type": "application/x-www-form-urlencoded"}, |
38 | 47 | ) |
39 | 48 |
|
40 | 49 | # see details https://auth0.com/docs/get-started/authentication-and-authorization-flow/call-your-api-using-the-device-authorization-flow#request-device-code |
41 | 50 | # print(device_response) |
42 | 51 | # { |
43 | | - # 'device_code': 'kJ3fIJ90RYXbdAOlhns3v7t3', |
44 | | - # 'user_code': '873280791', |
45 | | - # 'verification_uri': 'https://aitomaticinc.us.auth0.com/activate', |
46 | | - # 'expires_in': 900, |
47 | | - # 'interval': 5, |
| 52 | + # 'device_code': 'kJ3fIJ90RYXbdAOlhns3v7t3', |
| 53 | + # 'user_code': '873280791', |
| 54 | + # 'verification_uri': 'https://aitomaticinc.us.auth0.com/activate', |
| 55 | + # 'expires_in': 900, |
| 56 | + # 'interval': 5, |
48 | 57 | # 'verification_uri_complete': 'https://aitomaticinc.us.auth0.com/activate?user_code=873280791' |
49 | 58 | # } |
50 | 59 |
|
51 | 60 | return res.json() |
52 | 61 |
|
| 62 | + |
53 | 63 | def display_device_info(device_info): |
54 | 64 | code = device_info['user_code'] |
55 | 65 | url = device_info['verification_uri_complete'] |
56 | 66 |
|
57 | | - click.echo(""" |
| 67 | + click.echo( |
| 68 | + """ |
58 | 69 | Please visit: |
59 | 70 | {} |
60 | 71 | to login to Aitomatic cloud. |
61 | 72 |
|
62 | 73 | Verification code: {} |
63 | | - """.format(url, code)) |
| 74 | + """.format( |
| 75 | + url, code |
| 76 | + ) |
| 77 | + ) |
64 | 78 |
|
65 | 79 | click.launch(url) |
66 | | - |
67 | 80 | click.echo("Waiting for authentication...") |
68 | 81 |
|
| 82 | + |
69 | 83 | @click.pass_obj |
70 | 84 | def poll_authentication_status(obj, device_info): |
71 | 85 | res = requests.post( |
72 | 86 | url="https://{}/oauth/token".format(ORG), |
73 | 87 | data={ |
74 | | - "client_id": CLIENT_ID, |
75 | | - "grant_type": "urn:ietf:params:oauth:grant-type:device_code", |
76 | | - "device_code": device_info['device_code'] |
| 88 | + "client_id": CLIENT_ID, |
| 89 | + "grant_type": "urn:ietf:params:oauth:grant-type:device_code", |
| 90 | + "device_code": device_info['device_code'], |
77 | 91 | }, |
78 | | - headers={ "content-type": "application/x-www-form-urlencoded" } |
| 92 | + headers={"content-type": "application/x-www-form-urlencoded"}, |
79 | 93 | ) |
80 | | - |
| 94 | + |
81 | 95 | # response example |
82 | 96 | # {'error': 'authorization_pending', 'error_description': 'User has yet to authorize device code.'} |
83 | 97 | # {"error": "expired_token", "error_description": "..." } |
84 | 98 | # {"error": "access_denied", "error_description": "..." } |
85 | 99 | # {"access_token": "...", "id_token": "...", "refresh_token": "..."} |
86 | | - polling_data = res.json(); |
| 100 | + polling_data = res.json() |
87 | 101 |
|
88 | 102 | if polling_data.get('error') == 'authorization_pending': |
89 | 103 | time.sleep(device_info['interval']) |
90 | 104 | poll_authentication_status(device_info) |
91 | 105 |
|
92 | | - if polling_data.get('error') == 'expired_token' or polling_data.get('error') == 'access_denied': |
| 106 | + if ( |
| 107 | + polling_data.get('error') == 'expired_token' |
| 108 | + or polling_data.get('error') == 'access_denied' |
| 109 | + ): |
93 | 110 | click.echo(polling_data['error_description']) |
94 | 111 | exit(1) |
95 | 112 |
|
96 | 113 | if polling_data.get('access_token') is not None: |
97 | | - save_config({ 'at': polling_data['access_token'], 'rt': polling_data['refresh_token'], 'id': polling_data['id_token'] }) |
| 114 | + save_credential( |
| 115 | + { |
| 116 | + 'access_token': polling_data['access_token'], |
| 117 | + 'refresh_token': polling_data['refresh_token'], |
| 118 | + 'id': polling_data['id_token'], |
| 119 | + } |
| 120 | + ) |
98 | 121 | click.echo("Login successful!") |
99 | 122 |
|
| 123 | + |
100 | 124 | @click.pass_obj |
101 | | -def save_config(obj, data): |
102 | | - obj['at'] = data['at'] |
103 | | - obj['rt'] = data['rt'] |
| 125 | +def save_credential(obj, data): |
| 126 | + obj['access_token'] = data['access_token'] |
| 127 | + obj['refresh_token'] = data['refresh_token'] |
104 | 128 | obj['id'] = data['id'] |
105 | | - CONFIG_FILE.write_text(json.dumps(data)) |
| 129 | + if not CREDENTIAL_FILE.exists(): |
| 130 | + CREDENTIAL_FILE.parent.mkdir(parents=True) |
| 131 | + CREDENTIAL_FILE.write_text(json.dumps(data)) |
| 132 | + |
106 | 133 |
|
107 | 134 | def authenticated(f): |
108 | 135 | @click.pass_obj |
109 | 136 | def wrapper(obj, *args, **kwargs): |
110 | | - token = obj and obj.get("at") |
| 137 | + token = obj and obj.get("access_token") |
111 | 138 |
|
112 | 139 | if token is None: |
113 | 140 | prompt_login() |
114 | 141 | exit(1) |
115 | 142 |
|
116 | 143 | res = requests.get( |
117 | 144 | url="https://{}/userinfo".format(ORG), |
118 | | - headers={ "Authorization": "Bearer {}".format(token) } |
| 145 | + headers={"Authorization": "Bearer {}".format(token)}, |
119 | 146 | ) |
120 | 147 |
|
121 | | - if (res.status_code == 200): |
| 148 | + if res.status_code == 200: |
122 | 149 | f(*args, **kwargs) |
123 | 150 | else: |
124 | 151 | prompt_login() |
125 | 152 | exit(1) |
126 | 153 |
|
127 | 154 | return update_wrapper(wrapper, f) |
128 | 155 |
|
| 156 | + |
129 | 157 | def prompt_login(): |
130 | 158 | click.echo("You're not logged in. Please run `aito login` first.") |
131 | | - |
|
0 commit comments