check if github token is valid on page load and trigger re-auth flow if not (#120)
This commit is contained in:
@@ -2,7 +2,7 @@ use axum::{
|
||||
extract::{Request, State},
|
||||
middleware::Next,
|
||||
response::{Json as ResponseJson, Response},
|
||||
routing::post,
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ pub fn auth_router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/auth/github/device/start", post(device_start))
|
||||
.route("/auth/github/device/poll", post(device_poll))
|
||||
.route("/auth/github/check", get(github_check_token))
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
@@ -263,6 +264,40 @@ async fn device_poll(
|
||||
}
|
||||
}
|
||||
|
||||
/// GET /auth/github/check
|
||||
async fn github_check_token(State(app_state): State<AppState>) -> ResponseJson<ApiResponse<()>> {
|
||||
let config = app_state.get_config().read().await;
|
||||
let token = config.github.token.clone();
|
||||
drop(config);
|
||||
if let Some(token) = token {
|
||||
let client = reqwest::Client::new();
|
||||
let res = client
|
||||
.get("https://api.github.com/user")
|
||||
.bearer_auth(&token)
|
||||
.header("User-Agent", "vibe-kanban-app")
|
||||
.send()
|
||||
.await;
|
||||
match res {
|
||||
Ok(r) if r.status().is_success() => ResponseJson(ApiResponse {
|
||||
success: true,
|
||||
data: None,
|
||||
message: Some("GitHub token is valid".to_string()),
|
||||
}),
|
||||
_ => ResponseJson(ApiResponse {
|
||||
success: false,
|
||||
data: None,
|
||||
message: Some("github_token_invalid".to_string()),
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
ResponseJson(ApiResponse {
|
||||
success: false,
|
||||
data: None,
|
||||
message: Some("github_token_invalid".to_string()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Middleware to set Sentry user context for every request
|
||||
pub async fn sentry_user_context_middleware(
|
||||
State(app_state): State<AppState>,
|
||||
|
||||
@@ -407,6 +407,9 @@ pub async fn create_github_pr(
|
||||
e
|
||||
);
|
||||
let message = match &e {
|
||||
crate::models::task_attempt::TaskAttemptError::GitHubService(
|
||||
crate::services::GitHubServiceError::TokenInvalid,
|
||||
) => Some("github_token_invalid".to_string()),
|
||||
crate::models::task_attempt::TaskAttemptError::Git(err)
|
||||
if err.message().contains("status code: 403") =>
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@ pub enum GitHubServiceError {
|
||||
Repository(String),
|
||||
PullRequest(String),
|
||||
Branch(String),
|
||||
TokenInvalid,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for GitHubServiceError {
|
||||
@@ -22,6 +23,7 @@ impl std::fmt::Display for GitHubServiceError {
|
||||
GitHubServiceError::Repository(e) => write!(f, "Repository error: {}", e),
|
||||
GitHubServiceError::PullRequest(e) => write!(f, "Pull request error: {}", e),
|
||||
GitHubServiceError::Branch(e) => write!(f, "Branch error: {}", e),
|
||||
GitHubServiceError::TokenInvalid => write!(f, "GitHub token is invalid or expired."),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +32,22 @@ impl std::error::Error for GitHubServiceError {}
|
||||
|
||||
impl From<octocrab::Error> for GitHubServiceError {
|
||||
fn from(err: octocrab::Error) -> Self {
|
||||
GitHubServiceError::Client(err)
|
||||
match &err {
|
||||
octocrab::Error::GitHub { source, .. } => {
|
||||
let status = source.status_code.as_u16();
|
||||
let msg = source.message.to_ascii_lowercase();
|
||||
if status == 401
|
||||
|| status == 403
|
||||
|| msg.contains("bad credentials")
|
||||
|| msg.contains("token expired")
|
||||
{
|
||||
GitHubServiceError::TokenInvalid
|
||||
} else {
|
||||
GitHubServiceError::Client(err)
|
||||
}
|
||||
}
|
||||
_ => GitHubServiceError::Client(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,11 +178,27 @@ impl GitHubService {
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
octocrab::Error::GitHub { source, .. } => GitHubServiceError::PullRequest(format!(
|
||||
"GitHub API error: {} (status: {})",
|
||||
source.message,
|
||||
source.status_code.as_u16()
|
||||
)),
|
||||
octocrab::Error::GitHub { source, .. } => {
|
||||
if source.status_code.as_u16() == 401
|
||||
|| source.status_code.as_u16() == 403
|
||||
|| source
|
||||
.message
|
||||
.to_ascii_lowercase()
|
||||
.contains("bad credentials")
|
||||
|| source
|
||||
.message
|
||||
.to_ascii_lowercase()
|
||||
.contains("token expired")
|
||||
{
|
||||
GitHubServiceError::TokenInvalid
|
||||
} else {
|
||||
GitHubServiceError::PullRequest(format!(
|
||||
"GitHub API error: {} (status: {})",
|
||||
source.message,
|
||||
source.status_code.as_u16()
|
||||
))
|
||||
}
|
||||
}
|
||||
_ => GitHubServiceError::PullRequest(format!("Failed to create PR: {}", e)),
|
||||
})?;
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import { GitHubLoginDialog } from '@/components/GitHubLoginDialog';
|
||||
const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes);
|
||||
|
||||
function AppContent() {
|
||||
const { config, updateConfig, loading } = useConfig();
|
||||
const { config, updateConfig, loading, githubTokenInvalid } = useConfig();
|
||||
const [showDisclaimer, setShowDisclaimer] = useState(false);
|
||||
const [showOnboarding, setShowOnboarding] = useState(false);
|
||||
const [showGitHubLogin, setShowGitHubLogin] = useState(false);
|
||||
@@ -36,9 +36,12 @@ function AppContent() {
|
||||
}
|
||||
const notAuthenticated =
|
||||
!config.github?.username || !config.github?.token;
|
||||
setShowGitHubLogin(notAuthenticated);
|
||||
setShowGitHubLogin(notAuthenticated || githubTokenInvalid);
|
||||
}
|
||||
}, [config]);
|
||||
if (githubTokenInvalid) {
|
||||
setShowGitHubLogin(true);
|
||||
}
|
||||
}, [config, githubTokenInvalid]);
|
||||
|
||||
const handleDisclaimerAccept = async () => {
|
||||
if (!config) return;
|
||||
|
||||
@@ -18,7 +18,7 @@ export function GitHubLoginDialog({
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}) {
|
||||
const { config, loading } = useConfig();
|
||||
const { config, loading, githubTokenInvalid } = useConfig();
|
||||
const [fetching, setFetching] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [deviceState, setDeviceState] = useState<null | {
|
||||
@@ -31,7 +31,9 @@ export function GitHubLoginDialog({
|
||||
const [polling, setPolling] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const isAuthenticated = !!(config?.github?.username && config?.github?.token);
|
||||
const isAuthenticated =
|
||||
!!(config?.github?.username && config?.github?.token) &&
|
||||
!githubTokenInvalid;
|
||||
|
||||
const handleLogin = async () => {
|
||||
setFetching(true);
|
||||
|
||||
@@ -14,6 +14,7 @@ interface ConfigContextType {
|
||||
updateAndSaveConfig: (updates: Partial<Config>) => void;
|
||||
saveConfig: () => Promise<boolean>;
|
||||
loading: boolean;
|
||||
githubTokenInvalid: boolean;
|
||||
}
|
||||
|
||||
const ConfigContext = createContext<ConfigContextType | undefined>(undefined);
|
||||
@@ -25,6 +26,7 @@ interface ConfigProviderProps {
|
||||
export function ConfigProvider({ children }: ConfigProviderProps) {
|
||||
const [config, setConfig] = useState<Config | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [githubTokenInvalid, setGithubTokenInvalid] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
@@ -45,6 +47,26 @@ export function ConfigProvider({ children }: ConfigProviderProps) {
|
||||
loadConfig();
|
||||
}, []);
|
||||
|
||||
// Check GitHub token validity after config loads
|
||||
useEffect(() => {
|
||||
if (loading) return;
|
||||
const checkToken = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/auth/github/check');
|
||||
const data: ApiResponse<null> = await response.json();
|
||||
if (!data.success && data.message === 'github_token_invalid') {
|
||||
setGithubTokenInvalid(true);
|
||||
} else {
|
||||
setGithubTokenInvalid(false);
|
||||
}
|
||||
} catch (err) {
|
||||
// If the check fails, assume token is invalid
|
||||
setGithubTokenInvalid(true);
|
||||
}
|
||||
};
|
||||
checkToken();
|
||||
}, [loading]);
|
||||
|
||||
const updateConfig = useCallback((updates: Partial<Config>) => {
|
||||
setConfig((prev) => (prev ? { ...prev, ...updates } : null));
|
||||
}, []);
|
||||
@@ -100,7 +122,14 @@ export function ConfigProvider({ children }: ConfigProviderProps) {
|
||||
|
||||
return (
|
||||
<ConfigContext.Provider
|
||||
value={{ config, updateConfig, saveConfig, loading, updateAndSaveConfig }}
|
||||
value={{
|
||||
config,
|
||||
updateConfig,
|
||||
saveConfig,
|
||||
loading,
|
||||
updateAndSaveConfig,
|
||||
githubTokenInvalid,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ConfigContext.Provider>
|
||||
|
||||
Reference in New Issue
Block a user