Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 7 additions & 3 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ port: 3001
# Used for password reset links, payment redirect URLs, and batch notification emails
dashboard_url: "http://localhost:5173"

# Onboarding redirect URL for new users (first login)
# When set, users who have never logged in will be redirected here after authentication.
# onboarding_url: "https://onboarding.doubleword.ai"

# Database configuration
database:
# Configure connectivity to an external postgres database here
Expand Down Expand Up @@ -245,7 +249,7 @@ batches:
allowed_completion_windows:
- "24h"
- "1h"
# - "12h" # Uncomment to allow 12 hour
# - "12h" # Uncomment to allow 12 hour
# - "48h" # Uncomment to allow 48 hour

# Per-window relaxation factors for capacity acceptance.
Expand All @@ -256,8 +260,8 @@ batches:
# Keys must be present in allowed_completion_windows.
# Windows not listed here default to 1.0.
window_relaxation_factors:
"1h": 1.0 # strict — no over-acceptance for high priority window
"24h": 1.25 # 25% over-acceptance — time to provision more capacity
"1h": 1.0 # strict — no over-acceptance for high priority window
"24h": 1.25 # 25% over-acceptance — time to provision more capacity

# Allowed OpenAI-compatible URL paths for batch requests.
# This is used by both /files upload validation and /batches endpoint validation.
Expand Down
2 changes: 2 additions & 0 deletions dashboard/src/api/control-layer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,8 @@ export interface User {
user_type?: "individual" | "organization"; // User type
organizations?: OrganizationSummary[]; // only present when include=organizations or for current user
active_organization_id?: string; // only present for /users/current
last_login?: string | null; // ISO 8601 timestamp, null if user has never logged in
onboarding_redirect_url?: string; // only present for /users/current when last_login is null
}

export interface ApiKey {
Expand Down
33 changes: 18 additions & 15 deletions dashboard/src/contexts/auth/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ export function AuthProvider({ children }: AuthProviderProps) {
user,
);

// Redirect first-time users to onboarding if configured (server sets
// onboarding_redirect_url only when last_login is null). Org invite
// redirect params take priority.
if (user.onboarding_redirect_url) {
const urlRedirect = new URLSearchParams(window.location.search).get("redirect");
if (!urlRedirect) {
window.location.href = user.onboarding_redirect_url;
return;
}
}

// Determine auth method based on response headers or user data
const authMethod = user.auth_source === "native" ? "native" : "proxy";

Expand Down Expand Up @@ -73,28 +84,20 @@ export function AuthProvider({ children }: AuthProviderProps) {
}, [isDemoMode, isMswReady, checkAuthStatus]);

const login = async (credentials: LoginCredentials) => {
const response = await dwctlApi.auth.login(credentials);
await dwctlApi.auth.login(credentials);

setAuthState({
user: response.user,
isAuthenticated: true,
isLoading: false,
authMethod: "native",
});
// Re-fetch current user to pick up onboarding redirect and full user data
await checkAuthStatus();

// Invalidate user queries to refresh data
queryClient.invalidateQueries({ queryKey: queryKeys.users.all });
};

const register = async (credentials: RegisterCredentials) => {
const response = await dwctlApi.auth.register(credentials);

setAuthState({
user: response.user,
isAuthenticated: true,
isLoading: false,
authMethod: "native",
});
await dwctlApi.auth.register(credentials);

// Re-fetch current user to pick up onboarding redirect and full user data
await checkAuthStatus();

// Invalidate user queries to refresh data
queryClient.invalidateQueries({ queryKey: queryKeys.users.all });
Expand Down
7 changes: 7 additions & 0 deletions dwctl/src/api/handlers/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,13 @@ pub async fn get_user<P: PoolProvider>(
// Include active organization for /users/current requests
if is_current {
response = response.with_active_organization(current_user.active_organization);

// Include onboarding redirect URL for first-time users (last_login is null)
if response.last_login.is_none()
&& let Some(url) = &state.config.onboarding_url
{
response = response.with_onboarding_redirect_url(url.clone());
}
Comment thread
sejori marked this conversation as resolved.
}

Ok(Json(response))
Expand Down
12 changes: 11 additions & 1 deletion dwctl/src/api/models/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ pub struct UserResponse {
#[serde(skip_serializing_if = "Option::is_none")]
#[schema(value_type = Option<String>, format = "uuid")]
pub active_organization_id: Option<UserId>,
/// Onboarding redirect URL (only present for /users/current when last_login is null and onboarding_url is configured)
#[serde(skip_serializing_if = "Option::is_none")]
pub onboarding_redirect_url: Option<String>,
}

/// Query parameters for listing users
Expand Down Expand Up @@ -221,7 +224,7 @@ impl From<UserDBResponse> for UserResponse {
updated_at: db.updated_at,
auth_source: db.auth_source,
external_user_id: db.external_user_id,
last_login: None, // UserDBResponse doesn't have last_login
last_login: db.last_login,
groups: None, // By default, relationships are not included
credit_balance: None, // By default, credit balances are not included
has_payment_provider_id: db.payment_provider_id.as_ref().is_some_and(|s| !s.is_empty()),
Expand All @@ -234,6 +237,7 @@ impl From<UserDBResponse> for UserResponse {
user_type: db.user_type,
organizations: None,
active_organization_id: None,
onboarding_redirect_url: None,
}
}
}
Expand Down Expand Up @@ -262,6 +266,12 @@ impl UserResponse {
self.active_organization_id = id;
self
}

/// Set the onboarding redirect URL (for first-time users)
pub fn with_onboarding_redirect_url(mut self, url: String) -> Self {
self.onboarding_redirect_url = Some(url);
self
}
}

impl From<UserDBResponse> for CurrentUser {
Expand Down
Loading
Loading