xiaohongshu
xiaohongshu
lightfriend-add-integration
Step-by-step guide for adding new OAuth integrations to Lightfriend
Full skill instructions
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.rsxiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience