hackgt_nfc/
api.rs

1use std::fmt;
2use url::Url;
3use graphql_client::{ GraphQLQuery, Response };
4
5#[doc(hidden)]
6pub enum Error {
7	Network(reqwest::Error),
8	Message(&'static str),
9	GraphQL(Vec<graphql_client::Error>),
10}
11impl fmt::Debug for Error {
12	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
13		match self {
14			Error::Network(err) => write!(f, "{:?}", err),
15			Error::Message(s) => write!(f, "{}", s),
16			Error::GraphQL(err) => write!(f, "{:?}", err),
17		}
18	}
19}
20impl From<reqwest::Error> for Error {
21	fn from(err: reqwest::Error) -> Error {
22		Error::Network(err)
23	}
24}
25impl From<&'static str> for Error {
26	fn from(err: &'static str) -> Error {
27		Error::Message(err)
28	}
29}
30
31#[derive(GraphQLQuery)]
32#[graphql(
33	schema_path = "schema.graphql",
34	query_path = "api.graphql",
35	response_derives = "Debug",
36)]
37struct UserSearch;
38
39#[derive(GraphQLQuery)]
40#[graphql(
41	schema_path = "schema.graphql",
42	query_path = "api.graphql",
43	response_derives = "Debug",
44)]
45struct UserGet;
46
47#[derive(GraphQLQuery)]
48#[graphql(
49	schema_path = "schema.graphql",
50	query_path = "api.graphql",
51	response_derives = "Debug",
52)]
53struct TagsGet;
54
55#[derive(GraphQLQuery)]
56#[graphql(
57	schema_path = "schema.graphql",
58	query_path = "api.graphql",
59	response_derives = "Debug",
60)]
61struct CheckInTag;
62pub type CheckInReturn = (bool, check_in_tag::UserData, check_in_tag::TagData);
63
64pub struct CheckinAPI {
65	base_url: Url,
66	client: reqwest::blocking::Client,
67	auth_cookie: String,
68}
69
70/// An implementation of the [HackGT Check-In](https://github.com/HackGT/checkin2) API
71impl CheckinAPI {
72	/// Log into the API using a username / password combination provided to you
73	///
74	/// Note: this will block for a few seconds because the server has a high PBKDF2 iteration count by default
75	pub fn login(username: &str, password: &str, url: &str) -> Result<Self, Error> {
76		let client = reqwest::blocking::Client::new();
77		let base_url = Url::parse(url).expect("Invalid base URL configured");
78
79		let params = [("username", username), ("password", password)];
80		let response = client.post(base_url.join("/api/user/login").unwrap())
81			.form(&params)
82			.send()?;
83
84		if !response.status().is_success() {
85			return Err("Invalid username or password".into());
86		}
87
88		let cookies = response.headers().get_all(reqwest::header::SET_COOKIE);
89		let mut auth_token: Option<String> = None;
90		let auth_regex = regex::Regex::new(r"^auth=(?P<token>[a-f0-9]+);").unwrap();
91		for cookie in cookies.iter() {
92			if let Ok(cookie) = cookie.to_str() {
93				if let Some(capture) = auth_regex.captures(cookie) {
94					auth_token = Some(capture["token"].to_owned());
95				}
96			}
97		}
98
99		match auth_token {
100			Some(mut token) => {
101				// Create a HTTP cookie header out of this token
102				token.insert_str(0, "auth=");
103				Ok(Self {
104					base_url,
105					client,
106					auth_cookie: token,
107				})
108			},
109			None => Err("No auth token set by server".into())
110		}
111	}
112
113	/// Create an API instance directly from an auth token
114	///
115	/// Can be used to instantly resume an API instance after having obtained a token previously
116	pub fn from_token(mut auth_token: String, url: &str) -> Self {
117		let client = reqwest::blocking::Client::new();
118		let base_url = Url::parse(url).expect("Invalid base URL configured");
119		// Create a HTTP cookie header out of this token
120		auth_token.insert_str(0, "auth=");
121		Self { base_url, client, auth_cookie: auth_token }
122	}
123
124	pub fn auth_token(&self) -> &str {
125		&self.auth_cookie[5..]
126	}
127
128	/// Creates a new user with the provided username / password combination
129	///
130	/// Can be used to provision sub-devices like with [checkin-embedded](https://github.com/HackGT/checkin-embedded)
131	pub fn add_user(&self, username: &str, password: &str) -> Result<(), Error> {
132		let params = [("username", username), ("password", password)];
133		let response = self.client.put(self.base_url.join("/api/user/update").unwrap())
134			.header(reqwest::header::COOKIE, self.auth_cookie.as_str())
135			.form(&params)
136			.send()?;
137
138		if !response.status().is_success() {
139			Err("Account creation unsuccessful".into())
140		}
141		else {
142			Ok(())
143		}
144	}
145
146	pub fn delete_user(&self, username: &str) -> Result<(), Error> {
147		let params = [("username", username)];
148		let response = self.client.delete(self.base_url.join("/api/user/update").unwrap())
149			.header(reqwest::header::COOKIE, self.auth_cookie.as_str())
150			.form(&params)
151			.send()?;
152
153		if !response.status().is_success() {
154			Err("Account deletion unsuccessful".into())
155		}
156		else {
157			Ok(())
158		}
159	}
160
161	fn checkin_action(&self, check_in: bool, uuid: &str, tag: &str) -> Result<CheckInReturn, Error> {
162		let body = CheckInTag::build_query(check_in_tag::Variables {
163			id: uuid.to_string(),
164			tag: tag.to_string(),
165			checkin: check_in,
166		});
167
168		let response: Response<check_in_tag::ResponseData> = self.client.post(self.base_url.join("/graphql").unwrap())
169			.header(reqwest::header::COOKIE, self.auth_cookie.as_str())
170			.json(&body)
171			.send()?
172			.json()?;
173
174		if let Some(errors) = response.errors {
175			return Err(Error::GraphQL(errors));
176		}
177		let data = match response.data {
178			Some(data) => data,
179			None => return Err("Check in API returned no data".into()),
180		};
181		let check_in_data = match data.check_in {
182			Some(check_in_data) => check_in_data,
183			None => return Err("Invalid user ID on badge".into()),
184		};
185		let user = check_in_data.user.user_data;
186		if !user.accepted || !user.confirmed {
187			return Err("User not accepted and confirmed".into());
188		}
189
190		let tag_details = check_in_data.tags.into_iter()
191			.map(|item| item.tag_data)
192			.find(|item| item.tag.name == tag)
193			.unwrap(); // API ensures the tag we requested will be in the response so this won't panic
194
195		Ok((
196			tag_details.checkin_success,
197			user,
198			tag_details
199		))
200	}
201
202	/// Check a user into a tag
203	///
204	/// Returns a three item tuple containing:
205	/// - Check in success (true / false)
206	/// - User information
207	/// - Tag information (for the tag specified)
208	pub fn check_in(&self, uuid: &str, tag: &str) -> Result<CheckInReturn, Error> {
209		self.checkin_action(true, uuid, tag)
210	}
211
212	/// Check a user out of tag
213	///
214	/// See documentation for `check_in` for more details
215	pub fn check_out(&self, uuid: &str, tag: &str) -> Result<CheckInReturn, Error> {
216		self.checkin_action(false, uuid, tag)
217	}
218
219	/// Get a list of tag names from the check-in instance
220	///
221	/// Can optionally be filtered to only include tags that are currently active (computed from `start` / `end` attributes in check-in database)
222	pub fn get_tags_names(&self, only_current: bool) -> Result<Vec<String>, Error> {
223		let body = TagsGet::build_query(tags_get::Variables {
224			only_current
225		});
226
227		let response: Response<tags_get::ResponseData> = self.client.post(self.base_url.join("/graphql").unwrap())
228			.header(reqwest::header::COOKIE, self.auth_cookie.as_str())
229			.json(&body)
230			.send()?
231			.json()?;
232
233		if let Some(errors) = response.errors {
234			return Err(Error::GraphQL(errors));
235		}
236		if response.data.is_none() {
237			return Err("Check in API returned no data".into());
238		}
239		Ok(
240			response.data.unwrap()
241				.tags.into_iter()
242				.map(|tag| tag.name)
243				.collect()
244		)
245	}
246}
247
248#[cfg(test)]
249mod checkin_api_tests {
250	use super::CheckinAPI;
251
252	#[test]
253	fn login() {
254		let username = std::env::var("CHECKIN_USERNAME").unwrap();
255		let password = std::env::var("CHECKIN_PASSWORD").unwrap();
256
257		let instance = CheckinAPI::login(&username, &password).unwrap();
258		assert_eq!(instance.auth_token().len(), 64);
259
260		instance.check_in("7dd00021-89fd-49f1-9c17-bd0ba7dcf97e", "123").unwrap();
261
262		instance.get_tags_names(true).unwrap();
263
264		instance.add_user("test_user", "just testing").unwrap();
265		instance.delete_user("test_user").unwrap();
266	}
267}