1use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum Plan {
10 Free,
11 Pro,
12 Team,
13}
14
15impl std::fmt::Display for Plan {
16 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17 match self {
18 Plan::Free => write!(f, "Free"),
19 Plan::Pro => write!(f, "Pro ($9/mo)"),
20 Plan::Team => write!(f, "Team ($49/mo)"),
21 }
22 }
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct Identity {
28 pub user_id: String,
29 pub email: String,
30 pub plan: Plan,
31 pub team_id: Option<String>,
32 pub team_name: Option<String>,
33}
34
35#[derive(Debug, Clone)]
37pub struct AuthToken {
38 token: String,
39 identity: Option<Identity>,
40}
41
42impl AuthToken {
43 pub fn new(token: String) -> Self {
45 Self {
46 token,
47 identity: None,
48 }
49 }
50
51 pub fn from_env() -> Option<Self> {
53 std::env::var("MUR_API_TOKEN")
54 .ok()
55 .map(Self::new)
56 }
57
58 pub async fn verify(&mut self) -> Result<&Identity> {
60 let client = reqwest::Client::new();
61 let response = client
62 .get("https://mur.run/api/v1/auth/me")
63 .header("Authorization", format!("Bearer {}", self.token))
64 .send()
65 .await
66 .context("Verifying token with mur.run")?;
67
68 if !response.status().is_success() {
69 anyhow::bail!("Authentication failed: {}", response.status());
70 }
71
72 let identity: Identity = response.json().await.context("Parsing identity")?;
73 self.identity = Some(identity);
74 Ok(self.identity.as_ref().expect("identity set after successful verify"))
75 }
76
77 pub fn identity(&self) -> Option<&Identity> {
79 self.identity.as_ref()
80 }
81
82 pub fn plan(&self) -> Plan {
84 self.identity
85 .as_ref()
86 .map(|i| i.plan.clone())
87 .unwrap_or(Plan::Free)
88 }
89
90 pub fn token(&self) -> &str {
91 &self.token
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn test_plan_display() {
101 assert_eq!(Plan::Free.to_string(), "Free");
102 assert_eq!(Plan::Pro.to_string(), "Pro ($9/mo)");
103 assert_eq!(Plan::Team.to_string(), "Team ($49/mo)");
104 }
105
106 #[test]
107 fn test_plan_serialization() {
108 let p = Plan::Pro;
109 let json = serde_json::to_string(&p).unwrap();
110 assert_eq!(json, "\"pro\"");
111 }
112
113 #[test]
114 fn test_auth_token_default_plan() {
115 let token = AuthToken::new("test".into());
116 assert_eq!(token.plan(), Plan::Free);
117 assert!(token.identity().is_none());
118 }
119}