بنقرة واحدة
lightfriend-add-integration
Step-by-step guide for adding new OAuth integrations to Lightfriend
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Step-by-step guide for adding new OAuth integrations to Lightfriend
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Quickly commit, push, and create a PR while leaving validation to GitHub checks
Quickly commit and push local changes while leaving validation to GitHub checks
Step-by-step guide for adding new pages to the Yew frontend
Step-by-step guide for modifying database schema using Diesel migrations
Run formatting and linting checks on the codebase
Update Lightfriend documentation and create commit
| name | lightfriend-add-integration |
| description | Step-by-step guide for adding new OAuth integrations to Lightfriend |
This skill guides you through adding a new OAuth service integration (e.g., Google Calendar, Spotify, GitHub) to Lightfriend.
A complete integration includes:
First, create a table to store the OAuth credentials:
cd backend && diesel migration generate add_{service}_connection
Edit up.sql:
CREATE TABLE {service}_connection (
id INTEGER PRIMARY KEY NOT NULL,
user_id INTEGER NOT NULL,
access_token TEXT NOT NULL, -- Encrypted
refresh_token TEXT, -- Encrypted (if applicable)
expires_at TEXT, -- ISO 8601 timestamp
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX idx_{service}_connection_user_id ON {service}_connection(user_id);
Edit down.sql:
DROP TABLE {service}_connection;
Run migration:
cd backend && diesel migration run
cd backend && diesel print-schema > src/schema.rs
In backend/src/models/user_models.rs, add:
#[derive(Queryable, Insertable, Debug, Clone)]
#[diesel(table_name = {service}_connection)]
pub struct {Service}Connection {
pub id: i32,
pub user_id: i32,
pub access_token: String, // Encrypted
pub refresh_token: Option<String>, // Encrypted
pub expires_at: Option<String>,
pub created_at: String,
}
#[derive(Insertable)]
#[diesel(table_name = {service}_connection)]
pub struct New{Service}Connection {
pub user_id: i32,
pub access_token: String,
pub refresh_token: Option<String>,
pub expires_at: Option<String>,
}
In backend/src/repositories/connection_auth.rs, add methods:
use crate::schema::{service}_connection;
use crate::models::user_models::{Service}Connection, New{Service}Connection;
pub fn create_{service}_connection(
conn: &mut SqliteConnection,
new_connection: New{Service}Connection,
) -> Result<{Service}Connection, diesel::result::Error> {
diesel::insert_into({service}_connection::table)
.values(&new_connection)
.get_result(conn)
}
pub fn get_{service}_connection(
conn: &mut SqliteConnection,
user_id: i32,
) -> Result<{Service}Connection, diesel::result::Error> {
{service}_connection::table
.filter({service}_connection::user_id.eq(user_id))
.first(conn)
}
pub fn update_{service}_tokens(
conn: &mut SqliteConnection,
user_id: i32,
access_token: &str,
refresh_token: Option<&str>,
expires_at: Option<&str>,
) -> Result<usize, diesel::result::Error> {
diesel::update({service}_connection::table)
.filter({service}_connection::user_id.eq(user_id))
.set((
{service}_connection::access_token.eq(access_token),
{service}_connection::refresh_token.eq(refresh_token),
{service}_connection::expires_at.eq(expires_at),
))
.execute(conn)
}
pub fn delete_{service}_connection(
conn: &mut SqliteConnection,
user_id: i32,
) -> Result<usize, diesel::result::Error> {
diesel::delete({service}_connection::table)
.filter({service}_connection::user_id.eq(user_id))
.execute(conn)
}
Create backend/src/handlers/{service}_auth.rs:
use axum::{Extension, extract::Query, response::Redirect, http::StatusCode};
use serde::Deserialize;
use crate::repositories::connection_auth;
use crate::utils::encryption::{encrypt_data, decrypt_data};
use crate::models::AppState;
#[derive(Deserialize)]
pub struct {Service}AuthQuery {
code: String,
state: Option<String>,
}
// Step 1: Redirect to OAuth provider
pub async fn {service}_oauth_start(
Extension(user_id): Extension<i32>,
) -> Result<Redirect, StatusCode> {
let client_id = std::env::var("{SERVICE}_CLIENT_ID")
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let redirect_uri = format!(
"{}/api/{service}/oauth/callback",
std::env::var("BACKEND_URL").unwrap_or_default()
);
let auth_url = format!(
"https://{service}.com/oauth/authorize?client_id={}&redirect_uri={}&response_type=code&scope={}",
client_id,
urlencoding::encode(&redirect_uri),
"read write" // Adjust scopes as needed
);
Ok(Redirect::to(&auth_url))
}
// Step 2: Handle OAuth callback
pub async fn {service}_oauth_callback(
Extension(user_id): Extension<i32>,
Extension(state): Extension<AppState>,
Query(query): Query<{Service}AuthQuery>,
) -> Result<Redirect, StatusCode> {
let client_id = std::env::var("{SERVICE}_CLIENT_ID")
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let client_secret = std::env::var("{SERVICE}_CLIENT_SECRET")
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
// Exchange code for tokens
let client = reqwest::Client::new();
let token_response = client
.post("https://{service}.com/oauth/token")
.form(&[
("grant_type", "authorization_code"),
("code", &query.code),
("client_id", &client_id),
("client_secret", &client_secret),
("redirect_uri", &format!("{}/api/{service}/oauth/callback",
std::env::var("BACKEND_URL").unwrap_or_default())),
])
.send()
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.json::<serde_json::Value>()
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let access_token = token_response["access_token"]
.as_str()
.ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
let refresh_token = token_response["refresh_token"].as_str();
let expires_in = token_response["expires_in"].as_i64();
// Encrypt tokens
let encrypted_access = encrypt_data(access_token)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let encrypted_refresh = refresh_token
.map(|t| encrypt_data(t))
.transpose()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let expires_at = expires_in.map(|sec| {
chrono::Utc::now()
.checked_add_signed(chrono::Duration::seconds(sec))
.unwrap()
.to_rfc3339()
});
// Store in database
let mut conn = state.pool.get()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let new_connection = connection_auth::New{Service}Connection {
user_id,
access_token: encrypted_access,
refresh_token: encrypted_refresh,
expires_at,
};
connection_auth::create_{service}_connection(&mut conn, new_connection)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Redirect::to(&format!("{}/connections",
std::env::var("FRONTEND_URL").unwrap_or_default())))
}
// Disconnect
pub async fn {service}_disconnect(
Extension(user_id): Extension<i32>,
Extension(state): Extension<AppState>,
) -> Result<StatusCode, StatusCode> {
let mut conn = state.pool.get()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
connection_auth::delete_{service}_connection(&mut conn, user_id)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(StatusCode::OK)
}
In backend/src/main.rs, add routes:
use handlers::{service}_auth;
// In the router setup:
.route("/api/{service}/oauth/start",
get({service}_auth::{service}_oauth_start)
.layer(middleware::from_fn_with_state(app_state.clone(), require_auth)))
.route("/api/{service}/oauth/callback",
get({service}_auth::{service}_oauth_callback)
.layer(middleware::from_fn_with_state(app_state.clone(), require_auth)))
.route("/api/{service}/disconnect",
post({service}_auth::{service}_disconnect)
.layer(middleware::from_fn_with_state(app_state.clone(), require_auth)))
Create frontend/src/connections/{service}.rs:
use yew::prelude::*;
use gloo_net::http::Request;
use crate::config;
#[function_component(ServiceConnection)]
pub fn {service}_connection() -> Html {
let connected = use_state(|| false);
let loading = use_state(|| true);
// Check connection status on mount
{
let connected = connected.clone();
let loading = loading.clone();
use_effect_with((), move |_| {
wasm_bindgen_futures::spawn_local(async move {
// Check if connected via API call
// Set connected state accordingly
loading.set(false);
});
});
}
let on_connect = {
let token = /* get from context */;
Callback::from(move |_| {
let backend_url = config::get_backend_url();
web_sys::window()
.unwrap()
.location()
.set_href(&format!("{}/api/{service}/oauth/start", backend_url))
.unwrap();
})
};
let on_disconnect = {
let connected = connected.clone();
let token = /* get from context */;
Callback::from(move |_| {
let connected = connected.clone();
wasm_bindgen_futures::spawn_local(async move {
let _ = Request::post(&format!("{}/api/{service}/disconnect", config::get_backend_url()))
.header("Authorization", &format!("Bearer {}", token))
.send()
.await;
connected.set(false);
});
})
};
html! {
<div class="connection-card">
<h3>{"Service Integration"}</h3>
if *loading {
<p>{"Loading..."}</p>
} else if *connected {
<button onclick={on_disconnect}>{"Disconnect"}</button>
} else {
<button onclick={on_connect}>{"Connect"}</button>
}
</div>
}
}
In frontend/src/main.rs, add the connection component to the connections page or create a dedicated route.
In backend/.env:
{SERVICE}_CLIENT_ID=your_client_id
{SERVICE}_CLIENT_SECRET=your_client_secret
pub async fn refresh_{service}_token(
user_id: i32,
state: &AppState,
) -> Result<String, Box<dyn std::error::Error>> {
let mut conn = state.pool.get()?;
let connection = connection_auth::get_{service}_connection(&mut conn, user_id)?;
let refresh_token = decrypt_data(&connection.refresh_token.unwrap())?;
// Exchange refresh token for new access token
// Update database with new tokens
// Return decrypted access token
}
pub async fn call_{service}_api(
user_id: i32,
state: &AppState,
endpoint: &str,
) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
let mut conn = state.pool.get()?;
let connection = connection_auth::get_{service}_connection(&mut conn, user_id)?;
let access_token = decrypt_data(&connection.access_token)?;
let client = reqwest::Client::new();
let response = client
.get(&format!("https://api.{service}.com{}", endpoint))
.bearer_auth(access_token)
.send()
.await?
.json()
.await?;
Ok(response)
}
utils/encryption.rs